Mert Özen Further With Every Line
Software Architecture Architecture Decisions — #07

Keeping Decisions Alive: Decision Logs, Architecture Reviews and the Advice Process

Mert Özen Nov 8, 2026 12 min 3 views
Keeping Decisions Alive: Decision Logs, Architecture Reviews and the Advice Process

A decision log only works while it stays discoverable and is reviewed regularly: a generated index, a supersede chain checked in CI, calendar and condition triggers and a 30-minute monthly review. A C# tool that audits Bookstore's 15-ADR log, the advice process and the architecture advisory forum, the maintenance status of ADR tools at the time of writing, and a minimum process for a team of six.

Version note: The code in this post was built and run with C# 14 and .NET 10.

Guess First

The Bookstore team has grown to six people: four developers, one ops engineer, one product manager. It has been eight months since they started writing ADRs (Architecture Decision Records). The decision log folder today:

$ ls docs/decisions
0001-record-architecture-decisions.md
0002-sql-server-as-the-single-database.md
0003-start-with-a-modular-monolith.md
0004-order-email-through-an-in-process-queue.md
0005-public-order-number-format.md
0006-angular-spa-in-front-of-the-api.md
0007-use-postgresql-instead-of-sql-server.md
0008-cache-catalog-search-with-hybridcache.md
0009-payment-retries-with-a-resilience-pipeline.md
0010-public-api-compatibility-within-a-major-version.md
0011-order-events-through-a-transactional-outbox.md
0013-every-migration-can-be-rolled-back.md
0014-initial-bundle-stays-under-250-kb.md
0015-stay-on-a-supported-lts-dotnet-release.md
12-Shipping-Labels-As-PDF.md

Barbara, who joined the team this week, asks two questions: "Why do order e-mails go through a queue?" and "Is there a decision about shipping labels?" Can she answer both by looking at the list? And one more: which of these fifteen records deserves to be discussed again today, even though nobody has changed anything? Is there anything in the list that says so?

Keeping the Decision Log Discoverable

Three problems come out of the list. First, Barbara opens 0004 and reads the in-process queue decision, but that record is no longer valid; the file name does not tell her how to get to ADR-0011, which replaced it. Second, the shipping decision sits at the very end of the list with a different naming and is lost in ls's ordering; on top of that, nobody knows why number 12 is not 0012. Third, no file name says a decision's status, its topic or when it will be reviewed.

The value of a decision log is limited by how findable its records are. That takes three things: an index that shows each record's status and topic, an unbroken supersede chain, and consistently formatted records. Instead of leaving all three to human attention, we gave them to a small tool. We added two fields to the MADR (Markdown Architectural Decision Records) front matter: tags (the topic) and review-after (the next review date). MADR's front matter consists of optional fields, and nothing stops a team from adding its own:

---
status: accepted
date: 2026-03-12
tags: [structure]
review-after: 2026-09-12
---

The tool reads every ADR in the folder, checks the rules, lists those that need a review, and, if there are no errors, rewrites the index, that is, README.md:

using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using Bookstore.DecisionLog;

string folder = args[0];
DateOnly today = DateOnly.Parse(args.ElementAtOrDefault(1) ?? DateTime.UtcNow.ToString("yyyy-MM-dd"), CultureInfo.InvariantCulture);

Adr[] log = [.. Directory.GetFiles(folder, "*.md").Where(f => Path.GetFileName(f) != "README.md").Order().Select(Adr.Load)];
List<string> errors = [.. DecisionLogRules.Check(log)];
List<string> reviews = [.. DecisionLogRules.DueForReview(log, today)];

foreach (string e in errors) Console.WriteLine($"error: {e}");
foreach (string r in reviews) Console.WriteLine($"review: {r}");

if (errors.Count == 0)
{
    File.WriteAllText(Path.Combine(folder, "README.md"), DecisionLogRules.Index(log));
    Console.WriteLine($"index written: {log.Length} decisions");
}
return errors.Count == 0 ? 0 : 1;

namespace Bookstore.DecisionLog
{
    public sealed record Adr(string File, int Number, string Title, string Status, DateOnly Date, string[] Tags, DateOnly? ReviewAfter)
    {
        public static Adr Load(string path)
        {
            string[] lines = System.IO.File.ReadAllLines(path);
            Dictionary<string, string> meta = lines.Skip(1).TakeWhile(l => l != "---")
                .Select(l => l.Split(':', 2))
                .ToDictionary(p => p[0].Trim(), p => p[1].Trim().Trim('"'));
            string file = Path.GetFileName(path);
            int number = int.TryParse(file.Split('-')[0], out int n) ? n : 0;
            string title = lines.First(l => l.StartsWith("# ")).Substring(2);
            string[] tags = meta.TryGetValue("tags", out string? t) ? t.Trim('[', ']').Split(", ") : [];
            DateOnly? review = meta.TryGetValue("review-after", out string? r) ? DateOnly.Parse(r, CultureInfo.InvariantCulture) : null;
            return new Adr(file, number, title, meta["status"], DateOnly.Parse(meta["date"], CultureInfo.InvariantCulture), tags, review);
        }

