What Software Architecture Should Look Like
Learn what software architecture is, how to evaluate and improve it. Avoid common pitfalls and start designing smarter.
Wikipedia defines software architecture as the functional structures of a software system and the discipline of creating such structures and systems. Well, you can kind of see what they mean, but it’s a pretty dry definition, and it raises as many questions as it answers. After all, what does fundamental mean anyway? And for that matter, what does structure mean in this context too? The last bit is only saying a disciplined approach to architecting things produces architecture.
To be fair to Wikipedia, this is a difficult idea to nail down, even though most of us would agree that it’s an important one in some way. So here is my take on software architecture. What does it mean? And what does it take to do a good job of it?
My thinking on software development, design, and architecture is deeply informed by what I think of as an engineering approach. I described this generally applicable approach in my new book, “Modern Software Engineering.” It’s doing rather well in the rankings on Amazon at the moment, and he’s collecting some very nice reviews. So do check it out. There’s a link to that in the description below, too.
I think of myself as a software developer. Probably more accurately, these days, I’m a consultant first and a developer second. But I also spent a considerable part of my career being called a software architect in one form or another. I couldn’t then and can’t now tell you any significant difference in my focus between when I was called a developer and when I was called an architect.
Ultimately, I think this is about design. Maybe it’s about design at different scales. But I confess that I’m somewhat skeptical of that.
A popular definition
The slippery nature of defining architecture as design on a bigger scale, is that design in Information Systems is fractal. Some things matter at all levels of detail. And there are tiny details that will invalidate grand assumptions. And grand assumptions can prevent adequate progress at the level of tiny detail.
A popular description of software architecture comes from Grady Booch, who says, “Architecture represents the significant design decisions that shape a system, where significance is measured by cost of change.”
I agree with this definition, but I don’t like it very much. It implies that these are the details that we need to get right at the start to succeed. It makes some sense if we’re looking retrospectively at the system. But it’s a pretty poor guide to building one to be omniscient.
For something this complex, that simply isn’t going to happen. For me, there’s only one approach to architecture that makes any sense at all. It has to be an evolutionary process. We will guide the design of our system by modifying the constraints that we apply to that design.
These constraints are rules of the road or what we think makes sense at any given point. These form an essential aspect of our architecture. We create a kind of tourist map of the system that we can use to guide design choices. Here’s the tourist map from one of my systems. This map guides the system’s evolution in the direction of the outcomes that we aim to achieve. If we’re doing a good job, we will discover our inevitable mistakes and update our tourist map as our understanding deepens. Let’s try and work into this problem a little bit more carefully.
Defining Software Architecture
One excellent definition of software architecture is it’s the stuff that we wish we’d got right at the start of a project. Perhaps a little more helpfully, it’s the shared understanding that the expert developers have of the system design.
Both of these definitions come from Ralph Johnson. I’d add a couple of words to the second. It’s a snapshot of the shared understanding of the expert developers. You hope it doesn’t change too often, but you should expect it to change.
In combination, these definitions get something rather than a critical right. At the start of any project, we don’t yet know the answers, but we still need to guess what may work to begin.
If we’re going to be creating this system as part of a team, we need a way to discuss design ideas, communicate with each other about the system, and think about where different behaviors of the system will be. We handled our tourists’ map will help a lot with these conversations, debating new features in front of the tourist map. We can explore the implications of our design choices and maybe spot improvements or mistakes in the map.
I like Raph Johnson’s focus on the stuff we wish we got right. We focus on the stuff that worries us. I like it because we almost certainly won’t get it right the first time. But it is important stuff. That aligns very nicely with my thinking and approach to development in general, which is significantly focused on maintaining my freedom to get things wrong. That probably sounds a bit weird, but I want the freedom to make a mistake and learn and recover from it. For that, we need to contain the complexity and manage it.
The Importance of Good Software Architecture
Fundamentally, the architecture of our system is realized in the shapes that we make with our code. All software has an architecture. In some of them, though, it happened by accident. In others, it was worked on with great care and grew to be something good.
Good architecture allows us to isolate one part of the problem from another. This could be functional. Let’s create services aligned with bounded context so that they can change more independently. Or it could be in the jargon of architects non-functional. Let’s share the data in our system so that it can scale linearly as demand rises.
More formal approaches to architecture often discuss architectural measures of quality. This is the list from ISO. I can see how talking about the abilities of a system can help. But in reality, I don’t buy into the idea of non-functional requirements. Every non-functional requirement is a system feature that users care about. If not, why are we doing it at all?
Even something as seemingly self-centered as making our code maintainable is about ensuring our ability to continue delivering value to our users in the future. Security, performance, scalability, testability, deployability, resilience, and so on are behaviors of our system as much as taking payment with a credit card. We have to design the system to exhibit these behaviors. If we are sensible, we will design our system to manage the complexity of these things by compartmentalizing them from other behaviors where we can.
Evaluating the quality of a software architecture
People get worried about these qualities of a system and try to take them somewhat differently because they tend to have a broader impact than many features.
You can’t usually fix security or scalability, or resilience in a single place in your system. These are often referred to as “cross-cutting concerns,” but that doesn’t mean that these things are architectural concerns and other things are not. These are just behaviors that people often forget about while chasing different kinds of features.
As a software developer, it’s your responsibility to decide how your software should behave, regardless of your role or team size. This includes processing credit cards, choosing button colors, and ensuring the software is secure and scalable.
For some types of systems, some of these problems are genuinely difficult; and it helps a lot when you have smart, experienced people on the team who have seen ways to cope before. But that doesn’t mean that architecture is somebody else’s problem.
Whether you are on your first day on the job or a grizzled veteran, your decisions may impact the architecture of the system. This is another of those slippery aspects of software. There is very little distance between what looks like a small change and that small change has a massive impact.
The only things between that being true look or a good working design and architecture that we understand and can use to guide our fine-grained decisions. So that gives us another step forward in better understanding this complex concept.
The architecture of our software is the rules we agree to follow to avoid making mistakes. It’s essential to involve all developers in discussions to help them understand and contribute to the architecture as development progresses.
Architect as a job description has recently seen a bit of a decline. This is mostly in response to Agile developments reaction against big upfront design. As Dave Thomas famously said, this problem is, “Big upfront design is dumb. No upfront design is dumber”. So we need to start somewhere. But where? The honest answer is that you make a guess based on your understanding of the problem and your experience. If you’re any good at this kind of thing, you treat this guest as an experiment. How could you show that your guess is wrong? Try that. Working incrementally, whether in fine-grained design or big-picture architecture, is essential to doing a good job of anything complex.
There’s a caveat to this. But as always, you need to understand the trade-offs. If the problem you’re solving is pretty standard, you could adopt a tried and tested architectural approach and off-the-peg architecture if you like. This is a good idea as long as you understand the constraints and the fit of that architecture to your problem. Because the other big problem with software architecture as an idea is that there’s no one size fits all. There’s no such thing as a generically good architecture. Instead, it’s a matter of whether a particular architecture fits the needs of the system.
Incremental Design in Software Architecture
One of the most successful, widely applicable architectures was a layered system backed up by a relational database: a UI, some logic, and a database.
For systems with a moderate number of users and relatively simple crud-style features, this was a very good choice. It was a good choice, though, because it was pretty informative.
I think a principal reason that the clever but slow idea of transactions in relational databases took hold was that it allowed programmers to write multi-user systems without worrying too much about the horrible complexity of concurrency. If you open a transaction, read some data, change it, and write it out. Again, the database will block any other access to that data while you’re dealing with it. And as long as your transaction lasts, it locks access to the data. This was very good.
Suppose this architecture fits your problem. It kept the programming model simple. It’s still good if it fits your problem today. And it fits quite a lot of issues quite well. However, massive-scale computing became more common as the web grew in influence.
Having a relational database management system as a synchronization point to your data, a source of truth simply didn’t scale. So people started experimenting with other approaches.
Choosing tools and technologies
I was once asked to consult on a huge project. They had decided to use a no SQL database as their primary data store, which is fine. But they’d forgotten or didn’t realize the importance of the protection that transactions that they were more accustomed to had given them.
This no SQL database wasn’t transactional, so data was shared by multiple processes and written to buy multiple processes, and the results were essentially random. Whoever wrote last got to store their changes. Everyone else would lose this without any indication of the loss.
It made me feel a bit queasy just to think about this big system with shared data and no protection or coordination between concurrent processes. This was a problem of not thinking about the architecture of the system but only the tools. That’s not the same thing. A collection of tools alone doesn’t make a software architecture.
This team had seen one of the big web companies develop this great-looking no SQL database to help them scale up, and it did that, but only in the context of a whole raft of other choices and constraints that this team hadn’t thought to apply.
The Pitfalls of Misapplying Microservices
There is another somewhat similar very common mistake that probably is a little closer to your software. Microservices were invented to decouple development. They’re intended to be independently deployable, freeing teams to work separately from other teams, which means you don’t get to test them together before you release.
If you are on Netflix or Amazon, this is fantastic. If you’re a small development shop with a handful of developers, it’s almost certainly overkill and inefficient. And worse than that, most teams that I see that claim to practice micro-service approaches aren’t. They don’t have independently deployable services, which means that they have to test them all together before they feel confident to release them. This is a distributed monolith, a rather tough choice, suitable for some things but never simple.
Trade-offs in Software Architecture
I think software is more complicated than we often think, so the sensible thing is to approach it with a little more caution. I don’t mean that we should agonize for weeks or months before taking the first step; completely the opposite. But we should work on the assumption that our best guess, so far, is almost certainly wrong. So architect our systems to allow us to recover when we learn more and find out where it’s wrong. This takes skill, but it is realistically the only sensible choice.
Architecting a complex system is not something that someone in an ivory tower does at the start of a project. It’s also only partly intentional, like more fine-grained design decisions. It’s an iterative, incremental exercise in a guided evolutionary process. This is not precise. And sometimes, even despite cleverness, expertise, and experience, people get things wrong. I am afraid of the inevitable consequence of dealing with something as tricky and complex as software.
A few years ago, a performance optimization strategy in Intel processors meant that encryption could be broken, obviating all of the clever security architecture features in the web, cloud, on-premises systems, even mobile devices, and on pretty much every operating system. That is a salutary lesson in how fragile architecture and design can be to tiny things. A good way to approach this is to think about the things that you are unsure of.
Starting the right way
I was chatting with my friend John Henney a few days ago, and we were talking about design. He said, “As soon as someone says, ‘I don’t know,’ there’s a seam in the design.” That’s quite a good way to think. Start the design of your system with your best guesses based on what you know so far, and be on the lookout for the things that you don’t know or are unsure about. You don’t have to do a deep first exploration of these things. You shouldn’t. But you think about insulating the stuff you understand from the things you don’t.
Crude sketches in code of those things that you don’t yet understand, and abstraction can act as placeholders while you make progress and grow your understanding of the bits of the code that you do know.
Don’t build world-class security into the first sketches of your system. Don’t take on the burden of making your system perfectly scalable from day one. But do design your system as best as you can imagine so that you can enhance security without undoing the progress you’ve already made. And that you can take advantage of your great work so far and allocate it differently so that it does scale up if that’s demand.
Insulate the parts of the system that you understand well from those that you don’t yet. These design approaches are at the heart of what I describe in my modern software engineering book. They use the techniques to manage complexity to allow you to go back and enhance or even correct architecture missteps when you discover them.
My final advice on software architecture is to be intentionally vague about the details. Architecture should guide our decisions with constraints, but there are relatively few in great architecture. Those that are there are pretty rigidly enforced.
Being intentionally vague about software architecture details allows room for flexibility and evolution in the system. While architecture should guide design decisions, it should not be overly prescriptive or rigid. Rather than specifying every last detail, architecture should provide constraints and principles that can guide decision-making and prevent the introduction of unnecessary complexity or risk.
At the same time, it’s important to ensure that the constraints in place are strictly enforced to maintain the integrity of the architecture. This means that the key principles and boundaries of the system should be clearly defined and well-communicated to everyone involved in the development process.
By striking a balance between flexibility and rigor, good software architecture can help teams to build robust, adaptable systems that meet the needs of users and stakeholders. It can provide a roadmap for decision-making, prevent common mistakes and problems, and help ensure that software is scalable, maintainable, and effective in meeting its goals.
Here at Talendor, we take this matter very seriously when selecting experts to join our talent marketplace. They’re all vetted and have proven skills to roll out great software. Want to learn more? Click here to meet them!