Blogg
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
This is the continuation of my blog series The black t-shirt architect, a name I hope brings to mind the image of an architect still deeply involved in the nitty-gritty details of software development. In this blog series we take a closer look at three fundamental rules of software architecture: Everything is a trade-off, The ‘why’ is more important than the ‘how’ and finally Evolution is inevitable. In the first part we saw how it is essential to consider different alternatives when making significant design decisions and in this part we will follow up by looking at why documenting those design decisions is so important.
We concluded the first part in this blog series by stating that all architectural decisions are trade-offs and that there are no perfect solutions. This means that no decision is a given and therefore we always need to be careful to justify the decisions we make. This is where the second rule of software architecture comes into play and why ‘why’ is more important than ‘how’.
I am generally positive to direct, spoken communication when it comes to spreading information in a team or between teams. But there are situations when it becomes necessary to write things down and one such situation is when architectural decisions need to be communicated. The reason for this is probably obvious to many readers of this blog: architectural decisions need to survive the test of time in order to be valuable.
The ‘why’ I am currently living in Leiden is obviously a more interesting question than ‘how’ I got here. I got here by plane. That is how.
An important aspect of any architectural decision is the time aspect. We don’t want the architecture of a system to change too frequently and it is therefore important that our analysis of alternatives is done rigorously (yes, we are back at the first rule of architecture). However, no matter how rigorously we analyse alternatives on day one, six months later the environment in which the application operates will have changed and adjustments will be necessary.
How we design for an ever-changing environment will be the topic of the next and final part of this series: evolution is inevitable. I will therefore limit myself this time to focus on how we time-proof the decision as opposed to the design. In order to have any hope of maintaining a somewhat consistent design we need to document it. We need to document our architectural decisions in such a way that we don’t feel an urge to discard them in six months because a new API is available or a new version of Spring boot has been released. We need to document why we made the decision we made.
By focusing on why a particular architectural decision was made we force ourselves to consider the alternatives and explain the selection process, thus ensuring we follow the first rule. On the contrary, if we instead focused on how to implement a particular architectural decision we certainly run a risk of overlooking important aspects for the application architecture later on.
Further reason for focusing on the why is that since there are no silver bullets or perfect solutions every design decision can come under scrutiny and flaws be found. For that reason it is invaluable to be able to go back and review the reasons why said decision was made. If we have a log of our architectural decisions with justifications we can better avoid making adjustments in a manner that are opposite to earlier design decisions.
Imagine a scenario where you as an architect have made an architecturally significant decision. Michael Nygard defines this in his blog about architectural decision records as a decision affecting the structural composition of an application, its qualitative aspects or dependencies. Suppose you have made such a decision, knowing full well that it has been a trade-off. Let’s say you and your team has decided to build a set of micro-services to deliver a solution. These kind of decisions are hard to make and equally hard to reverse.
Creating a mess however is easy. It does not take many commits before classes with a very clear single responsibility becomes bloated service classes, utility classes pop up everywhere and architectural structure gets stretched to the breaking point. We need a good way to communicate our intended architecture. I’m not talking about the dusty old impenetrable tomes called Software Architecture Documents but rather something altogether more palatable and easier to digest. Even for developers who just want to do stuff (read: write code).
Michael Nygard, already mentioned above, introduced the idea and a format for Architectural Decision Records (ADRs). ADRs are brief summaries of the decision made, the various forces pushing for different solutions, the justification for the decision made, etc. ADRs function as a log of architectural decisions where each decision can either be proposed, approved, superseded or deprecated. When a new architectural decision supersedes a prior one both ADRs should be updated with information on which decision supersedes which.
| ADR section | Description |
|---|---|
| Title | Short, numbered noun phrase, e.g. ADR-001: Graph database for complex data relations |
| Status | Proposed, accepted, superseded or deprecated |
| Context | Forces at play on the decision, i.e. the trade-offs |
| Decision | Decision, expressed as “We will…” plus justification |
| Consequences | Context after decision is implemented |
Table 1: Structure of an ADR as imagined by Michael Nygard
Each ADR is supposed to be brief, at most 2 pages, e.g. if images or diagrams are included. However, as with all documentation ADRs runs the risk of becoming outdated or incomplete and thereby drastically loosing much of its value. This is a serious threat and if we want to prevent this from happening we need to take countermeasures. I would like to suggest two countermeasures against this threat: first a command line tool to manage your ADRs and second using ArchUnit to verify that your application follows your architectural decisions.
Every problem needs a tool and if the tool runs on the command line, so much the better. One tool for managing ADRs is Nat Pryce’s ADR-tools. This tools helps you create and update ADRs as their status updates or one ADR supersedes another. ADR Tools is available via Homebrew for Linux/MacOS X or by downloading a release zip for Windows (but might need some additional configuring for Windows).
Once installed you can navigate to your project root and type:
$ adr init
This will create an ADR stating that architectural decision records will be used. Create a new record by typing:
$ adr new Implement service using layered architecture
And a new ADR record is created with a template you can use to fill in more details about the actual architectural decision.
Alternatively there are also ADR extensions in VS Code, e.g. adr-tools or ADR Manager as well as Adr-Manager plugin for Intellij. The syntax or commands in these tools are all slightly different but they are similar enough that anyone will do.
The second countermeasure to avoid letting our ADRs and architecture at large slip into a state of slop is ArchUnit. This is a really cool test framework built for Java but a port for C# exists as well. ArchUnit let’s you write tests that can ensure your code lives by your architecture decisions, e.g. code doesn’t cross layer boundries they aren’t expected to, or that you don’t expose domain object in the APIs etc. If you are at all concerned that the developers in your team might take short-cuts that in the long run can have a detrimental effect on the architecture, give this a try. Below I include two simple ArchUnit tests. The first one (actually a copy from ArchUnits examples on GitHub but that I adapted to my own applications) verifies that the application adheres to a layered architecture, Controllers -> Services -> Persistance. The second test verifies that all public methods in my controller classes are annotated with PreAuthorize.
@AnalyzeClasses(packages = "se.callistaenterprise.demo")
class LayeredArchitectureTest {
@ArchTest
static final ArchRule adr_0001_layered_architecture = layeredArchitecture().consideringAllDependencies()
.layer("Controllers").definedBy("se.callistaenterprise.demo.api..")
.layer("Services").definedBy("se.callistaenterprise.demo.service..")
.layer("Persistence").definedBy("se.callistaenterprise.demo.persistence..")
.whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
.whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Services");
}
ArchUnit: Verify layered architecture
@AnalyzeClasses(packages = "se.callistaenterprise.demo")
class LayeredArchitectureTest {
@ArchTest
static final ArchRule adr_0002_all_endpoints_secured = methods().that()
.areDeclaredInClassesThat().areAnnotatedWith(RestController.class)
.and().arePublic()
.should().beAnnotatedWith(PreAuthorize.class);
}
ArchUnit: Verify REST endpoints are secured
Because any architectural decision is a trade-off it is necessary to document the justification for our decision. One way to document our decisions and their justifications is to use a log of significant architectural decisions in architectural decision records. These follow a common structure that is easy to read and understand. We can further help administrate our ADRs with tools like ADR Tool and verify that we implement our architectural decisions in the code by using test frameworks such as ArchUnit.
If you are still wondering why I moved to Leiden, here is the reason why. :)