Ten Principles of Good Software Architecture

David Bennett
13 min readAug 27, 2020

What is a good architecture?

Many times when we think of architecting technical systems we focus on design patterns, algorithms, technology being used, and connecting circles in diagrams. These are all good to keep in mind when developing systems. However, I would like to talk about the less tangible factors that go into good architecture. One model that I would like to try to apply to architectural development is Dieter Rams’ 10 Principles of Good Product Design.

I think Rams’ approach to developing good physical products can apply in a very similar way to architecting great technical systems. Yes, when we develop for a company we are usually developing a product. However, sometimes I feel it is helpful to shift my mindset to think of the code and systems I create as my product that I am delivering to my company. The products I deliver to the company enable the company to effectively deliver on their product offerings.

I will take each of Rams’ ten principles, and modify what he originally stated into the form that I see when applying it to architectural design. I will then provide a practical example. When I say architecture in the below principles I am referring to both the actual code syntax and the system design around it.

1. Good design is innovative

Architecture itself does not need to be innovative but the technologies it makes use of should be. Innovative architecture always develops in tandem with innovative technology, and can never be an end in itself.

A few years ago I co-founded a big data ad-tech startup. When I was designing the system I had to make the decision to be innovative and create our own technology to process all this data. Or to make use of existing innovative technologies in an innovative way. Creating our own innovative technology was not going to be the answer; especially for a cash strapped startup. For our solution we decided to use Storm and Hadoop which allowed us to process streaming data and run massive batch processing on the whole of it. These were proven, innovative, well supported, large scale data processing systems that took us further in a shorter period of time than rolling our own technology ever could. It instead, allowed us to focus on the key differentiators that make our company stand out.

2. Good design makes a product useful

Our product is our code. Code is written to be used by the developers working side by side with you; not just by the end user of the system. Good architecture emphasizes the usefulness of a piece of code whilst disregarding anything that could possibly detract from it.

One of the best examples I can think of would be the Python requests library. The Python standard library has urllib2 and urllib3 to deal with making HTTP connections. Ignoring the version differences, the simple construction of an HTTP request is much easier in requests.

Urllib3

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'}
data = urllib.parse.urlencode({'key': 'value'})
data = data.encode('ascii')
req = urllib.request.Request(
'http://example.com', data, headers, method='POST'
)
with urllib.request.urlopen(req) as response:
resp_data = response.read()
json_data = json.loads(resp_data)

Requests

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'}req = requests.post('http://example.com', data={'key':'value'}, headers=headers)json_data = req.json()

Requests frames it’s API around the most critical element of an HTTP request; the HTTP method. Often I know the method I will use when calling an API before I know the URL of it. Requests understands this; and by re-framing the API to be method-centric it not only simplifies the API but it also greatly simplifies the library code. The library code does not need to deal with all the edge cases around a POST request when it knows the user is making an OPTIONS call. The design decisions around this library made it useful for both the end user and the developers working on the library. Good design must always emphasize making the code useful to fellow developers.

However, a great API does not make for a great piece of code if it is not well documented or does not follow standards. Poor documentation, unintuitive APIs, non-standard interfaces all detract from the usefulness of code.

3. Good design is aesthetic

The aesthetic quality of a system is integral to it’s usefulness because systems we use every day affect our person and our well-being. It should have a clean interface, a clear separation of concerns, and be something pleasant they look forward to interacting with.

As a developer I feel like my well being is improved when working with aesthetic, beautiful, and elegant systems. What makes a library or system aesthetically beautiful is a clear interface, good encapsulation and lack of hacks. The code I write is better and I am overall happier whenever I am working in beautiful systems with clean code. In contrast, my whole week can be thrown off when I am assigned a task where I need to work within an area of code that is painful to work through.

Bad code begets bad code because fellow developers just want to get out of that mess and onto their next task. They see the tangled web in front of them as too much of a burden to refactor in the 2 points they allotted to it. They will settle for a hack on top of the mess of existing hacks. While we can argue if the developer should have added the hack we cannot debate what contributed to the desire for a hack. The messy code the developer was presented with took a toll on their mindset when taking on the task.

I see this on a weekly basis when taking care of my house. When my house is clean I make the effort to keep it clean. I wipe down the counters after dropping a few crumbs and put the dishes directly in the dishwasher after use. However, if the dishwasher is full and there are dishes already in the sink (especially if they are not my dishes) I am much less likely to undertake the task of cleaning. I may even balance another dish on top of the pile of dishes in the sink. The effort to clean everything up looks to be too much for what I have energy for and even looking at the mess pulls the energy out of me.

