Skip to content

Architecture decisions

  • Lesson 17
  • 50 min
  • Module 6: Communicate the proposal

What you will be able to do

Write each big design choice as a short record. It says what you decided, which options you compared, why, what it costs, and when to look again. Then the next person can check the reason instead of arguing again.

Before you startWhat to know and to have readyAbout 50 minutes

What you need to know

What you need on your computer

  • Your folder design-pack from the last module, with python -m pytest -q printing 46 passed.
  • The labs folder nextia-learning-labs next to it, as in every module.

Tested with Python 3.12 on macOS (Apple silicon). The Windows (PowerShell) and Linux commands are written, not tested.

A family changes its phone plan. They compare three plans. The cheapest one has 5 GB of data a month. They use about 2 GB, so they choose it. The mother writes a note and puts it on the fridge: “Cheap plan, because we use 2 GB. Look again if we use more than 4 GB.”

A year later, the son says: “This plan is too small. Why did we choose it?” Without the note, the family argues again from the start. Nobody remembers the 2 GB or the other two plans. With the note, the answer takes one minute. They check the phone’s data screen. It says 2.3 GB, so the plan is still right. If it said 4.5 GB, the note would say that it is time to change, and nobody would feel wrong.

The note is small. It keeps four things: the choice, the reason with its number, the options that lost, and the signal to look again.

The problem: a decision without its reasons

Section titled “The problem: a decision without its reasons”

Six months after launch, a customer complains about one answer. Omar asks in a meeting: “Why don’t we use the strong model? It must be better.” Tomás agrees. Amira was not in the routing work, and Priya is on holiday. Nobody can show the measurement.

So the team switches to chat-strong for every answer. The sensitivity table of Unit economics shows the cost. At the course’s end, it is US$173.99 a month instead of US$51.90: +235%. And the quality does not change. On Grace’s 67 questions, chat-strong was 85.8% correct and chat-small 86.6%, because the misses come from search. The team pays more than three times as much for nothing. The measurement was done; only the reason was lost.

The opposite failure is just as common. Tomás proposes Kubernetes and four services again, and the team spends a week in the same debate as in Simplicity and evolution. A good decision that nobody can explain gets made twice, or undone by accident.

An architecture decision is a choice about the structure of a system that is expensive to change later. Examples are one application or several services, which database keeps the data, and which model answers. A choice of a variable name is not one; a choice of a database is.

An architecture decision record, or ADR, is a short file that keeps one such decision. It is the note on the fridge for a system. Each record has six sections:

  • Status: proposed, accepted, or replaced by a later record, and who decided.
  • Context: the problem, the requirement it serves (Q1 to Q8), and the numbers that matter, each one labelled measured, price or assumption.
  • Options: two or three real choices, each with what it costs, what it does well and what it does badly.
  • Decision: the option that you chose, in one sentence.
  • Consequences: what becomes easier, what becomes harder, and what you must do now.
  • Revisit when: the measured signal that makes you look at the decision again.

Why a file, and why one decision per file? A file sits next to the design, in the same folder and the same history. A new person finds it where the work is. One decision per file keeps each record short, so people read it. And the name “record” matters. You do not change an accepted record to say something new. You write a new record that replaces it, and the old one stays as history.

A tiny example: the first record, section by section

Section titled “A tiny example: the first record, section by section”

Kwame writes the first record about Module 2’s choice. Every value in it comes from the pack. Here is the file decisions/adr-0001-one-modular-application.md in full:

