What is good software architecture?
tl;dr
A good software architecture does not just solve a problem. It also
- states the problem and its design goals (including any assumptions),
- imposes only necessary constraints on the implementation (is not too fine-grained),
- is visible and understandable, and
- shows its work (decisions are traceable to design goals).
So, your software has an architecture.
This is a sequel to my first post, ‘What is software architecture?’, in which I defined a software architecture as a model, resulting from intentional, goal-oriented decisions, about how to solve a problem with software.
However, just because you have an architecture, this does not necessarily mean that it is a good one. What makes a software architecture good?
Isn't it enough that it is working?
If you implement a system based on your architecture, and it works, why bother with the question? First off, this approach requires you to build the software before you can evaluate your architecture. However, discovering architectural flaws after the implementation phase is typically expensive. It is much cheaper to eliminate flaws if they are found earlier in the software delivery lifecycle.
And then, there are the ‘-ilities’. Software architecture is often concerned with quality attribute requirements in areas such as security, maintainability, adaptability, and others (collectively referred to as ‘-ilities’), which deal with risk. Much of software architecture is risk management. If there is no risk you may miss your performance target, there is no need for the architecture to address performance. If there is no risk your system may be insecure, the architecture does not have to deal with security concerns. The crux of risks is that they may not materialize. Generally, this is a good thing. You don't want risks to materialize. But how can you tell then whether they have been addressed?
‘What's this?’ – ‘This is my charm against tigers.’ – ‘But there are no tigers around here!’ – ‘Yes! See how well it is working?’
How can you be sure that your anti-tiger charm is working (or that your architecture is maintainable) if there are no tigers in the country anyway (or if you have not yet had to perform maintenance on the finished system)? If you want your home to be secure, would you buy a front door lock that claims to prevent theft, but only as long as there are no thieves around? I wouldn't. I would want some assurance that the lock – or the charm – or the architecture – is actually capable of eliminating or at least mitigating the risks it is supposed to address.
So how do you do that? How do you provide this assurance?
What does ‘good’ mean?
As we have seen above, ‘good’ for an architecture does not just mean that the finished system is functional. And if you look at my definition again, you will notice that it says the architecture is a model anyway. The architecture is not the implementation. So, what does ‘good’ mean for this model? I propose four criteria:
- It states the problem.
- It imposes only necessary constraints.
- It is visible and understandable.
- It shows its work.
These are my ‘conditions for software architecture goodness’. In the following sections I will elaborate on them in turn and show why I think each of them is necessary.
A good software architecture states the problem.
By definition, a software architecture is supposed to describe how to solve a problem with software. To be considered a good software architecture, it needs to actually do this. While this may sound obvious, there is one small hurdle to clear: solving a problem requires the problem to be known. In practice, it can be surprisingly hard to get a clear and complete problem statement. While it may be easy to summarize the problem in a single sentence, this is rarely enough to give a complete picture of what is really needed.
‘I want a self-driving car.’ – ‘OK, we can do that. We'll put a little controller on an R/C car, and …’ – ‘Oh, and it must be able to drive my family to any address in the USA without getting involved in an accident.’ – ‘Ooof!’
The finished system must fulfill the needs of its stakeholders, and it must typically do so in a specific environment while being subject to a particular set of constraints. These can be formulated as a set of design goals which will drive the architecture. If it is not possible to get a definitive answer to a question relevant to the architecture (because the business requirements are not yet clear or depend on circumstances outside your control, e.g., future growth), you will have to make assumptions, which then become part of your design goals.
If the architecture of a software system does not state the problem it is set to solve (including the breakdown into design goals), the software implemented based on this architecture may still fulfill its stakeholders' needs, and it may still be able to operate more or less smoothly in the target environment, but it will only do so by accident. Since the whole point of the software architecture is to ensure that result, if it does not do so, it is a bad architecture, even though the system itself may be fit for purpose.
A further advantage of explicitly listing your design goals is that it becomes easier to see when things change. If, for example, the number of users to support is given as ‘up to 10,000’ and your business is so successful that the growth projection for the next year exceeds this number, it is a no-brainer to say, ‘Maybe we should review how our architecture is going to cope with this.’ If the number of users the architecture was designed for was not specified explicitly, you may only notice that you are operating outside of the system's specifications when the first problems appear in production.
Thus, a good software architecture contains a clear and complete problem statement as well as a set of design goals derived from the context and from the stakeholders' needs (including any assumptions you need to make). This is why the first chapter of the arc42 architecture documentation template contains an overview of the major business goals, functional requirements, and quality goals, as well as pointers to the complete documentation of each.
A good software architecture imposes only necessary constraints.
Once the problem is clearly stated, you need to make decisions about how to solve it. How many decisions? Paraphrasing Occam's razor, decisions are not to be multiplied without necessity.
Because every decision puts constraints on the solution space (and if it doesn't, it is unnecessary). This is a good thing because the whole point of software architecture is to constrain the solution space in such a way as to exclude bad implementations, i.e., implementations that fail to reach the design goals. But too many constraints are bad, too.
If there are two architectures that solve the same problem, but one of them puts more constraints on the solution than the other, the additional constraints are not necessary. Imposing them excludes valid implementations and thus limits the flexibility of the solution design. If (when) requirements change, but the context from which the design goals were derived does not, the smaller, coarser grained architecture will not need to change. The larger, finer grained architecture may need to be adapted, however, because the additional constraints may stand in the way of the necessary implementation changes.
Therefore, while there is no hard limit on how fine-grained a software architecture can be (at the limit, it could describe a single implementation exactly), I posit that a software architecture that imposes only constraints necessary to reach its design goals is strictly better than one that imposes additional constraints.
(And if you can think of a case where an additional constraint would make the architecture better, ‘because it would enable X’, then maybe you forgot to name X as a design goal.)
A good software architecture is visible and understandable.
A software architecture is a model of how to solve a problem with software. To actually solve the problem, the software system needs to be implemented according to the architecture, which means that the people implementing it need to know about the architecture. If there is only one developer, and it is the same person as the software architect, and the implementation is completed so quickly that there is no chance of forgetting something important, it may suffice if the architecture lives only in their head. In all other cases (more than one person working on the implementation, or one person working on it over a longer period of time), if the architecture is to be preserved over the course of time, it must be made visible and must be represented in a way that will be understandable to developers (either the architects themselves or others) in the future.
Sometimes, software architecture is described as a ‘shared understanding’ of the system structures by the relevant stakeholders (for references, see my previous post). Building a shared understanding is much easier if there is something visible to point to when doing the sharing.
Making a software architecture visible can be done in various ways. The obvious one is having separate architecture documentation. For guidelines on what to document and how to do it, I found the arc42 template useful.
The internal architecture of an application can be made visible in the code by making judicious use of your programming language's modularization features and appropriate naming of modules and other elements (packages, classes, functions, …). For example, in the Java world, if you want to decouple an application from the implementation of a certain piece of logic, you can put the interface definitions and implementation classes into separate JAR files and declare only the interface JAR as a compile-time dependency. The implementation JAR is added as a runtime dependency only, which prevents the application from accessing the implementation classes directly. An example of this is SLF4J, which provides an interface to logging, with several implementations backed by different logging libraries.
There are also ways to test conformance to an architecture. For Java-based software, one option is ArchUnit, which allows you to write unit tests that check dependencies between packages and classes. Some static code analysis tools offer similar features. Tools like these make the architecture visible by raising flags on architecture violations.
While none of these techniques can guarantee that the architecture will be followed when changes are made in the future, projects using them have much better chances of keeping their architecture alive.
A good software architecture shows its work.
The paper What is your definition of software architecture? cites Barry Boehm and his students at the USC Center for Software Engineering like this:
A software system architecture comprises
- […]
- A rationale which demonstrates that the components, connections, and constraints define a system that, if implemented, would satisfy the collection of system stakeholders’ need statements.
While I think a software architecture that is missing this rationale is still a software architecture, i.e., the rationale is not the defining aspect, I agree that a good software architecture does show its work in this way.
I define a software architecture as a model, resulting from intentional, goal-oriented decisions. This means each decision is made to support one or more design goals, and this in turn means someone has hopefully thought about how the decision does this and why the option chosen does this better than other options that were also considered. Documenting these thoughts (possibly in Architecture Decision Records) makes the architecture better understandable for everyone who was not there when it was created (or cannot remember everything perfectly). And it makes it easier to work with when something changes. If a change is made to the business requirements that has an impact on the system's design goals, you can examine the rationale to determine which decisions may need to be revisited. If a change to the architecture is proposed, you can look up from which design goals the current state was derived, and determine the impact of the change on these goals.
Documenting these links is especially important for design goals that are not directly observable because they do not concern behavior but deal with risk reduction (like many of the ‘-ilities’ do). Sometimes, the rationale connecting a design goal with architectural derisking measures is the only visible artefact that shows the design goal has been addressed.
Conclusion
A software architecture that results in a working system when implemented is … not bad, but to be really good, it should also …
- … state the problem it is solving and break it down into design goals.
- … refrain from imposing constraints on the implementation that are not necessary to reach its design goals.
- … be visible and presented in ways understandable by fellow architects and developers (which may include the future you).
- … provide a rationale that links design goals and architecture decisions and explains how the latter result in an architecture that meets the former.
If your software architecture does these things, it will be easier to implement and easier to work with in the long term. And, of course, it should also work.