        public string Id => $"ADR-{Number:D4}";
    }

    public static partial class DecisionLogRules
    {
        private static readonly string[] Statuses = ["proposed", "accepted", "rejected", "deprecated"];

        [GeneratedRegex(@"^\d{4}-[a-z0-9]+(-[a-z0-9]+)*\.md$")]
        private static partial Regex FileName();

        [GeneratedRegex(@"^superseded by (ADR-\d{4})$")]
        private static partial Regex SupersededBy();

        public static IEnumerable<string> Check(Adr[] log)
        {
            Dictionary<string, Adr> byId = log.Where(a => a.Number > 0).ToDictionary(a => a.Id);
            foreach (Adr adr in log)
            {
                if (!FileName().IsMatch(adr.File))
                    yield return $"{adr.File}: file name must look like NNNN-title-with-dashes.md";

                Match superseded = SupersededBy().Match(adr.Status);
                if (superseded.Success)
                {
                    // Keep the chain intact: the new record must exist and must be accepted
                    string next = superseded.Groups[1].Value;
                    if (!byId.TryGetValue(next, out Adr? target))
                        yield return $"{adr.File}: superseded by {next}, which does not exist";
                    else if (target.Status != "accepted")
                        yield return $"{adr.File}: superseded by {next}, which is '{target.Status}'";
                }
                else if (!Statuses.Contains(adr.Status))
                    yield return $"{adr.File}: unknown status '{adr.Status}'";
            }
        }

        public static IEnumerable<string> DueForReview(Adr[] log, DateOnly today)
        {
            foreach (Adr adr in log)
            {
                if (adr.Status == "accepted" && adr.ReviewAfter is DateOnly due && due <= today)
                    yield return $"{adr.Id} '{adr.Title}' was due for review on {due:yyyy-MM-dd}";
                if (adr.Status == "proposed" && today.DayNumber - adr.Date.DayNumber > 30)
                    yield return $"{adr.Id} '{adr.Title}' has been proposed for {today.DayNumber - adr.Date.DayNumber} days";
            }
        }

        public static string Index(Adr[] log)
        {
            StringBuilder md = new();
            md.AppendLine("# Decision log").AppendLine();
            md.AppendLine("| ADR | Title | Status | Date | Tags |");
            md.AppendLine("|-----|-------|--------|------|------|");
            foreach (Adr a in log.OrderBy(a => a.Number))
                md.AppendLine($"| [{a.Id}]({a.File}) | {a.Title} | {a.Status} | {a.Date:yyyy-MM-dd} | {string.Join(", ", a.Tags)} |");
            return md.ToString();
        }
    }
}

Running it on the opening folder:

$ dotnet run -- ../../docs/decisions 2026-11-08
error: 12-Shipping-Labels-As-PDF.md: file name must look like NNNN-title-with-dashes.md
error: 12-Shipping-Labels-As-PDF.md: superseded by ADR-0021, which does not exist
review: ADR-0003 'Start with a modular monolith' was due for review on 2026-09-12
review: ADR-0009 'Payment retries with a resilience pipeline' has been proposed for 68 days
exit code: 1

The two errors are the mechanical counterpart of two problems from the opening: the file name does not match the pattern, and the record appears to be superseded by an ADR-0021 that does not exist. The record was merged with a draft status line left over from another PR (pull request). After renaming the file to 0012-shipping-labels-as-pdf.md and fixing the status:

$ dotnet run -- ../../docs/decisions 2026-11-08
review: ADR-0003 'Start with a modular monolith' was due for review on 2026-09-12
review: ADR-0009 'Payment retries with a resilience pipeline' has been proposed for 68 days
index written: 15 decisions
exit code: 0
# Decision log

| ADR | Title | Status | Date | Tags |
|-----|-------|--------|------|------|
| [ADR-0001](0001-record-architecture-decisions.md) | Record architecture decisions | accepted | 2026-03-02 | process |
| [ADR-0002](0002-sql-server-as-the-single-database.md) | SQL Server as the single database | accepted | 2026-03-05 | data |
| [ADR-0003](0003-start-with-a-modular-monolith.md) | Start with a modular monolith | accepted | 2026-03-12 | structure |
| [ADR-0004](0004-order-email-through-an-in-process-queue.md) | Order e-mail through an in-process queue | superseded by ADR-0011 | 2026-03-19 | messaging, ordering |
| [ADR-0005](0005-public-order-number-format.md) | Public order number format | accepted | 2026-03-26 | ordering, api |