# ADR-0001: One modular application with a background worker
## Status
Accepted. Kwame, with Amira.
## Context
The assistant has an interactive part (answers, J1 and J2) and a slow part (ingestion, J3). Tomás proposed separate services (an answer service, an ingestion service, a search service) on Kubernetes. Demand is small and uncertain: 1,465 questions a day in the base scenario, 0.58 requests in flight at the busiest minute (cost model, from measured times). One developer (Amira) maintains it (Q6).
## Options
1. One application, two processes: the API and an ingestion worker from the same code, the queue in PostgreSQL. One deployment, one set of tests.
2. Three services with their own deployments and an HTTP or message link between them. Each can scale alone; three pipelines, three sets of logs, network calls that can fail between them.
3. Kubernetes with the three services. All of option 2, plus a cluster to run.
## Decision
Option 1: one modular application (modules `answers`, `ingestion`, `search`, `tenants`) that runs as an API process and a worker process.
## Consequences
- One deployment and one test suite; the modules call each other as functions, not over the network.
- The worker can still run on its own and scale to zero, because it reads jobs from the queue.
- A split later is cheaper because the modules already have clear boundaries.
## Revisit when
- Ingestion needs a different machine (for example a GPU) or a different release rhythm from the API, or
- the API needs more than 4 replicas at the busiest minute, or
- a second team owns part of the code.

Read it once more, section by section:

  1. Status names the people, so a question has someone to go to.
  2. Context has the two numbers that decide it: 0.58 requests in flight at the busiest minute and one developer. It also names the requirement, Q6. The 1,465 questions a day is an assumption from Module 1; the 0.58 comes from it and from measured times.
  3. Options keeps the two options that lost, with their price in work: three pipelines, three sets of logs, network calls that can fail. Tomás’s idea is in the record, fairly stated. That is why he can accept the decision.
  4. Decision is one sentence.
  5. Consequences is honest about both sides. One deployment is easier; strict module boundaries are now a duty.
  6. Revisit when holds Module 2’s three split triggers, word for word. Each one is a signal that someone can read: the model choice, the request logs, the team list.
Six boxes in reading order, the sections of ADR-0001. Status: accepted, Kwame with Amira. Context: 0.58 requests in flight at the busiest minute; one developer, Q6. Options: one application, three services, Kubernetes. Decision, with a thick border: option 1, one modular application, an API and a worker process. Consequences: one deployment; the worker scales to zero; a later split is cheaper. Revisit when, in orange: ingestion needs a GPU; over 4 API replicas; a second team. A dashed arrow from Revisit when, labelled 'a signal fires', leads to a last box: a new record replaces this one.
The six sections of an architecture decision record, filled in with the first record. The revisit triggers close the loop: when one fires, a new record replaces this one.
Text description of the diagram

The first record in six sections. Status says it is accepted, by Kwame with Amira. Context gives the numbers: 0.58 requests in flight at the busiest minute, and one developer. Options lists one application, three services, and Kubernetes. The decision is option 1, one modular application with an API and a worker process. The consequences are one deployment, a worker that scales to zero, and a cheaper split later. Revisit when lists three triggers: ingestion needs a GPU, the API needs more than 4 replicas, or a second team arrives. When one fires, the team writes a new record that replaces this one.

A record changes its status, never its story. It starts as proposed while the owners read it. It becomes accepted when they agree. If one of its revisit signals fires and the team decides differently, a new record is accepted. The old one gets one changed line: “Replaced by ADR-0006”. Nothing else in it changes, so anyone can still read why the first choice made sense at the time.

Four states. Proposed, written but not agreed. An arrow labelled 'the owners agree' goes to Accepted, the team builds it, drawn with a thick border. From Accepted, an arrow labelled 'a revisit signal fires; a new record is accepted' goes to Replaced by ADR-NNNN, kept, never deleted. From Proposed, an arrow labelled 'the owners say no' goes to Rejected, kept with the reason.
The statuses of a record. A record is never deleted: a rejected or replaced record still explains a choice that someone will propose again.
Text description of the diagram

A record starts as proposed. If the owners agree, it is accepted and the team builds it. Later, a revisit signal can fire and the team can decide differently. Then the team accepts a new record and marks the old one “Replaced by” the new one. If the owners say no to a proposal, they mark it rejected, with the reason. In both cases the file stays in the folder.

Many teams also write the date in the status line. The pack keeps the files in git, so each commit keeps the date for you.

A decision log is the set of records, in order. The pack’s log has five. Each one comes from a module of this course:

