Adding implementation plans and snapshots to ADRs
On this page9 sections ▾
An Architecture Decision Record (ADR) explains the context, options, and chosen approach. I find it more useful when it also connects the decision to the code: the files likely to change, or a small example of the pattern that was implemented.
For example, "We will implement a distributed cache for the web app" records the decision, but leaves questions about the caching pattern, registration, and abstraction. A short implementation section gives the next developer somewhere to start.
The contents depend on whether you write the ADR before or after implementation.
#When do you write the "how"? Proactive vs. reactive
Are you writing the ADR before implementation (proactive) or after (reactive)? Both are valid, but the "how" section serves a different purpose in each case.
#Scenario 1: ADR up-front (the "Expected Touchpoints")
Write the ADR as part of the design phase, before a single line of code is written. This is the recommended approach.
Call the "how" section "Expected Implementation Touchpoints" or "Implementation Plan."
It's a high-level list of the files, modules, and classes you expect to change — your plan for implementing the decision. It helps the team review the scope and gives the developer a starting checklist.
One rule: do not go back and update this section after the work is done. Its value is as a snapshot of the plan, not the final implementation. If the reality differed from the plan, that's useful information in itself (and worth a comment in the PR).
#Scenario 2: ADR after-the-fact (the "Implementation Snapshot")
Sometimes you're documenting a decision that was already made, or you're getting started with ADRs and back-filling your decision log.
Call the "how" section "Implementation Snapshot."
It's a set of small, illustrative code snippets that capture the actual pattern that was implemented. It connects the abstract decision to the real code, so future developers don't have to dig through PR history to figure out how something was originally built.
One rule: this is a snapshot, not a live-updating document. You're capturing the pattern, not committing to maintaining it.
#Keeping the implementation section useful
The section should connect a decision such as "We will add caching" to a concrete approach: registering IDistributedCache in Program.cs and using Cache-Aside in WeatherService. It should also be clear whether the text records the plan or the implemented pattern.
For Scenario 1: label it as the original plan, so readers do not mistake it for a description of the current code.
For Scenario 2: it's a snapshot of the pattern, not the values. If your cache expiration time changes, the ADR is still correct because the pattern of using the cache is unchanged. If the pattern itself changes — say you switch to read-through caching — that's a new architectural decision and warrants a new ADR to supersede the old one.
Keep the snippets focused on signatures, DI registration, and the relevant pattern. A line-by-line diff belongs in the linked PR.
#A concrete example: the "Weather API" caching ADR
Here's an ADR for a hypothetical Weather API, written after the fact (Scenario 2) using the Implementation Snapshot pattern.
Caching - Implement Redis Distributed Cache for Weather API
- Status: accepted
- Deciders: Gordon Beeming
- Date: 2025-11-14
- Tags: caching, performance, redis, .net, csharp
Technical Story: PBI 42 - Product API is slow under load
#Context and Problem Statement
The Weather API is performing poorly under load, specifically when fetching detailed forecasts. Each request results in a complex SQL query. The application is hosted in a scaled-out, multi-instance environment (e.g., Azure Container Apps with 2+ replicas). We need to reduce database load and improve API response times.
#Considered Options
- In-Memory Cache (
IMemoryCache): Use the built-in .NET in-memory cache. - Distributed Cache (
IDistributedCachewith Redis): Use a shared, external cache. - No Cache: Continue hitting the database for all requests.
#Decision Outcome
Chosen option: "Distributed Cache (IDistributedCache with Redis)", because IMemoryCache is in-process. In our scaled-out environment, this would lead to data inconsistency, as one instance's cache would not be shared with the others. A distributed cache provides a single, shared source of truth for all instances.
#Consequences
- ✅ Cache hits avoid the forecast database query, reducing work per request.
- ❌ Adds new infrastructure: a Redis instance (increased cost and maintenance).
- ❌ Adds a new point of failure (if Redis is down, we must handle cache misses gracefully).
- ⚠️ We now have a cache invalidation strategy to manage (a new source of complexity).
#Implementation Snapshot
(This ADR was written after the fact. If it had been written up-front, this section would be "Expected Touchpoints" and might just list the files: Program.cs and Services/WeatherService.cs.)
To implement this, we registered the Redis cache service in Program.cs and modified the WeatherService to implement the Cache-Aside Pattern.
Pattern 1: DI service registration (Program.cs)
This snippet shows how we registered the IDistributedCache implementation with .NET's dependency injection container.
//
// File: Program.cs
//
var builder = WebApplication.CreateBuilder(args);
// ... other services
// Add Redis Distributed Cache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "WeatherAPI_";
});
// ...Pattern 2: The Cache-Aside pattern (WeatherService.cs)
This shows the core logic. We modified the existing service to check the cache before doing the expensive database call.
//
// File: Services/WeatherService.cs
//
public class WeatherService : IWeatherService
{
private readonly IDistributedCache _cache;
private readonly WeatherDbContext _db;
// ... other dependencies
public WeatherService(
IDistributedCache cache,
WeatherDbContext db,
ILogger<WeatherService> logger)
{
_cache = cache;
_db = db;
// ...
}
public async Task<Forecast> GetForecastAsync(string location)
{
string cacheKey = $"forecast:{location}";
// 1. Try to get from cache
var json = await _cache.GetStringAsync(cacheKey);
if (json is not null)
{
return JsonSerializer.Deserialize<Forecast>(json);
}
// 2. Cache Miss: Get from "real" service (the existing logic)
var forecast = await _db.Forecasts
.Where(f => f.Location == location)
.FirstOrDefaultAsync(); // <-- This is the expensive part
// ... (assume null handling)
// 3. Set in cache for next time
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
};
json = JsonSerializer.Serialize(forecast);
await _cache.SetStringAsync(cacheKey, json, options);
return forecast;
}
}#Links
#The ADR as a funnel: progressive disclosure
Arrange the ADR so readers can get the decision first, then continue to the implementation details if they need them.
-
Level 1: The "What" (title, status, date) Anyone scanning the list of ADRs wants to know: "What decisions have been made recently?" They get that and move on.
-
Level 2: The "Why" (context, decision outcome, consequences) Product managers, team leads, architects, new developers. They want to know what the problem was, what was decided, and why. Most people stop here.
-
Level 3: The "How" (implementation snapshot/touchpoints) Developers implementing the feature, or future developers implementing something similar. They want to know: "What is the approved pattern? What does this look like in our code?" The snapshot gives them that starting point.
-
Level 4: The "Deep Dive" (links) Maintainers can follow the links to the exact code and the PR discussion.
This lets readers stop at the decision summary or continue into implementation details and the original PR.
For a new decision, add the expected files and components before implementation. For an existing decision, capture a small example of the implemented pattern and link the PR. Label either section as a snapshot so readers know what it records.