Barbara now sees in the index that ADR-0004 is superseded by ADR-0011 and follows the link to the new record; the "messaging" tag finds both together. The tool runs in CI (Continuous Integration), together with the immutability script from the second post, on every PR that touches docs/decisions; with exit code 1 the PR turns red. It also matters that the index is a generated file: an index updated by hand goes stale just like the decision log itself.

"Is This Decision Still Valid?" Triggers

The tool's two "review" lines make the most neglected job of a decision log visible: revisiting decisions. An ADR was right on the day it was made; has the context changed? There are five triggers for asking that, and at Bookstore each has its place.

The calendar. The review-after field is the written form of saying "let's think about this again in six months" when the decision is made. ADR-0003's date passed in September; the modular monolith decision should be reviewed now that the plan to split into four teams is clear. Not every decision needs a date; only those made with low confidence and those in fast-changing contexts.

A condition. Since the second post, every ADR has a reopening condition in its More Information section: "more than 4 instances" for ADR-0008, "more than 10 lost e-mails a month" for ADR-0004. Nobody tracks these conditions on a calendar; a metric or an event triggers them. This is where the fitness functions from the sixth post come in: when a check turns red, the first question should be "do we fix the code, or reopen the decision?"

Waiting records. ADR-0009 has been proposed for sixty-eight days. A proposal that waits a long time is either a risk nobody has decided on (the "omission" kind from the third post) or a topic nobody cares about anymore. Either way the answer is the same: decide, reject, or withdraw the record.

An incident. If the root cause analysis of an incident contains the sentence "we knew this", an accepted cost has materialized. The analysis should name the related ADR's number and say whether the decision will be revisited.

A new person. Barbara's "why is it like this?" is a trigger too. If the answer is not in an ADR, we have found a missing record; if it is in an ADR but Barbara is not convinced, the record may be weak or the context may have changed. Newcomers' questions are a free review of the decision log.

A Lightweight Architecture Review

The triggers need a place to gather, or each gets lost in a separate Slack message. At Bookstore that place is a 30-minute meeting once a month. The agenda lives as a file in the decision log itself:

# Architecture review, first Tuesday of the month, 30 minutes

1. Decision log since last review (5 min): new, superseded, rejected ADRs
2. Due reviews (10 min): output of the decision log check; for each one
   "still valid / write a superseding ADR / move review-after"
3. Proposed ADRs older than 30 days (10 min): decide, reject or drop
4. Context and Container views (5 min): "is this still true?"

Output: one commit to docs/decisions with the outcomes; nothing else.

The output of the meeting is a commit: status changes, new review-after dates, drafts of new superseding ADRs. No design happens in the meeting; if a decision needs to be debated again, someone is assigned a spike like the one in the third post or a design doc like the one in the fourth. The five minutes spent on the Context and Container views from the fifth post ask what the staleness check cannot catch: whether the model tells an incomplete story about reality.

For a team of six, 30 minutes a month is six hours a year. In return the team has a decision log that knows which decisions are still valid. If the meeting ends twice in a row with "nothing to talk about", shorten it; if the decision log is growing fast, make it every two weeks.

The Advice Process and the Architecture Advisory Forum

As the team grows, the question "who makes the decision?" comes back. In the first post we touched on the advice process from Andrew Harmel-Law's December 2021 article on martinfowler.com, "Scaling the Practice of Architecture, Conversationally". Its single rule: anyone can make an architecture decision, but first they must consult everyone meaningfully affected by it and the people with expertise in the area. The decision-maker does not have to follow the advice; they have to ask for it and record it. The goal is broad input, not consensus.

Harmel-Law proposes this rule together with four supporting elements. The first is decision records: ADRs into which the advice is written too. The second is the architecture advisory forum: a weekly, hour-long open meeting where teams present proposed decisions, new spikes and decisions being revisited. The forum does not decide; attendees can only give advice or suggest someone else to consult. The third is testable architecture principles written by the teams themselves (the article suggests 8–15). The fourth is the team's own tech radar. Fowler's ADR post of March 2026 makes the same connection: the advice gathered while drafting an ADR should be summarized in the ADR.