Record Decision The number that decides it Kind Owner
ADR-0001 one modular application with a worker 0.58 requests in flight at the busiest minute measured times × assumed demand Kwame, Amira
ADR-0002 PostgreSQL for records, vectors, the queue and the answer cache about 18 MB stored in month 0; vector search 0.22 ms over 10,000 vectors measured Mei, Kwame
ADR-0003 chat-small for every answer, no routing 86.6% correct against chat-strong’s 85.8%, at 1/19 of the cost per question measured Priya, Kwame
ADR-0004 managed PostgreSQL US$77.55 a month against US$413.58, people’s time included price + assumption Mei
ADR-0005 point-in-time restore, tested every quarter RPO 5 minutes, RTO about 70 minutes assumption, to test Mei, Omar

The picture below adds the most useful column: what each record is waiting for. Each revisit trigger has today’s value next to it, so you can see how far away it is.

Five records on the left, each with an arrow to its revisit trigger and today's value. ADR-0001, one modular application: over 4 API replicas at the busiest minute; today 0.58 in flight, 1 replica. ADR-0002, PostgreSQL for everything: search p95 over 100 ms or 5 million chunks; today 0.22 ms over 10,000 vectors, measured. ADR-0003, chat-small, no routing: chat-strong 5 points better on a new question set; today 85.8% against 86.6%. ADR-0004, managed PostgreSQL: an extension it lacks, or over 4 vCores; today 1 vCore, B1ms. ADR-0005, point-in-time restore: a restore test over 2 hours; today about 70 minutes, an estimate. The last trigger is red, because its today value is not measured yet.
Larkfield's decision log: each record with its revisit trigger and today's value. Four are far from their line. The fifth, in red, has no measured value yet: the first restore test must give it one.
Text description of the diagram

The five records and what each one waits for. The modular application waits for more than 4 API replicas; today there is 1, with 0.58 requests in flight. PostgreSQL for everything waits for a search p95 over 100 ms; today a vector search takes 0.22 ms. Chat-small waits for chat-strong to be 5 points better on a new question set. Today, chat-strong is 0.8 points worse. The managed database waits for a need beyond 4 vCores; today it has 1. The restore plan waits for a test over 2 hours; today the 70 minutes are an estimate, so this trigger is shown in red.

Look at the red box. Four records rest on measurements. The fifth rests on an estimate: most of the 70 minutes are assumed people and server times. The log makes that weakness easy to see, and the roadmap in this module’s last lesson gives it a task.

Two decisions of the course have no record of their own. The answer cache lives inside ADR-0002, and batching the embeddings changes no structure. A record is for a choice that is expensive to undo, not for every good idea.

Guided practice: the decision records in the pack

