A practical approach to software architecture
When designing software for large systems, it’s often easy to know when you have arrived to a good architecture. It feels too simple given the amount of time you spent working on it, yet it solves every problem you throw at it.
I am often asked how to get there, so this post is a deconstruction of my method to design software. For the sake of simplicity, I will assume that the architecture you are building is not a complete greenfield project. Although if it is, I think most of the methods would apply there too.
As you know, I believe in the evolutionary school of software architecture, so my process is iterative. From a high level, I evolve an existing architecture based on whether it solves the problem at hand. As all evolutionary processes, every step has a purpose: environment, fitness function, selection and mutation.
In details, that’s how it goes:
Step 1: catalogue all the requirements that the software needs to address, ranked in rough order of importance.
Step 2: draw the architecture for the existing system. For every requirement listed in step 1, look at the areas that need to change to solve the problem. If the solution to a problem changes a single component, move to the next in the list. If all the major requirements only change a single part of the architecture, the architecture is stable and you are finished.
Step 3: Consider the first requirement that involves changing many parts. Ask yourself which systems are impacted. Describe what concept was missing as a standalone component, but was partially implemented throughout the other systems. At this stage it is useful to consider all the previous iterations of the architecture and see how they solve the problem differently.
Step 4: Gather the behaviour in a single component, and remove it from the others. Some parts will change a lot, and maybe other duplication will start appearing. Split and merge components at will, the goal is to think of the system differently.
Go back to step 2 with this new architecture.
Explanations and gotchas
The first step is actually the most critical part of the process: having an exhaustive list of all the problems is what ensures the fitness of your solution. I have seen countless developers wanting to solve only for the most important part of the problem. They invariably build software that is too brittle, as every new requirement breaks the original paradigm.
Write down every requirement, even the crazy ones that you probably won’t have to support for years. Taking those into account will make the resulting architecture simpler. Simple architectures are often composed of basic building blocks rather than purpose-built components. Complex behaviour should emerge from a combination of basic components instead.
The second step requires a precise understanding of the current or proposed architecture. A whiteboard should be enough to describe the architecture. It holds more than you can keep in your head, but not too much that it’s intractable. With a bit of practice (and several iterations of the loop), you will have integrated the list of requirements in your head. Identifying the brittle areas that a new problem shatters should be a matter of scribbling a different coloured marker on the whiteboard.
In contrast with the global view that step 2 demands, step 3 requires digging into the components; it is much more narrowly focused. Sometimes, existing components have an obvious coupling or mixed behaviour that it is easy to extract. More often the answer lies into the code. This often means reverse engineering the actual behaviour rather than believing the documentation. I found myself many times over believing that a particular module behaved a certain way until I read the code and realized what it was actually doing.
Step 4 is the creative part of the process, and involves mutating the architecture. This is what I feel the slowest at. Don’t be shy, the stuff you can come up with does not need to be better at every step. Backtracking is the norm, not the exception. If for some reason you are stuck, go back to step 2 and tackle another requirement first; this often helps.
When is it done ?
You will know when you are done.
The diagram on your whiteboard is harmonious, and the concepts written on it are roughly at the same level of abstraction.
You are done when every new requirement only causes a localized change in the architecture. When you feel almost ashamed to explain it to a colleague because they will think it’s too simple. The best compliment you can get is “well yeah, it’s kinda obvious” because this means you modelled it in an intuitive way.
This is both simple and hard, and requires a few abilities:
- To keep track of large amounts of information (requirements) in your head.
- To be able to think abstractly, then being able to dive into finer implementation details.
- To be creative with solutions.
And it takes time.
That’s the way I have been approaching software architecture, and it has been working pretty well so far. That said, like every process it should evolve, so I’d love to hear other perspectives on this.