Skip to content
Colin Wren
Twitter

Documenting the context behind your code with Architectural Decision Records

Documentation, Software Development6 min read

filing cabinets
Photo by Jan Antonin Kolar on Unsplash

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.

After running 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.

1# 1. A data model for representing a User Journey Map
2
3Date: 2021-04-16
4
5## Status
6
72021-04-16 accepted
82021-06-16 superseded by ADR-0002
9
10## Context
11We successfully validated the idea of reciprocal.dev using a prototype that used a hardcoded set of predefined behaviours.
12
13In order to build a system which will allow users to create their own maps we need a data model that will hold information on the different screens and connections for multiple versions and variants.
14
15## Decision
16
17Decided to extract the values out of the prototype and put them into a structure that allowed the prototype to work with a simple JSON document.
18
19The data model has the following concepts:
20
21- Screen: The positioning and image for a screen shown on the map
22- Connection: The positioning, path and label for a connection between screens
23- Version: An object comprising of a list of screens and connections
24- Variant: An object that describes how the values of the screens and connections for a specific version are updated
25
26When a version is rendered the entire map is re-rendered to show that version's map.
27
28When a variant is selected the map for the currently selected version is updated based on the changes described in the variant.
29
30## Consequences
31
32Prototype was adapted to use a set data structure and the data for the app was moved into a JSON document that could be read in at runtime.
ADR describing the decision to change the prototype to be data driven and the development of the first data model

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 0002-change-version-and-variant-concepts-to-configuration-driven-features.md.

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).

1# 2. Change version and variant concepts to configuration driven features
2
3Date: 2021-06-16
4
5## Status
6
72021-06-01 proposed
82021-06-16 accepted
9
10## Context
11After conducting more user interviews to understand the terminology and approach that teams took to describing variance in their products we discovered that most people looked at a product's features and how they changed based on configuration (such as market and device features).
12
13These features are often associated with a particular team that is responsible for providing new functionality so it's important to have the ability to independently version a feature.
14
15We also encountered scaling issues with the current data model that applies variants to a version of the map. The more versions there was the more complex the variant object became.
16
17## Decision
18
19- We decided to change the way that we handled variance in the map and divided the map into collections of screens to represent a feature.
20- We added the concept of configuration values, these are global values that can be set across the map.
21- Features can be versioned independently of each other and use the global configuration values to determine which version to show.
22
23## Consequences
24
25- The data model is now able to scale better as each features allow for each sub-set of map objects to handle their own versions and variance based on configuration values
26- We can align the map and data model with the concepts and terminology the user is using
27- Using the same terminology and concepts as the map in the data model will mean the data model code is easier to understand
28- We need to update the prototype to use the new data model
ADR describing changes to the data model based off feedback from user interviews

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’.

Summary

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.