As great designers we should strive for aesthetic, clean, systems as they will likely stay clean and lead to more energetic and engaged teams.

4. Good design makes a product understandable

It clarifies the system structure. Better still, it can make the code talk. At best, it is self-explanatory.

I really see this as where design patterns come into play. Recently, we had a hierarchy of objects which all had an operation which we needed to perform on them. As our system evolved we had more and more operations we needed to perform on each object in the hierarchy. Instead of just adding another method to each object we instead took the path of implementing the Visitor pattern. This effectively separated the objects from the operations we needed to perform on them. Going back into that area of the code is now a breath of fresh air. It greatly simplified the code and really allowed the code to speak for itself around what these objects are for and what operations we are performing on them.

I think good design should be like a good children’s story. It should be easy to read and we should not need to break out a dictionary (e.g. StackOverflow/Google) to understand it. We should also be able to quickly fall into the narrative and see the logical conclusion coming a mile away. It should not be The Silmarillion, or Moby Dick, or probably anything by James Joyce. Good design should not be something we need to slog through and only fully understood by a select few. I am not only speaking about code here; I am also speaking about system design. When reviewing a technical specification for a new system I always look for it to be understandable and with limited surprises. In its most basic form I am looking for a technical spec to document exactly the solution to a problem that my coworkers and I had contained within our heads. In its worst form, it is a stream of consciousness list of items with limited context .

5. Good design is unobtrusive

See code as fulfilling a purpose, as a tool. Its design should be both neutral and restrained. There should be no obtrusive side effects of your code.

Recently I was digging into a bug that was causing strange networking issues when running a task in a separate asynchronous process. Basically, only in a certain context was our network request unexpectedly dying. After digging into this bug for hours I found that a library we imported in another part of our code modified the networking library of Python (urllib3) to account for a weird SSL use case they were trying to work around in their code. That modification was causing the bug. Everything worked once I patched their code to remove the patch they were applying to the underlying Python library. Whatever use case they were trying to patch their way around was not even applicable in our usage of their library and was actually causing bugs in our code.

This is a clear case of obtrusive design. That library was modifying the underlying network stack of Python in an undocumented way. The side effect of us importing their code was that the networking stack was unknowingly getting changed. If that library was designed better it should have stuck to fulfilling its purpose of connecting two systems together, not patching Python in it’s own way. If the library had issues with the underlying networking stack it could have worked around it in many other ways than it did (re-implementation, importing a different networking library, etc.). And if it did need to patch Python in some way it should have made it quite explicit in it’s documentation.

6. Good design is honest

It does not do more than its intended purpose. It does not attempt to manipulate the developer with promises that cannot be kept. It is well documented around its purpose and limitations.

I see this many times with libraries which promise the world and never state their limitations. All systems have limitations and make trade-offs, but not all systems state the trade-offs they are making. Many times I have tried to make use of a library only to find out that it really does not satisfy my use case. It is frustrating building off of a library only to find later that a subset of it is not thread-safe. Even worse still to find that they closed an issue mentioning its lack of thread safety two years ago and marked it “won’t fix” without updating their documentation. While it is hard to admit to ourselves where our systems may fall short we must do so in order to be stewards of good architecture.

This is something I need to constantly remind myself about. After I finish documenting what a system does I need to take a step back and think about all the things others think it will do for them and then I need to document those edge cases. Upfront honesty will keep my team from getting false bugs that are really feature requests which we have already planned for.

7. Good design is long-lasting

It avoids being fashionable and therefore never appears antiquated. It does not jump to use the latest technology trends. It uses well established and proven technologies that are developed from first principles.

When envisioning the ideal architect tend to think of an old woodworker working with hand tools. The simple fact that they are using hand tools shows that they value correctness over speed of getting things done. Others may say the woodworker is stuck in his old ways and should use the latest and greatest tools; but it is hard to argue with the result he produces. By sticking with his hand tools not only does he have full control over the item he is making but he is forced into a more deliberate process which makes for a long-lasting heirloom piece of furniture. It comes down to do I want to use the power tool that will make me able to build it quicker and maybe chop of my finger or do I use the hand tools and focus on getting it done right the first time with fingers intact?