Bookstore's monthly review is a lightweight version of the architecture advisory forum for a small team. What makes the advice process concrete is MADR's front matter: an ADR with an empty consulted field triggers the question "whom did you consult?" in PR review. The three criteria from the first post also tell you whom to consult: for a decision that crosses the system boundary, external teams and product; for a one-way door, ops and the long-term owners of the decision. When the team splits into four stream-aligned teams, the forum will become weekly, and how one team's decision affects the others will be discussed there.

A Look at Tooling

There are tools written for ADRs, and we checked the maintenance status of each on GitHub and NuGet at the time of writing. Nat Pryce's adr-tools is the reference tool for the Nygard format; its last release is 3.0.0 (July 2018) and its last commit is from March 2020. log4brains turns ADRs into a static website; its last release is v1.1.0 (December 2024) and there have been no commits since. The last NuGet release of dotnet-adr, distributed as a .NET global tool, is 1.1.8 (March 2024); the last commit in the repository is a dependency update from July 2025. MADR itself stands at 4.0.0 (September 2024), but its repository is active. In June 2026 Olaf Zimmermann and his co-authors introduced YADR (YAML ADR Template), the YAML (YAML Ain't Markup Language) counterpart of MADR; a new option for anyone looking for a format machines can read too.

This picture says something: ADR tools are small and most are unmaintained, but the need for them is small too. An ADR is a Markdown file; opening a new record means copying a template. The real value is in the generated index, the supersede chain check and the immutability rule. All of these run with the two small scripts and one console app from this series, in the team's own repository, without depending on anyone's maintenance. If you pick a tool, the test should be whether plain Markdown is left behind when the tool disappears; adr-tools, MADR and the Structurizr !adrs importer from the fifth post are safe in that sense.

A Minimum Process for a Small Team

If we reduce the seven posts of the series to a single list for Bookstore's team of six:

  • A decision gets an ADR when it is expensive to reverse, reaches several modules or the system boundary, or moves a prioritized scenario; otherwise a PR description is enough (post 1).
  • An ADR is opened as a PR with the MADR 4.0 template and status proposed; it contains at least two real options, the cost, and a reopening condition (post 2).
  • When the options are disputed, they are weighed against prioritized scenarios, with a spike if needed; sensitivity points, trade-off points and risks go into the ADR (post 3).
  • When the solution is unclear and several teams are affected, a mini design doc comes first; the decision becomes an ADR afterwards (post 4).
  • ADRs, arc42 sections and the C4 (Context, Container, Component, Code) model live in the same repository; diagrams are generated from the model and pass a staleness check in CI (post 5).
  • Every ADR's Confirmation is either an automated check or an item on a short PR checklist (post 6).
  • The decision log index is generated, the supersede chain and immutability are checked in CI, a 30-minute review happens once a month, and for every decision the people consulted are written in the consulted field (this post).

None of these items needs a special tool or a separate role. What they need is accepting that decisions are not something separate from code: they live in the same repository, are reviewed in the same PRs and are checked in the same CI.

Common Pitfalls

Write and forget. If the decision log only grows and nobody looks at it, after a while it turns into an archive nobody reads. Without calendar and condition triggers, records go stale silently.

Keeping the index by hand. A README updated by hand falls behind in the first busy week. Generate the index and check the generation in CI.

Breaking the supersede chain. A superseded by that points to a record that does not exist or was not accepted takes the reader nowhere. Let the tool check the chain.

Turning the advice process into consensus. Waiting for everyone's approval ties the decision to the slowest and most objecting person. The rule is to consult and record; the decision stays with the person making it.

Turning the review meeting into a design meeting. You cannot redesign a decision in thirty minutes. The meeting decides: still valid, to be reopened (with an owner and a date), or closed.

Depending on a tool. A decision log locked into the format of a tool whose maintenance has ended shares the tool's fate. Plain Markdown and small scripts in the repository are the longest-lived solution.

A Small Experiment

★☆☆ Run the tool on your own decision log (or on the ten decisions from the first post if you have none). How many errors and how many review lines came out? Which of the decisions without a review-after field would you give a date?

★★☆ Add two rules to the tool: a record with an empty tags field gives a warning, and an accepted ADR without a consulted field gives an error. Then add a separate section per tag to the index.

★★★ Hold your first 30-minute architecture review with your team. Start with the tool's output, go through the four agenda items in order, and commit the meeting's output as a single commit. A month later, in the second meeting, ask: of the records the first meeting decided to reopen, how many were actually revisited?

The series ends here. Across seven posts we covered recognizing a decision, recording it, weighing it, debating it in writing, documenting it, protecting it automatically and keeping it alive. Most of the decisions in Bookstore's decision log (modular monolith, outbox, cache) are covered in detail in the category's other series; the next stop is the upcoming Modular Monolith series, which turns ADR-0003 itself into code.