Section titled “Guided practice: the decision records in the pack”
  1. In your folder design-pack, with the environment active, try the new command:

    Terminal window
    python -m costmodel check
    This command arrives in Module 6. Download the end-of-module-6 snapshot or keep going.
  2. Copy the pack check from the snapshot end-of-m06.

    Terminal window
    S=../nextia-learning-labs/C26/snapshots/end-of-m06
    cp "$S/costmodel/packcheck.py" costmodel/
  3. Run the check again. It asks the questions that a reviewer asks about the whole pack:

    Terminal window
    python -m costmodel check
    PASS requirements: journeys, out of scope, measurable quality attributes
    PASS demand: every value has evidence (verified or assumed)
    PASS prices: every price has its source
    PASS measured: every value names its run
    PASS architecture: every component has a job, an owner and a place on the diagram
    FAIL decisions: every ADR has status, context, options, decision, consequences, revisit: decisions/ has no adr-*.md
    PASS failures: at least 5, each with owner, mitigation and signal
    FAIL roadmap: every ADR appears in the roadmap: decisions/roadmap.md does not exist yet
    FAIL presentation: assumptions, risks and an unresolved assumption: decisions/presentation-outline.md does not exist yet
    Design pack check: 6 of 9 passed

    The first five modules pass. The three failures are this module’s work, one per lesson.

  4. Copy the record template into the folder decisions:

    Terminal window
    cp "$S/decisions/template-adr.md" decisions/
  5. Make a copy of the template with the name decisions/adr-0001-one-modular-application.md. Open it, and fill in the six sections from the tiny example above. Use your own words if you like, but keep the six headings exactly.

  6. Copy the other four records and the failure table. The failure table now links rows F2 and F6 to ADR-0004 and ADR-0005.

    Terminal window
    cp "$S"/decisions/adr-000[2-5]*.md decisions/
    cp "$S/decisions/failure-modes.md" decisions/
  7. Run the check:

    Terminal window
    python -m costmodel check
    PASS requirements: journeys, out of scope, measurable quality attributes
    PASS demand: every value has evidence (verified or assumed)
    PASS prices: every price has its source
    PASS measured: every value names its run
    PASS architecture: every component has a job, an owner and a place on the diagram
    PASS decisions: every ADR has status, context, options, decision, consequences, revisit
    PASS failures: at least 5, each with owner, mitigation and signal
    FAIL roadmap: every ADR appears in the roadmap: decisions/roadmap.md does not exist yet
    FAIL presentation: assumptions, risks and an unresolved assumption: decisions/presentation-outline.md does not exist yet
    Design pack check: 7 of 9 passed
  8. Run the tests. They still print 46 passed: this lesson added documents and the check, but no new test.

    Terminal window
    python -m pytest -q
    .............................................. [100%]
    46 passed in 0.08s

    If you want your pack to equal the snapshot exactly at the end of the module, copy the snapshot’s ADR-0001 over yours now. Keep your own text in another folder to compare.

The decisions check passed. But try this: before you write ADR-0001, save a copy of the empty template under the name adr-0001-one-modular-application.md. The check still prints PASS decisions. It looks for the six headings, not for the reasons under them.

So the check is the floor, not the review. A person must still read each record and ask five questions:

  1. Does every number in the context say measured, price or assumption?
  2. Are the options real, and is the losing side stated fairly?
  3. Does the decision fit in one sentence?
  4. Do the consequences name a cost, not only benefits?
  5. Can someone read each revisit trigger from a log, a test or a list, today?

The fifth question catches the most. “Revisit when we grow” passes the check and fails the review.

Write one when a choice is expensive to undo, when people disagreed, or when the reason is a measurement that took work. Larkfield’s five records each pass at least two of these tests. A record takes about half an hour to write, and it saves the next debate.

Do not write one for choices that are cheap to change: a library version, a variable name, the colour of a button. A log of fifty small records hides the five that matter.

What it costs to keep: the triggers need someone to look at them. Larkfield’s growth plan from Module 5 already reads three signals every month. The others are in the request logs, the evaluation set and the team list.

Common mistake

A record without its revisit section

Symptom: the check fails on the decisions:

FAIL decisions: every ADR has status, context, options, decision, consequences, revisit: adr-0003-chat-small-without-routing.md has no ## Revisit when

Cause: someone renamed or deleted the heading ## Revisit when in ADR-0003. Without it, the record does not say which measurement would change the decision, so nobody will check it again.

Fix: put the heading back with a measured trigger. For ADR-0003: “a new question set shows chat-strong at least 5 points better than chat-small”.

Common mistake

An accepted record, rewritten

Symptom: ADR-0003 now says “chat-strong answers every question”. Nobody can find the routing measurement any more, and the next reviewer asks why the cost tripled.

Cause: someone edited the old record to match a new decision. The history of the reason is gone.

Fix: restore the old text from git. Write a new record, ADR-0006, with its own context and options. In ADR-0003, change only the status line to “Replaced by ADR-0006”.

Change one condition. Priya builds an evaluation set for Bramble Books: 40 questions about supplier terms, with long tables. On it, chat-strong is 7 points better than chat-small. This case is constructed for the exercise; the course did not measure it.

ADR-0003’s trigger was “at least 5 points better”. Write the new record, ADR-0006, in a scratch file outside your pack, so your pack still equals the snapshot. Give it the six sections. Use the measured costs per question from Module 4. Chat-small costs US$0.000171, routed US$0.000815, and chat-strong US$0.003197. Bramble Books asks about 65 questions a day in the base case, about 1,950 a month. Then write the one line that changes in ADR-0003.

