Increasing complexities

When we go through the process of software design we usually follow some path from high-level solution design to component designs. There’s an idealistic mindset that we’d want to start with a technical design that would look something like this:

Very high-level design

Seems simple enough, we have a front-end talking to a backend that talks to our database. Our next step would be to define an architecturally sound design that defines the patterns we’d implement to enable functionality between these components. We might end up with something more complex like this:

More detailed component level design

Immediately we see a lot more complexity.

To be clear, I’m not trying to say complexity should be avoided or that complexity is a bad thing. I’m trying to advocate a delay in introducing complexity.

To further expand on this, let’s look at the next design that our tech lead or senior engineer might get involved with:

A design detailing specific systems involved in the overall architecture

To be clear, at this point we haven’t touched a single bit of code, but we’re already prescribing which technologies we plan on using. We often find ourselves wanting to take the design even further until we end up with a bunch of empty code blocks that simply has a method name.

public int WriteMessage(string message)
{
    // Write message to DB and return the ID
    
    
}

public string GetMessage(int id)
{
    // Get message from DB using ID


} 

Mistakes were made

It’s only once an engineer starts filling out the code blocks that they might realise the design have been overcomplicated, and we could have done this in a simpler fashion.

That’s where simply delaying some of these complexities could have greatly benefited us.

A better approach

Let’s go back to our original design. Very high-level design Feels like a simpler time, right? Let’s pretend that our instructions were as simple as

We need to write a message, get an ID and eventually retrieve that message based on its ID.

It sounds insane but this simple instruction can often spiral into outward complexities as we try to account for the unknown.

The unknown can be split into two categories, namely questions we can get answers to and questions we can’t get answers to…yet. The former looking kind of like the following:

  • What if we need some layer of authentication?
  • Could the message contain some form of object notation?
  • The front-end might need higher a level of responsiveness.
  • What about logging and auditing requests for existing messages?
  • Will requests from the system need to feed into our event streams?

The latter might seem more like this:

  • What would be the best database system for the messages?
  • How could we make this system endlessly extensible in case we need to store more than just messages?
  • Does the read/write speed of our middleware have a strong impact on the frontend?

These latter questions we’ll often only get answers to well into the development cycle. Our database question could only be answered once we’ve done extensive testing locally, using flat files, we might realise that we don’t really need something more complicated.

As long as we follow SOLID design principles we can account for incoming complexities down the line, but let’s not account for them before they arrive - they might not arrive at all.

Outside of coding

We recently created new agile projects and as part of doing so we were asked many, many different questions like

  • “What issue types do you required?”
  • “What statuses/columns do you need?”
  • “How should your workflows look?”

As a team we were debating the above questions and trying to think of future and past scenarios and how they would play into the answers to these questions.

Eventually I simply said let’s keep it as simple as we possibly can. Keep the default issue types, the default statuses, and the default workflow. If complexity comes knocking on our doors further down the line, let’s first see if we can’t mitigate that complexity into our existing setup and only complicate the setup if we have absolutely no choice.

By trying to predict future scenarios we create complexity that we might never need. Delay that complexity until you have no choice but to face it, and you’ll likely face less complexity than you would have otherwise.

Unavoidable Complexity

This is in no way a means to avoid proper planning. And with proper planning comes certain complexity. Perhaps you operate under business constraints of having to use AzureSQL, or your tech strategy defines that APIs are out of the question and everything should be event driven.

These will introduce complexity that we can’t avoid, but these are questions we can also answer concretely from the outlay.

Conclusion

Don’t try and plan for every eventuality. In software development this is near impossible, especially given shifting requirements, priorities, and technologies. Plan for what you know, and wait for the unknown before you account for it.

If you’re adopting a new pet, you know what you need to take care of it immediately. No need to plan for the possibility that they might turn out to be a talking dog that wants to host their own podcast. Leave the recording setup on the shelf for now.