This blog post has first been published in the Qafoo blog and is duplicated here since I wrote it or participated in writing it.
Cover image for post Developers Life is a Trade-Off

Developers Life is a Trade-Off

At Qafoo, we train a lot of people on topics like object oriented software design, automated testing and more. It happens quite often that an attendee asks questions like "Which is the best solution for problem class $x?", "What is the optimal tool for task $y" or "There is a new technology $z, is that the future of web development?". Some are disappointed when I reply "It depends" or "That does not exist", but that's the truth.

There is no silver bullet and one of the most important skills every developer needs to hone is to assess possibilities and to find the best trade-off for the current challenge.

To make that point clear I'm giving three examples from my personal experience, some where it went well and some where it did not.

The NoSQL Dilemma

Choosing a storage system is probably the most common decision to be made in any web project. For one project we already knew that there will be a large amount of data, so we started RnD on recent storage solutions and NoSQL was the big topic then. Cassandra appeared to suite our needs perfectly from its description. So we gave it a try and prototyped against it, using a thin class layer that hid the database detail.

It was a wise decision to create that layer whereas going with Cassandra from the start was not a very good one. While the database met our expectations, it crashed multiple times with loosing data in our development VMs and none of us had a clue, why, or more importantly, how to fix it. So we revised that decision and eventually went with MySQL and a denormalized schema, where only important attributes became dedicated table fields and the main data structure was serialized JSON in blob. That worked fine for several years.

What did we do? We found the solution which promised to fit our requirements quite well. Luckily, we did not jump blindly on it, but kept it a bit away from the system and evaluated further. Constraints like "knowledge on maintenance" were eventually considered and so we made a trade-off between these requirements that pushed us in contrary directions.

Overengineered State Machines

You don't need to jump on such a high-level bandwagon to find examples on where you need to make trade-offs. In one project I created a component to match our business model against a 3rd party one to use their data in our system. While the models were similar, a fundamental difference was how it was determined when data was published and deleted.

That problem appeared perfect for applying a state machine to it to trigger changes on our side when the 3rd party model changed. So I went the extra mile by creating an abstract state machine with Node and Transition classes. A transition was triggered through an abstract Condition and a node fired an Event which eventually triggered that change in our model. It was beautiful code in my eyes, with really small classes (1-2 lines executable code). To "ease" configuration and since I expected frequent changes, I made it configurable in XML.

Some time later, a co-worker needed to check if the implemented logic still met new requirements. Digging through that, starting from the XML configuration, running through so many classes to get the complete picture of what it did, took him a large amount of time. It turned out that the logic was fine, but the amount of time spent re-occured some more times after that experience.

A year later, we needed to finally adjust the logic. But the model did not fit the new requirement well, so that we needed to implement many additional classes and the XML configuration became even more complex.

What went wrong? I did not evaluate all available options and pick a good trade-off for the situation. As a minimal solution, I could have hidden the state processing behind a class and code it straight with nested if conditions. Maybe that would have been 40 lines of code, but a good bunch of unit tests could have covered that. Maybe a solution in between, abstracting only the states and hard-coding the trigger and transition logic, would have been the optimal trade-off.

So, what's the moral of the story? Whenever you take a software design decision in your project, there is a ton of possible solutions. Just picking a random, interesting, clean, … one is most probably not the right choice. Instead, you need to check your actual constraints and then find the best trade-off between the possibilities. Which one that is can vary greatly.

Hack Hack Hack

There are times in a project, where the option of just hacking a problem solution into the core of the system looks viable. Be warned explicitly: only do that with great care and foresight. In many environments that can lead to stacked up technical debt and possibly result in a really hard to maintain project.

In that project we had an import workflow which performed time-costly analysis on the imported data. It worked well as the incoming chunks of data were small. But then a party registered which provided huge chunks of data and the system became unresponsive. Not only that their imports were delayed, but it affected all other involved parties, too. On the other hand, that big-chunk-party did not even need all the analysis we performed on their data.

A clean solution would have been to create a configuration flag for the kinds of analysis. Another one would have introduced sharding for the import process, that was already in the backlog with low priority. Both solutions would have required too much time, we needed to fix the actual problem immediately. So we hacked a condition into a prominent place in the core, skipping the expensive analysis for the big-chunk-party.

I had an eye on that code quite for a long time, being prepared to refactor it whenever viable. But there was no need. The code worked fine, nobody needed to touch it again and the problem was solved. Eventually, the expensive analysis was not necessary at all anymore, so we removed it and with it the ugly hack.

What did we actually do? We made a trade-off. That one was very much in one of the possible directions, but because we knew of the potential problems and were prepared to revise the decision, we could take the risk.

Bottom Line

One of the most important tasks of a developer is to make trade-offs. They occur wherever you look in your every day life. It is a highly important step to realize and accept this. And it is important to hone that skill. You need to open your mind for new technology and techniques, learn and try them wherever you can. But then you need to step back, analyze the current situation and then find the best trade-off between all possible approaches. In addition, you need to be able to reflect your decisions and be prepared to revise them. The plus side: by doing so, you will surely learn something for all the upcoming trade-offs.