We’ve all been there. Fixing a bug, or perhaps implementing a whole new feature in a legacy project, you come upon a class (or perhaps an entire package structure) that seems to make no sense. It looks convoluted and your code must interact with it. There is an immediate desire to refactor it to something different, but what if there is a compelling reason behind the madness? In another part of the codebase, you can see three adapters, implemented using four different styles. Which one should you go with, when writing a new adapter? The answer may be hidden 4 levels deep in a Confluence page, but who knows if the information there is even up to date? Perhaps the only person who knew the answer is no longer available.
The motivating problem
The term Architecture Decision Record was first introduced by Michael Nygard in his 2011 article “Documenting Architecture Decisions”. In it, he made the following claims (paraphrasing):
- Architectural decisions in an agile project will be made throughout its lifespan (not all at once),
- Large documents are never kept up to date,
- Small, modular documents have at least a chance at being updated,
- Nobody ever reads large documents.
A new person joining a project (or even an existing developer seeing a new part of the codebase) may have a variety of emotional reactions to a past decision. When the rationale or consequences of it are not known, that person can either blindly accept, or blindly change that decision. Neither of these is preferred.
Blindly accepting a decision may be the wrong choice, if the context has changed, and too much blind acceptance can lead to a fearful approach to the codebase (“don’t change anything, or it might break”).
On the other hand, blindly changing the decision can have negative consequences, as well. After all, it is likely that there was a motivating force behind it, such as an untested non-functional requirement.
Benefits of using ADRs
ADRs serve as a historical record of architecturally significant decisions, which preserve the decisions, their context and consequences, as well as their current status.
The context provides information about the forces at play (technological, political, social, etc.) when the decision was made, making it easier to see whether the rule should still apply, or if it should be challenged.
Preserving the decision itself makes it possible for ADRs to serve as a list of rules for interacting with the codebase. New joiners can read them and immediately understand how they should write their code.
The consequences show how the decision affects the project in all the different ways – positive, neutral and negative. It shows that the decision itself was not made blindly. If the ADR is being proposed (e.g., using a Pull Request), it helps maintainers evaluate whether it should be accepted.
Finally, the status helps keep track of which ADRs are currently accepted. As the project evolves, new ADRs may be proposed, while existing ADRs may be deprecated or superseded (with a reference to the replacement). Keeping inactive ADRs alongside active ones prevents teams from repeating mistakes such as accidentally reverting to a decision that was already proven not to work.
Well-written ADRs eliminate architectural ambiguity, while reviewing past ADRs allows teams to learn from previous decisions and make more informed choices over time.
Writing an ADR
An Architecture Decision Record is a short text file, using a lightweight text formatting language like Markdown or Textile. Each file should be numbered sequentially and monotonically. Numbers must not be reused.
If a decision is reversed, the existing file should be kept, but marked as superseded (with a reference to the new ADR).
Over the years, many different formats have been proposed, some of which have been gathered in a Github repository by Joel Parker Henderson.
Perhaps the best way to start writing your own is with Michael Nygard’s original template, which his aforementioned article follows (making it a great example). The one change I will introduce is moving the status higher, to make it easier to see:
# Title
A short noun phrase, e.g. "ADR 1: Database Migration Strategy".
## Status
E.g., Proposed, Accepted, Rejected, Deprecated, Superseded
## Context
What are the forces at play (technological, political, social, project local) when making this decision?
## Decision
What is the response to those forces? It should be stated in full sentences, with active voice (e.g., "We will …").
## Consequences
What are the positive, neutral and negative consequences of this decision?
Another format you might want to adopt is MADR 4.0.0 (see examples here). Some of its notable differences (as used in the MADR repository) are:
- Its titles use active voice and omit the version number, e.g., “Use Dashes in Filenames”,
- It lists all considered options (decision and alternatives) and encourages listing pros and cons for each of them
- The decision should provide a justification (why it was better than the alternatives)
- It provides additional metadata as Markdown front matter (e.g., date, list of decision makers)
It is, however, more complex (especially in its full form) and you may find its structure not entirely to your liking. If that is the case, it can still be valuable as a source of inspiration for the format you end up adapting in your project.
Storing ADRs
ADRs, being decisions about code, should be stored with the code. They should be stored in their own folder in the repository, such as docs/adr/NNN-title.md
.
This has the added benefit that new ADRs can be proposed by issuing Pull Requests against the repository and discussions about them can be had asynchronously using code review tools.
If external stakeholders must have access to these ADRs, or if easier access is desired, the ADRs can be automatically published as a website as part of a CI pipeline. An example of this is the documentation of Backity, which uses GitHub Pages and Just the Docs to publish its developer documentation along with ADRs.
ADRs as code
The one disadvantage of ADRs is that decisions stored in plain-text files can’t be automatically enforced. An alternative is to write ADRs as code, using tools like ArchUnit. A simple ArchUnit rule can look like this:
@ArchTest static final ArchRule INTERFACE_NAMES_SHOULD_NOT_START_WITH_I = noClasses().that().areInterfaces() .should().haveNameMatching(".*\\.I[A-Z][A-Za-z0-9_-]*") .because("the prefix does not add any useful information");
ArchUnit tests can then run as part of CI and enforce architectural rules, while the tests preserve a historical record. While the API may be lacking a place for context and consequences, these can be provided either via a comment or the because()
method. Deprecated or superseded ADRs can be disabled (anntotated with @Disabled
in case of ArchUnit tests).
The previous ArchUnit rule can be refactored to include the missing information:
// Status - accepted, because not @Disabled @ArchTest static final ArchRule INTERFACE_NAMES_SHOULD_NOT_START_WITH_I = // Title // Decision: noClasses().that().areInterfaces() .should().haveNameMatching(".*\\.I[A-Z][A-Za-z0-9_-]*") .because(""" the prefix does not add any useful information. Context: Historically, some codebases prefix interfaces with 'I' (e.g., `IService`, `IRepository`). \ This convention stems from older programming languages but \ does not add meaningful information in modern development. Positive consequences: - Interface names will feel more natural and domain-driven, \ making them easier to understand at a glance. """);
An example violation (reported by Junit) would look like this:
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that are interfaces should have name matching '.*\.I[A-Z][A-Za-z0-9_-]*', because the prefix does not add any useful information.
Context:
Historically, some codebases prefix interfaces with 'I' (e.g., `IService`, `IRepository`). This convention stems from older programming languages but does not add meaningful information in modern development.
Positive consequences:
- Interface names will feel more natural and domain-driven, making them easier to understand at a glance.
' was violated (1 times):
Class <dev.codesoapbox.backity.core.backup.application.IGameProviderFileBackupService> has name matching '.*\.I[A-Z][A-Za-z0-9_-]*' in (IGameProviderFileBackupService.java:0)
Given the obvious advantage of automatic enforcement, one should prefer writing ADRs as code and resort to plain-text files only when the alternative is not possible. In some cases, when a decision requires a major refactoring, you may want to write ADRs as plain-text files, then supersede them with executable ADRs once the refactoring is done.
Photo by Sergio Capuzzimati
Be First to Comment