Documenting the context behind your code with Architectural Decision Records
Recently as part of the work I’ve been doing on Reciprocal.dev I found myself needing to record a set of decisions that I had made around the data model that the app uses.
Traditionally my first port of call would be to capture these decisions in a Confluence page where essentially they’d just act as an archive ready for a point in time where I could be bothered to do the archeology to find and read them.
A friend suggested Architectural Decision Records (ADRs) a while ago for capturing this type of information so I gave this new approach a go and I’m a little disappointed I hadn’t done this sooner.
What are Architectural Decision Records?
Put simply, Architectural Decision Records (ADRs) are a lightweight means of capturing the decisions you make as you build up the codebase for your product.
The ADR website goes into a lot more detail about the concept and the benefits but I’ll keep this post high level instead of getting bogged down in the complexities of Architetural Knowledge Management.
There are multiple formats that ADRs can follow that range from the lightweight format from Michael Nygard;
- Title — The name of the decision
- Status — The status of the decision, was it accepted, rejected etc
- Context — The context for why the decision needed to be made
- Decision — The change that is being made
- Consequences — The impact such a change will have to the code and product
To the more complex format from Jeff Tyree and Art Akerman;
- Issue — The context of why the decision is being made
- Decision — The change that is being made to solve the problem
- Status — The status of the decision, was it accepted, rejected etc
- Group — A means of grouping related decisions
- Assumptions — Are there any assumptions that are being made that have lead to the decision being made?
- Constraints — Are there any constraints that have influenced the decision?
- Positions — A list of alternative options to the chosen decision
- Argument — An outline of the argument for the chosen decision
- Implications — The impact of the the decision being made
- Related Decisions, Requirements, Artifacts, Principles — Links to other resources
- Notes — Anything that doesn’t fit into the above
However, there’s no particular format you have to subscribe to and you can even make your own for your particular needs, as long as you consistently apply the format across all records.
Why use Architectural Decision Records?
Software development is a balancing act and teams will always have to make decisions to ensure that everything stays off the ground.
If teams put too much weight on the side of technical perfection then the solution they develop may fail to provide value to the user due to being misaligned with their current needs and if there’s too much weight on the side of speedy delivery then it’s likely that the solution will be full of bugs and end up causing more problems than it fixes.
As such, there’s always going to be a need to find the center point where a team can produce a solution quick enough and to a high enough quality. Finding the center point however requires compromise and compromise means that decisions need to be made in order to move forward.
These decisions can range from broad project wide decisions such as deciding to use a microservices architecture vs a monolithic one to codebase specific decisions like the inclusion of a particular library to tackle a problem.
Usually the broad project wide decisions will involve multiple teams and many, many meetings so they’re usually documented extensively so that everyone working on the project has the context available to them in order to develop solutions that work within the desired architecture.
The smaller more codebase-specific decisions are often overlooked. They may sometimes be logged in a Spike writeup, in the README, version control commit or even in comments, but it’s rare that the context and consequences of such a decision is documented. This is where ADRs really provide value.
ADRs give developers a means to capture this information using a format that’s lightweight enough to not feel like a maintainance overhead and as the ADRs live alongside the code they are accessible without the need to leave the codebase (which is a personal bugbear for me as I hate having to search multiple systems to find a page I need to read).
ADRs allow for capturing the evolution of your codebase
As the codebase develops over time other means of documenting decisions such as READMEs and code comments will cease to provide value as they are rewritten to reflect the AS-IS and while an argument could be made that you could see the history via version control — who is going to do that?
Version control commits such as merge/pull requests provide limited value, even if they include the context for any decisions around the code and include links back to previous commits there may be multiple decisions that go into one commit, so there’s no clean way to target one specific decision and trace it’s evolution.
On a personal note, I find ADRs more useful than reading pull requests as they exist in the current codebase meaning I don’t need to trawl through the version control history to understand the decisions that lead to the code’s current condition.
Also, as someone who tends to remember specific terms instead of exact files I get a lot of value from having the ADRs turn up in the search results when searching the codebase as this allows me to read up the context around the code I’m looking for.
Using Architectural Decision Records in your codebase
ADRs are flexible — there’s no particular language, format or filetype you need to use to capture the information but there is optional tooling that will mostly likely shape your choices if you use them.
The set up I use is the Nygard format in a set of Markdown files that are managed by a NodeJS CLI tool called
adr which handles jobs like creating new records that follow a numbering system and showing which ADRs link to each other.
This is a just a personal preference and was mostly influenced by the project I’m building being in TypeScript so I was already using npm to install dependencies.
Example use — Capturing changes to my data model code
I’ll describe the approach I’ve taken for my project to illustrate the value ADRs bring to my codebase but this is by no means a defintion of the way you have to use them.
The codebase in this example contains a set of TypeScript classes that make up the data model that’s used to power the Reciprocal.dev prototype we’ve been using to validate the idea behind the app. We’ve had many iterations over the prototype.
I’ll use the decision to change the concepts of version and variants into features and configuration to illustrate how ADRs helped. I’ve omitted a lot of the technical details such as code and UML diagrams for the sake of brevity.
npm install -g adr to get the
adr tools installed on my machine I ran
adr init 'en' in order to set up the tool’s configuration for my codebase.
I then ran
adr new "A data model for representing a User Journey Map" to capture my first decision — to build the first data model for representing the data that was used to render the Reciprocal.dev map.
This created a new Markdown file called
0001-a-data-model-for-representing-a-user-journey-map.md which followed the Nygard template of status, context, decision, consequences.
I then added the context about why we needed the data in the first place (to render a map of screens and connections); the decision to store different versions of the map of screens and connections, and the consequences of doing this (we could abstract the data that powered the prototype away from the code and create multiple maps).
I also set the status to ‘accepted’ as this was the data model we had been using in the app.
Later during some user interviews it became apparent that while users understood the versions and variants concept they would actually use a different set of concepts, that of a set of features and how they responded to configuration values.
This meant that we had to change the data model to apply changes to a sub-set of the map and to have conditions attached to when those sub-sets would be changed.
I made the decision to align the data model with the concepts being used in the app and in order to capture this ran
adr new "Change version and variant concepts to configuration driven features" which created a new Markdown file called
In this file I kept the status as ‘proposed’ and provided the context for the change (the output of user interviews), the decision (to update the concepts used in the data model) and the consequences (breaking changes to code using existing data model, code will follow the same concepts so will be easier to understand, greater flexibility for use to make smaller changes).
Once the work was done to prove out the new feature & configuration data model, I updated the status of ADR-0002 to ‘accepted’ and changed ADR-0001’s status to ‘superseded by ADR-0002’.
I’ve found Architectural Decision Records really useful for documenting the context behind the decisions I’ve made as I’ve developed my data model codebase.
The process and format is lightweight enough to capture the information without it feeling like a chore and the records are easy to access thanks to being part of the codebase.
Context is key to building understanding and even if you’re a solo contributer working on a small codebase you may find ADRs useful if context switching or a long time away from the code leaves you forgetting why you made certain decisions.