Reading The Architecture of Open Source Applications, especially Chapter 4, struck a cord with me:
The authors, Margo Seltzer and Keith Bostic, state, that building systems based on simple components as building blocks lead to systems which are superior to monoliths in the five important -abilities: understandability, extensibility, maintainability, testability, and flexibility.
This made me wonder how to clearly differentiate between these abilities, since many discussions about software architecture evolve around one or more of these abilities.
Also the abilities are rather generic, making them applicable to system design in general, so I consider a strong understanding of them essential to be able to perform well.
Here are some definitions & thoughts:
understandability
describes if the code clearly communicates its purpose to readers other then the author.
This is important because code is read more often than written and if your code is hard to understand it will take up more time to read, or worse, it won’t be understood at all; Problem is this is a subjective metric - it depends on the knowledge & background of the reader.
To me, this effectively comes down to:
- don’t be clever.
- make implicit assumptions explicit.
- optimize for readability.
extensibility
speaks to a system being able to accommodate new functionality or changes over time, without requiring bigger rewrites of existing code.
I think every software engineer has experienced changes cascading through their code base. Yet I think it’s hard to avoid sometimes. When building a system, chances are you don’t know every thing up front, meaning you can always run into this situation.
While I think extensibility can be achieved in parts by building loosely coupled, eventually consistent systems, it’s better to perform technical spikes regularly, to ensure the problem at hand is understood properly, as well as possible integrations into the existing code base.
maintainability
describes the afford required to keep a system in good shape, e.g. the amount of work required to regularly upgrade your dependencies, fix defects; but also incorporate non functional requirements after a system has launched.
Since I don’t know of a better way to describe this I’d say “how often do you upset your product owner?” - mostly because product owners always want new features, and you can’t deliver them if you’re busy keeping the system up.
testability
measures how easy it is to write test for your system.
Good testability often speaks to an easy understand code base; It’s also extremely important as a safety net when thinking about maintenance. I use tests as an indicator on how easy it is to understand a system. If you’re tests don’t immediately point you to the source of a bug, or require so much setup that it’s hard to even find the testing code, you don’t have this ability.
flexibility
describes how your system adapts to external changes.
External changes for a system range from slow responses from 3rd parties over occasional timeouts to unavailability.
To me, this mostly concerns operations but is extremely important to get right. If done wrong, your software will be hard to keep up and running.
I’d always argue: if it is worth building a system to begin with, it should always be kept available.
Fortunately, many architectural patterns exist to enable this.
The challenge
I think that these abilities build upon each other; and you always have to find trade offs since you can’t fulfill all of them at once. And trade offs can only be discussed in a specific context, meaning there is no one size fits all solution.
For example, rather generic trade offs:
- when building a technical spike, it might be okay to write less understandable & maintainable code, since your goal is a proof something is actually doable. When you have to go live with one, however, you should invest time in rebuilding critical systems/ part of a system, which are hard to understand/ maintain.
- when taking over an existing project on should check if the system reaches the desired level of flexibility & maintainability. Nobody wants to be stuck with a system that fails all the time.
- when starting a new project, the project expectations should be stated explicitly. E.g. when the project team is expected to change often, writing understandable and testable code should have higher priority, to reduce onboarding time.
But also more specific ones:
- hard to understand code is hard to test. You are probably missing good abstractions.
- introducing abstractions just for tests makes your code harder to understand.
- decoupling two systems, e.g. with a circuit breaker, also make it less easy to understand; simply because there’s more code to read.
If you think it’s simple, then you have misunderstood the problem.
- Bjarne Stroustrup
My key take away is this: software engineering is hard, and it helps to have some guidelines in mind when trying to make the best decision for the moment.
I think these abilities help with that.