Let’s suppose you have just joined a developer team which is about to build an awesome new product. As the system is going to be fairly complex, you have decided to take some time upfront to think about the right, architectural design.
So you stand infront of an empty whiteboard. Quite some blank space to fill! Where do you even start? What is the best way to define a software architecture from scratch?
The truth is that it is hard or even impossible to define a strict procedure when it comes to constructing a software architecture. Just like it is impossible to give a step by step guide on how to win every argument. Or on writing a great book people care about.
However, there are quite some practical tools that can help you to get along. In this article, I want to present some practical tips and good practices on designing a software architecture. The tips are less formal and comprehensive than existing methodologies like ATAM. Instead, they rather aim on quick results. Further, they are highly opinionated and are based on my experience from several large consulting and engineering projects.
Do you have some favorite design methods which should be on the list? Leave me a comment, I’d love to read about them.
Alright, let’s get started with part one.
#1 Start with thinking about abstract components, not deployment diagrams
When it comes to software architecture, many developers immediately start to think in technical building blocks. Databases, VMs, Web Servers, Message Brokers, Cloud Computing Platforms and such. While it is important to think about a system’s deployment strategy, it highly depends on a lot of structural and behavioral decisions which are yet to be made. Besides that, 3rd party components add a lot of overall complexity to the software system. Don’t distract your focus by thinking about them too early.
At the beginning, it is better to use abstract components and abstract concepts as architectural building blocks, much like UML suggests with its component diagram. For example, a system might need a payment processor. You might decide that it should work asynchronously. You might decide that it needs some kind of persistent state. Characterizing these aspects in a rather abstract way is a good starting point to later think about concrete solutions. These solutions will highly depend on functional and non-functional requirements as well as organizational and technical constraints which are yet to be discovered.
A design discussion beginning like “Let’s take a MongoDB database and an Azure app service. I think C# might be cool choice.” does not pay enough attention to these things.
#2 Don’t start by choosing patterns.
Patterns are a great tool when it comes to structural component design. But using them as a starting point often leads to over-engineered systems or hype-driven-development. MVC, Pipes and Filters, DDD implementation patterns, CQRS, Ports and Adapters, Event Sourcing, … all of them can be highly valuable building blocks when creating the design of a certain component. But neither of them should be prematurely considered in being a top level approach. Before you utilize a certain approach or method, try to get an overall view on the top level components. The big picture, if you will. Every approach comes with its own advantages and disadvantages. Changes are that one single design approach will not fit every part of your software system.
#3 Don’t expect too much from the first design iteration.
Software architecture and project plans have one thing in common: The first shot is always wrong. The beginning of the project is where you have the least knowledge about the technical and non-technical challenges you are going to face throughout development. Defining a final architecture (or project plan) in that development phase is a rather bold venture. Thus, work in iterations. Allow the architectural design to grow with the knowledge you gather about the system.
#4 Create a top level view on functional requirements
Functional requirements define the functions a system must provide. Ideally, the product owner and other domain experts capture them in a set of user stories, which deliver detailed information about actors, preconditions, possible flows of a certain feature, and so on. However, in an early design phase, you do not have to know every business rule, user story and every aspect of the system’s domain. Instead, it is a good idea to start by getting a top level view on functional requirements. A good starting point is to create a mind map containing the most important nouns of the application domain. Cluster them by functional topics and find the most important actions/verbs around these nouns.
The mind map gives you a first idea about functional parts of the system and its complexity. This can also help in getting a feeling about the suitability of possible design approaches. For example, if you got a complex domain which dominates the overall complexity of the software, a domain driven design approach could be beneficial. However, if domain logic narrows down to some trivial aggregations and mappings, DDD and its typical implementation patterns might end up resulting in an over-engineered design phase combined with lots of shallow wrapper types and poor abstractions.
#5 Identify non-functional requirements carefully
Non-functional requirements naturally have major influence on architectural design. They describe the quality attributes of a system. Some examples are:
The effort that is needed to extend a certain feature.
Short response time / high throughput.
The ability of a system to handle an increasing amount of work.
The usability of a system in different environments.
The conformability of a system to policies and standards.
Looking at such a list of quality attributes, one could say: Fine, I want all of them! However, it is a good idea to choose the set of non-functional requirements carefully. First, each of them can significantly increase design and implementation complexity. Besides that, non functional requirements may conflict with each other. Let us take a look at some examples:
- A very performant system may be less portable because it might demand certain environment/hardware features.
- A highly extensible system may be less performant because the extensibility introduces certain abstraction layers, lowering thoughput.
- The CAP theorem states a proven, fundamental tradeoff between consistency, availability and partition tolerance in distributed shared-data systems.
Such trade-off situations force us to carefully identify the important quality attributes, which must be satisfied by the software architecture. It is a good idea to document and discuss them with any stakeholders.
#6 Watch the scope of non functional requirements
Not every non functional requirement should be considered as a top level quality attribute of the system. Some quality attributes can be limited to a local scope. This is nice, because the implementation complexity resulting from the non-functional requirement can then also be limited to that local scope.
At some point, you are going to discuss possible non-functional requirements with product experts and other stakeholders. When you hear statements like “our system must have attribute X because of reason Y”, carefully listen whether reason Y applies to the system as a whole or to a limited part of it. In many cases, reason Y only applies to a very specific situation or use case.
For example, one stakeholder might say:
“Our system must allow a very high throughput because there is an unbelievable high number of data points that need to be aggregated for report generation.”
Instead of declaring “Performance” as a top level architecture goal, limit the scope of the proposed quality attribute. The above quality attribute proposal translates to “Our system has a component aggregating certain data. That component must have a very high throughput.”
In solution space, this may allow us to isolate the aggregating component as some kind of asynchronous worker. Any extra implementation or integration effort that supports high throughput (e.g. special data storage or patterns like CQRS or Event Sourcing) can be limited to that worker.
When discussing non-functional requirements, it is a good idea to bring some top level visualization of the system’s functional requirements (tipp #4). Ask the stakeholders to think about possible quality attributes for each of the functional clusters you identified there. This helps to encourage a scope-aware discussion of non-functional requirements without talking about technical components.
That’s all for now. In the next part, we will take a more in-depth look at component design and architectural documentation challenges. If you have any questions or feedback regarding the tipps above, just leave me a comment, I’d love to read your opinion. 😊