The latest and greatest technology is often the most compelling and we can be easily lured into making use of it. But it usually has the most bugs. I would rather be building off of 10 year old proven technology than a 2 year old new framework that only one company is using in production. The new framework looks to fill in the gaps of the 10 year old tech but what it usually delivers is a whole new set of problems. The latest single page app framework might be really nice but nobody can argue with my faster page load times coming out of my server-side rendered site. My server-side rendered site will never appear antiquated if it performs better and embodies all the other principles I have mentioned.

8. Good design is thorough down to the last detail

Nothing must be arbitrary or left to chance. It does not make assumptions about the data within the system. Care and accuracy around the simple things like testing and commenting shows respect towards your fellow developers.

I see this all the time with big data. As soon as I make an assumption about a piece of data coming in some customer will have a way to break my assumption. If I make the assumption that all customers will have a first name and last name then Prince will come along and break that rule. Then I assume all names to be ASCII and he comes and changes his name to a unicode codepoint. Good architecture limits the assumptions it makes. Making assumptions makes for on-call to be pinged at 3am and makes for an upset coworker.

9. Good design is environmentally-friendly

Good architecture is friendly to the environment around it. In an efficiency sense it makes good use of data structures and algorithms to minimize CPU cycles. In a visual sense it minimizes the use of syntactic sugar and prefers explicitness for more readable code. In a practical sense it is easier for the developer to work with.

Think of a system that must calculate the total amount that each customer has spent over the past month. I could iterate over each customer in the last month and calculate their total spend each time the report needs to be run. Or instead, I could maintain a database of customers and update their total spend only once a new order comes in. The latter approach is more environmentally friendly in that over time it will expend less CPU cycles recalculating the same total spend for the customers who have no new orders.

I always try to make my systems environmentally-friendly in both the physical and visual sense. I strive to reduce duplicate work and make my code as readable as possible. Sometimes these are antithetical; the best example would be ORMs. They are wonderful in that they are great for the visual environment of the developer but often add a lot of overhead and result in sub-optimal queries. Good design strikes a balance here.

When it comes to the development environment a good system should be easy to bootstrap and well documented. The reason Docker is so prevalent now is due to this principle. It is well encapsulated and limits the impact of changes. Making a change to a docker image will not change your local environment. The same goes for virtual environments: a separate python virtual environment is environmentally friendly to a developer’s working environment. Balance here is key as well as it is easy to optimize for operational ease at the cost of efficiency.

10. Good design is as little design as possible

Good architecture solves its intended problem and nothing else. It minimizes abstractions until they are well warranted. It is cohesive and focused.

Keep it simple, silly.

KISS is a design principle for a reason. Simple designs are quite often the best. They are easy to understand and often accomplish the intended task in the most efficient manner. If you are making dinner for your family at home would you rather a chef’s knife or a Swiss Army knife? The Swiss Army knife will work but you will likely not use all it’s functions and it will not do as good of a job at slicing that tomato.

When thinking about this principle I often think back to my first job and a piece of code I had been assigned to work on. It was written by a great developer but it was a massive mess of abstractions. There were a handful classes I had to work with, each one had a separate interface, base class, and single implementation class file. To make things worse we had no foreseeable separate implementation of these base classes and interfaces. The developer was trying to plan for a potential future that would likely not exist. He forgot to keep it simple with a few concrete classes. If we needed the additional abstraction later we could do a quick refactor and pull the shared items into a base class. Instead, I had to jump through the mess of abstractions modifying things along the way and make sure each base class only has the one implementation that I thought it did.

The Unix philosophy embodies this well. It’s primary tenant is to write programs that do one thing and do it well. It emphasizes building simple, short, clear, modular, and extensible code that can be plugged together easily with pipes.

Concluding Thoughts

I present these principles as ideals to strive for. I have violated every single one of these principles at some point in my career and I still miss on some of them as I develop systems today. But I find it nice to come back to these ideals every so often to remind me about the good principles of proper architecture I should be striving for. It is often easy to miss the forest in the trees of sprint tasks. We must constantly keep in mind that we are pushing to build two products, the product for the end user, and the product for ourselves to work with. Too often we focus on the end user (that’s what user stories are all about) but I think we need to focus on the product we build for ourselves and our fellow developers. That is why I like the idea around architecture as a product and found it helpful to apply principles of good product design to it.

--

--