Show a worked solution
# ADR-0006: Route Bramble Books' questions to chat-strong when chat-small declines
## Status
Proposed. Priya and Kwame. Replaces ADR-0003 for Bramble Books only.
## Context
Bramble Books' new evaluation set (40 questions on supplier terms) shows chat-strong 7 points
better than chat-small (constructed for this exercise; to measure on the real set). ADR-0003's
revisit trigger was 5 points. Bramble Books asks about 1,950 questions a month (assumption,
demand.toml base). Q4: at least 85% correct.
## Options
1. Keep chat-small for Bramble Books: US$0 more; 7 points below what chat-strong reaches.
2. Routed for Bramble Books (escalate when chat-small declines or a citation check fails):
(0.000815 - 0.000171) x 1,950 = about US$1.26 more a month (measured costs per question,
the routing measurement's escalation share; to check on the new set).
3. chat-strong for every Bramble Books question: (0.003197 - 0.000171) x 1,950 = about
US$5.90 more a month; p95 6.56 s against 3.79 s.
## Decision
Option 2 for Bramble Books; Larkfield stays on chat-small (ADR-0003).
## Consequences
- The model choice becomes a setting per tenant, so the tenants' table gets a column.
- Priya runs both evaluation sets before every model or prompt change.
## Revisit when
- Routed answers for Bramble Books are not at least 5 points better than chat-small on its set, or
- the escalation share passes 40%, where option 3 becomes simpler for about the same price.

The one line that changes in ADR-0003 is its status: “Accepted. Priya and Kwame. Replaced by ADR-0006 for Bramble Books.”

Check your record against the five review questions. Is every number labelled? The 7 points are constructed, the costs per question are measured, and the volume is an assumption. Are the losing options fair? Option 3 gives the best quality, but it costs more and is slower. Is each trigger readable? Both come from the evaluation set and the request log.

Check your understanding

Why does a record keep the options that lost? Give one way it helps, with an example from Larkfield.

Show the answer

The losing options show that the choice was compared, not guessed, and they keep the other side’s case fairly. When Tomás proposes Kubernetes again, ADR-0001 already lists it as option 3, with its costs. These are three pipelines, a cluster to run, and network calls that can fail. The team does not repeat the debate. It checks the revisit triggers instead. If none has fired, the answer stands; if one has, the team writes a new record.

Check your understanding

Predict: at the end of the guided practice, you delete the heading '## Options' from ADR-0004 and run python -m costmodel check. What does the decisions line say, and how many checks pass?

Show the answer

The decisions line fails: FAIL decisions: every ADR has status, context, options, decision, consequences, revisit: adr-0004-managed-postgresql.md has no ## Options. Only 6 of 9 checks pass. The decisions check fails now. The roadmap and the presentation still fail, because their files do not exist yet.

Check your understanding

Omar asks: 'Can we drop the restore test? It costs people's time every quarter.' Which record answers him, and what would you say, in three or four sentences?

Show the answer

ADR-0005 answers him. Its decision is a point-in-time restore tested every quarter, because the RTO of about 70 minutes is mostly assumed people and server time. The test is the only way to measure it, and the decision log shows it as the one record without a measured value. Its revisit trigger is “a restore test takes more than 2 hours”. Without a test, that trigger can never fire, so the record could be wrong without anyone knowing.

Recap

  • An architecture decision is a choice about structure that is expensive to change. An ADR keeps one such decision in a short file with six sections: status, context, options, decision, consequences and revisit when.
  • Label every number in the context: measured, price or assumption. Keep the options that lost, fairly stated.
  • Never rewrite an accepted record. A new record replaces it, and the old one changes only its status line.
  • Larkfield’s decision log has five records. Four rest on measurements; ADR-0005’s 70 minutes are an estimate until the first restore test.
  • python -m costmodel check now prints 7 of 9 passed. It checks the shape of each record; a person reviews the content.

Next: Stakeholder presentation