DEVBLUEPRINTS

Blog

  • Sobre o blog
  • Arquivo
  • Newsletter
  • RSS

Legal

  • Termos e Privacidade
  • Contato
  • Sobre mim

Inscreva-se na Newsletter

Autorizo o envio de comunicações por e-mail ou qualquer outro meio e concordo com os Termos e Política de Privacidade

 

Blog
  • Sobre o blog
  • Arquivo
  • Newsletter
  • RSS
Legal
  • Termos e Privacidade
  • Contato
  • Sobre mim
© 2026 Todos os direitos reservados — Desenhado e construido comFooter Heartpor Ednaldo Luiz
Blog/System Design

Architecture Decision Records (ADR): record decisions without creating bureaucracy

Code shows how the system works, but it rarely explains why it took that shape. Learn to use ADRs to preserve context, alternatives, and trade-offs without turning architecture into bureaucracy.

software architecture
architecture decisions
documentation
best practices
Architecture Decision Records (ADR): record decisions without creating bureaucracy
Ednaldo Luiz
Ednaldo Luiz
Level: Intermediate
Level:
Published: 22 de agosto de 2026
Last updated: 22 de agosto de 2026
20 min read
views: - views

Introduction

You know that architecture decision that felt obvious when it was made?

The team picked PostgreSQL, put a queue between two services, adopted an identity provider, or decided to keep the system as a modular monolith.

Back then, there were probably good reasons.

The choice may have been shaped by a tight deadline, by the team's operational capacity, by a regulatory requirement, or simply because the volume did not yet justify a more complex solution.

A few months later, though, all that is left is the implementation.

Code shows what was built, but it does not explain why that option won, which alternatives were rejected, and what price the team agreed to pay.

Then someone asks:

Why did we do it this way?

If nobody remembers, the answer has to be reconstructed from commits, old tickets, Slack messages, and conversations with people who may no longer be at the company.

That usually leads to two bad behaviors.

The first is treating the existing architecture as untouchable:

There must be a reason. Better not touch it.

The second is changing everything without knowing the context:

This looks wrong. Let's redo it.

Either way, the team decides in the dark.

popularized Architecture Decision Records in 2011 precisely to preserve the reasoning behind architecturally significant decisions, using short, modular documents kept close to the project. Current Microsoft guidance follows the same idea: a system's architecture is the accumulated result of its decisions, not just the current diagram.

An Architecture Decision Record, or ADR, captures a decision while the context is still fresh.

Not to set up a technical records office, and not to document every detail of the code.

The goal is simple: keep important decisions from losing the reason that made them sensible.

TL;DR

An ADR is a short record of a relevant architecture decision, covering the problem, the factors that shaped the choice, the alternatives evaluated, and the consequences accepted.

Record decisions with lasting impact, high cost of reversal, or effects on structure, contracts, security, operations, and quality attributes.


What an ADR actually records

An Architecture Decision Record captures a single architecture decision and the reasoning behind it.

The collection of these documents forms the Architecture Decision Log, the system's decision history.

  • 0000-adopt-adr.md
  • 0001-use-managed-postgresql.md
  • 0002-call-fulfillment-over-http.md
  • 0003-publish-events-with-outbox.md

An ADR does not need to describe the whole architecture.

It captures one specific point:

why decide
what was weighed
what stands
the price accepted
Context
→
Alternatives
→
Decision
→
Consequences
why decide
Context
→
what was weighed
Alternatives
→
what stands
Decision
→
the price accepted
Consequences

Nygard's original template has five fields: title, status, context, decision, and consequences. Each file handles a single decision, stays short, and remains in the repository even after it is superseded.

It is a starting point, not a contract. If you look at it and miss owner, category, or scope, add them. If a field nobody fills in is there, drop it. What has to survive is the reasoning, not the form.

One decision per file. That is what separates an ADR from the other documents that show up during development.

ArtifactMain questionRole in the lifecycle
ADRWhat did we decide, and why?Preserves the decision and its context
Technical proposal (RFC, Design Doc)What are we proposing, and how could it work?Explores, discusses, and details a solution
DiagramHow is the system organized today?Represents structure, relations, or flows
RunbookHow do we operate or recover the system?Guides operational activities
PostmortemWhat happened in the incident, and how do we avoid a repeat?Records operational learning

An RFC can lead to an ADR. The RFC holds the broader discussion, the experiments, and the details of the proposal. The ADR records the outcome once the choice is settled, and points back to that material.

A diagram can also be updated as a consequence of the decision. Even so, it usually shows how the system ended up, not all the forces that led to that shape.

The UK Government Digital Service (GDS), which publishes the architecture guidance followed by the British public sector, stresses that an ADR is not a complete description of the architecture. It should live alongside diagrams, technical documentation, and other artifacts, without trying to replace them.

A simple way to hold on to that difference:

Architecture documentation shows the system. The ADR explains a choice that helped the system take that shape.


When a decision deserves an ADR

The biggest risk when adopting ADRs is documenting too much.

If every library upgrade, class rename, or implementation detail spawns a new document, the decision log quickly turns into noise.

The question is not:

Is this decision technical?

Almost all of them are.

The right question is:

Does this decision carry enough architectural weight that its context needs to survive?

A decision is architecturally significant when it touches structure, dependencies, interfaces, quality attributes, or construction techniques, and when backing out of it is expensive. It also counts when more than one plausible option exists and no ready-made guidance tells you which to pick.

In practice, an ADR tends to pay off when at least one of these conditions shows up:

  • It changes the structure of the system: modular monolith or microservices, synchronous or asynchronous communication, splitting a domain into another service, event-driven architecture.
  • It affects quality attributes: availability, latency, consistency, security, observability, disaster recovery, maintainability.
  • It creates a lasting contract: public API, event format, persisted schema, versioning strategy, protocol between teams.
  • It introduces a dependency that is hard to replace: database, broker, main framework, identity provider, proprietary cloud service.
  • It carries a high cost of reversal: data migration, protocol change, topology change, vendor contract, a model that forces rewriting several consumers.
  • It affects more than one team or system: corporate authentication, observability platform, event convention, shared deployment strategy, ownership of shared data.
  • It represents an important exception: sometimes the ADR records not the adoption of a standard, but the reason for not following it.

For example:

This service will not use the corporate broker because it has to run in a region where the broker is not available yet.

Without the ADR, the exception can look like an oversight.

Practical heuristic

A year from now, someone will probably ask “why did we do it this way?”, and will the answer depend on information that is not obvious in the code?

If so, there is probably an ADR here.

Some decisions usually do not justify an ADR:

  • renaming a class;
  • choosing between two equivalent ways of writing a loop;
  • bumping a patch dependency;
  • adding an endpoint that follows an established pattern;
  • reorganizing a private method;
  • solving a local, easily reversible detail.

The boundary will not be perfect.

That is fine.

A team is better off with a clear heuristic it adjusts through use than with an attempt to invent a universal definition of “architectural”.


What makes an ADR useful months later

Filling in a template does not guarantee a good ADR.

The test is different: six months from now, someone will open that file without having taken part in any of the conversations. That person has to come away knowing five things.

  1. What the problem was.
  2. What was in conflict.
  3. Why one option won.
  4. What you agreed to pay for it.
  5. How to notice the decision no longer holds.

The five original fields already cover that. adds drivers, options with pros and cons, confirmation, and review triggers. Microsoft also suggests recording the confidence level when the decision is made under uncertainty.

None of these fields is mandatory. The questions below help you decide which ones are worth it in your case.

PartQuestion answeredCommon mistake
Title and statusWhich decision is on the table, and what is its state?Vague titles like “Database”
ContextWhich problem, facts, and constraints demand a decision?Opening by defending the preferred solution
DriversWhich criteria carry the most weight?Listing generic qualities with no priority
OptionsWhich real alternatives were evaluated?Creating straw-man options
DecisionWhat will we do, and why did this option win?Using ambiguous language
ConsequencesWhat gets better, worse, or newly demanding?Recording benefits only
ConfirmationHow will we know the decision was implemented?Merging the document and forgetting the work
Review triggersWhich change of context calls for reconsidering?Treating the decision as permanent

Context comes before the solution

A bad context starts like this:

We need to adopt Kafka to improve scalability.

The solution arrived before the problem.

A better context tells what was happening:

Processing notifications synchronously increases checkout latency and propagates downtime of the communication service into order completion.

Now you can compare Kafka, RabbitMQ, SQS, or a jobs table in the database. Maybe none of them.

The context should record the facts, constraints, and assumptions available at that moment:

  • expected volume;
  • latency target;
  • deadline;
  • budget;
  • operational capacity;
  • security requirements;
  • ;
  • vendor limitations;
  • confidence level of the estimates.

That does not mean turning the ADR into a research report.

It means making clear under which conditions the decision was reasonable.

Drivers need priority

A driver is the criterion that pulled the decision: a requirement, a constraint, a quality goal, a deadline. Many alternatives look good when all of them carry the same weight.

So record the drivers that actually determined the choice:

  1. Reduce operational risk
  2. Ship to production in eight weeks
  3. Preserve ACID transactions
  4. Keep cost within the approved budget
  5. Avoid depending on technology the team does not know

That order tells a story.

Maybe the cheapest option was rejected because it raised operational risk. Maybe the most scalable solution lost because the current volume did not justify its complexity.

An unordered list tells none of those stories. “We evaluated cost, security, and performance” does not say which of the three won when they collided.

Alternatives have to be real

A straw-man option exists only to make the chosen one look better.

  • Option A: managed database
  • Option B: build our own distributed database

That is not a comparison.

An honest ADR presents two to four plausible options and includes, when it makes sense, keeping things as they are.

Every alternative deserves its best argument.

The question is not:

How do we prove our choice is right?

It is:

Which option best serves the drivers within the current constraints?

Consequences record the price paid

Every relevant architecture decision trades one property for another. Architecture is trade-off, and the ADR is where the price of that trade gets written down.

Choosing asynchronous communication can improve failure isolation, but it introduces eventual consistency, retries, idempotency, and extra observability.

Choosing a managed service can reduce operational work, but it raises cost and vendor dependency.

A weak consequence says:

The solution will increase complexity.

A useful consequence says:

Consumers will have to process messages idempotently, because delivery may happen more than once.

The more concrete the consequence, the more useful the document.

And if an ADR's consequences are all upside, the decision did not come out perfect. Nobody looked for the price.

Confidence and triggers keep decisions from being permanent

Not every decision is made with complete information.

Sometimes the team has to decide with:

  • estimates that have not been validated;
  • a limited benchmark;
  • a new vendor;
  • uncertain future load;
  • a deadline that rules out a larger proof of concept.

Hiding that uncertainty does not strengthen the ADR.

It does the opposite.

A decision can be accepted with low or moderate confidence, as long as the document records the assumptions and defines when to revisit it.

Review this decision when:

  • sustained volume exceeds the tested scenario;
  • monthly cost passes the approved limit;
  • the vendor drops support for the feature;
  • an incident shows the RTO is not being met;
  • a new region has to be supported.

A decision does not need to be permanent to be right.

It needs to fit the context you know and stay reviewable when that context changes.

Confirmation closes the loop

One of the most interesting MADR fields is Confirmation: how to verify that the implementation really honors the decision.

In a file, an accepted ADR and an ADR that turned into code look identical. Confirmation is what tells them apart.

It comes in two forms, and decisions that hold up tend to use both:

  • Proof it happened: the restore test that ran, the benchmark attached, the security review completed, the implementation Pull Request linked.
  • Guardrail that keeps it true: the ArchUnit rule that blocks the forbidden dependency, the alert that fires when the metric leaves its range, the published contract that breaks the build of whoever strays from it.

The first shows the decision reached the system. The second keeps it from being undone without anyone noticing, which is how an agreed architecture quietly turns into something else.


How ADRs fit into the team's flow

ADRs work best when they ride along with the flow the team already uses.

Creating another portal, another approval queue, and another mandatory meeting for every decision is the fastest way to kill the practice.

A simple flow can be:

  1. Identify the decision. The team runs into a choice with architectural impact and names an owner. One person drives the record, but does not decide alone.
  2. Write the proposal. The ADR starts with status Proposed, context, drivers, and alternatives. The document should begin while the decision is still open.
  3. Review with the people affected. Open a Pull Request and bring in the teams or specialists you need. The level of review follows the risk and the blast radius.
  4. Accept or reject. Record the outcome explicitly and merge the history. A rejected ADR deserves to exist too: the next person who considers the same idea already knows why it did not stick, without redoing the investigation from scratch.
  5. Implement and confirm. Link issues, Pull Requests, tests, and evidence. The work ends when the decision reaches the system, not when the Markdown is approved.

AWS adds a use the flow above does not cover: the records come back later, in code reviews and in related decisions. That is what takes the ADR out of the folder and puts it into everyday conversation.

Governance follows the impact

An ADR should not be a synonym for asking an architect's permission.

In practice, the scope of the review should follow the blast radius of the decision:

Local andreversibleTeam reviewContract changebetween servicesWhoever publishes it,whoever consumes itSecurityimpactWhoever ownssecurityCorporatedecisionCross-teamforum
Each row is an independent case: a decision fits one level, it does not pass through all of them.

The mistake lives at the extremes.

With no review, the ADR turns into a personal justification: someone decides alone and writes the document afterwards, just to have an excuse ready if anyone asks.

With central approval for everything, it turns into a bottleneck: every decision waits in line for someone who does not live the day to day of that system.

Balance does not come from a fixed approval ladder. It comes from talking early with the people who will feel the impact, and letting whoever is closest to the decision decide.

Where to keep them

For decisions local to one application, the simplest default is:

  • your-project/
    • docs/
      • adr/
        • README.md
        • 0000-adopt-adr.md
        • 0001-use-managed-postgresql.md
        • 0002-call-fulfillment-over-http.md
        • 0003-publish-events-with-outbox.md
        • template.md

The README.md in that folder is the index. It is what turns loose files into a decision log, making each decision's status visible without opening one record at a time.

docs/adr/README.md
# Architecture Decision Log

| ADR  | Decision                              | Status             |
| ---- | ------------------------------------- | ------------------ |
| 0000 | Adopt ADRs in the team                | Accepted           |
| 0001 | Use managed PostgreSQL                | Accepted           |
| 0002 | Call Fulfillment over synchronous HTTP| Superseded by 0003 |
| 0003 | Publish domain events with Outbox     | Accepted           |
docs/adr/README.md
# Architecture Decision Log

| ADR  | Decision                              | Status             |
| ---- | ------------------------------------- | ------------------ |
| 0000 | Adopt ADRs in the team                | Accepted           |
| 0001 | Use managed PostgreSQL                | Accepted           |
| 0002 | Call Fulfillment over synchronous HTTP| Superseded by 0003 |
| 0003 | Publish domain events with Outbox     | Accepted           |

Markdown in the same repository gives you:

  • proximity to the code;
  • change history;
  • review by Pull Request;
  • search;
  • links to commits and issues;
  • updates alongside the implementation.

Nygard, Google Cloud, and GDS all recommend keeping decisions close to the relevant code. For decisions that affect several systems, a central repository or index can complement the local records.

A practical split:

  • Decision local to one service: ADR in the service repository.
  • Cross-cutting platform decision: ADR in the central architecture repository.
  • Cross-cutting decision implemented across several services: central ADR, with two-way links to the repositories.

The source of truth has to be clear.

Copying the same ADR by hand into Git, a wiki, and a portal creates three competing versions. A better strategy is to keep one source of truth and publish its rendered view automatically wherever other people need to read it.

Location is not a detail. An action research study published in 2024 showed that decisions spread across components and repositories remain hard to find, even after ADRs are adopted.

The study was carried out at a single company, so its results should not be treated as a universal guarantee.

When a decision changes

Six months later, that decision may turn out to be wrong. That is normal, and it is no reason to erase it.

ADR-0002 recorded that Checkout would call Fulfillment over synchronous HTTP. Months later, a Fulfillment outage started taking Checkout down with it, and the team replaced the call with a PaymentConfirmed event published through a Transactional Outbox.

The way out is not editing ADR-0002 until it agrees with the new decision. It is writing ADR-0003 and pointing each one at the other:

0002-call-fulfillment-over-http.md
# ADR-0002: Call Fulfillment over synchronous HTTP

* Status: Superseded
* Superseded by: ADR-0003
0002-call-fulfillment-over-http.md
# ADR-0002: Call Fulfillment over synchronous HTTP

* Status: Superseded
* Superseded by: ADR-0003
0003-publish-events-with-outbox.md
# ADR-0003: Publish domain events with Outbox

* Status: Accepted
* Supersedes: ADR-0002
0003-publish-events-with-outbox.md
# ADR-0003: Publish domain events with Outbox

* Status: Accepted
* Supersedes: ADR-0002

Two lines in two files. Whoever arrives later sees the old decision, the new one, and the turn between them.

ADR-0002 does not become garbage when ADR-0003 shows up. Git history has the change, but nobody opens git log looking for a decision. ADR-0002 is where someone finds, without knowing to look for it, that synchronous HTTP was already tried there and what it cost to find out it did not fit.

Without it, a year from now someone proposes the direct call again, because it is simpler, and nobody can show that the team already paid to find out.

None of this forbids editing an ADR. A policy that tends to work:

  • Typo or broken link: fix it directly.
  • Ambiguous wording, with no change to the decision: clarify it through a Pull Request.
  • Consequence that showed up later: add a dated note.
  • Change of option or direction: a new ADR.
  • Decision that never left the page: mark it rejected or abandoned, keeping the reason.

What cannot happen is changing the decision in silence and destroying the timeline.

Supersede. Do not delete.


A lean template to get started

The best template is not the most complete one.

It is the one the team can fill in clearly and consult later.

The model below combines Nygard's minimal structure with a few useful MADR fields: drivers, options, confirmation, and review triggers. Every field beyond context, decision, and consequences can be simplified when it does not add value.

docs/adr/template.md
# ADR-NNNN: Title stating the decision

* Status: Proposed | Accepted | Rejected | Superseded
* Date: YYYY-MM-DD
* Owner: person or team
* Deciders: people or roles involved
* Supersedes: ADR-NNNN, if applicable
* Superseded by: ADR-NNNN, if applicable
* Confidence: high | medium | low, if relevant

## Context and problem

Which problem demands a decision?

Record the relevant facts, requirements, constraints, and assumptions.
Avoid defending a solution in this section.

## Decision drivers

* Priority criterion
* Relevant quality attribute
* Deadline, cost, or operational constraint
* Security or compliance requirement

## Considered options

### Option A

* Pros:
* Cons:
* Risks:

### Option B

* Pros:
* Cons:
* Risks:

### Keep things as they are

* Pros:
* Cons:
* Risks:

## Decision

We will adopt [option] because [rationale tied to the drivers].

We will not adopt [alternatives] because [relevant reasons].

If the whole proposal is rejected, record here why it did not move forward.

## Consequences

### Positive

* Expected gain

### Negative and accepted risks

* Cost, limitation, or new responsibility

### Mitigations

* Action used to reduce an accepted risk

## Confirmation

How will we verify the decision was implemented and works?

* Pull Requests:
* Tests:
* Metrics:
* Evidence:

## Review triggers

Which change of context calls for reconsidering this decision?

Prefer verifiable conditions over intentions: a numeric limit, an assumption
that fell, an incident.

## References

* Technical proposal (RFC, Design Doc)
* Issue
* Benchmark
* Diagram
* Postmortem
* Official documentation
docs/adr/template.md
# ADR-NNNN: Title stating the decision

* Status: Proposed | Accepted | Rejected | Superseded
* Date: YYYY-MM-DD
* Owner: person or team
* Deciders: people or roles involved
* Supersedes: ADR-NNNN, if applicable
* Superseded by: ADR-NNNN, if applicable
* Confidence: high | medium | low, if relevant

## Context and problem

Which problem demands a decision?

Record the relevant facts, requirements, constraints, and assumptions.
Avoid defending a solution in this section.

## Decision drivers

* Priority criterion
* Relevant quality attribute
* Deadline, cost, or operational constraint
* Security or compliance requirement

## Considered options

### Option A

* Pros:
* Cons:
* Risks:

### Option B

* Pros:
* Cons:
* Risks:

### Keep things as they are

* Pros:
* Cons:
* Risks:

## Decision

We will adopt [option] because [rationale tied to the drivers].

We will not adopt [alternatives] because [relevant reasons].

If the whole proposal is rejected, record here why it did not move forward.

## Consequences

### Positive

* Expected gain

### Negative and accepted risks

* Cost, limitation, or new responsibility

### Mitigations

* Action used to reduce an accepted risk

## Confirmation

How will we verify the decision was implemented and works?

* Pull Requests:
* Tests:
* Metrics:
* Evidence:

## Review triggers

Which change of context calls for reconsidering this decision?

Prefer verifiable conditions over intentions: a numeric limit, an assumption
that fell, an incident.

## References

* Technical proposal (RFC, Design Doc)
* Issue
* Benchmark
* Diagram
* Postmortem
* Official documentation

Do not make every field mandatory from day one.

A simple ADR can be forty lines long.

A more sensitive decision may call for a benchmark, a security analysis, or several teams taking part.

Size should follow importance and uncertainty, not documentation anxiety.

A compact example

The example below is fictional, but it shows how requirements, team capacity, and consequences can appear without turning the ADR into a report.

0001-use-managed-postgresql.md
# ADR-0001: Use managed PostgreSQL for Checkout

* Status: Accepted
* Date: 2026-08-20
* Owner: Checkout Tech Lead
* Deciders: Checkout Tech Lead, Platform SRE, and Security
* Confidence: medium

## Context and problem

Checkout stores orders, payments, and state transitions. Charging twice or
recording a payment with no order are mistakes we cannot fix afterwards,
so we need transactions and referential integrity guaranteed by the
database.

The product requires an RTO of one hour, an RPO of five minutes, and
encryption at rest.

We are six engineers and none of us has ever run a database in production.
The launch is planned for eight weeks from now.

The projection is 40k orders per day, peaks of 300 per minute during
campaigns, and under 500 GB of data within twelve months. Confidence in
that number is medium: it comes from the product estimate, not from
observed traffic.

## Decision drivers

* ACID transactions and referential integrity guaranteed by the database.
* Meet the one-hour RTO without depending on someone being awake.
* Ship to production in eight weeks.
* Backup, restore, and failover the team can actually test.
* Stay close to what the team already knows how to operate.

## Considered options

### Managed PostgreSQL 16

* Replica in another zone, continuous backup, and point-in-time recovery
handled by the provider.
* Costs roughly three times an equivalent virtual machine.
* No superuser, and a closed list of extensions.

### Self-managed PostgreSQL 16

* Full control, lower raw cost, no blocked extensions.
* Puts patching, replication, major version upgrades, and middle-of-the-
night on-call on the same six people who are building Checkout.

### Managed MySQL 8

* Meets Checkout's transactional requirements without reservations.
* Ruled out with no claim of PostgreSQL being technically superior: the
team's other two services already run PostgreSQL, and keeping two
dialects would double migrations, restore scripts, and what each person
needs to know at three in the morning.

### Managed CockroachDB

* Survives the loss of a zone with no manual failover, scales horizontally
without sharding in the application, and speaks the PostgreSQL protocol.
* On the availability driver, it is the strongest option on the list.
* Nobody on the team has operated it. Execution plans, behavior under
contention, and cost per transaction differ enough that the first lesson
would arrive during a payment incident.

## Decision

We will use managed PostgreSQL 16, with a replica in another zone and
point-in-time recovery.

We will not run the database ourselves, because six people with no
database on-call experience cannot safely hold a one-hour RTO.

We will not use MySQL, because the choice would make the team maintain two
relational dialects without giving Checkout anything in return.

We will not adopt CockroachDB now. It solves a scale problem we do not
have yet, and the cost of learning to operate it would land right in the
eight weeks before launch. If the load trigger fires, it is the first
alternative to revisit.

## Consequences

### Positive

* Continuous backup, replica, and failover stay with the provider.
* Constraints, foreign keys, and transactions live in the database, not in
the application.
* The team reuses the migrations, monitoring, and on-call knowledge from
the other two services.

### Negative and accepted risks

* Around 900 dollars a month against 300 for an equivalent virtual
machine.
* No superuser: an extension outside the provider's list becomes a support
request or a plan change.
* Scaling is vertical up to the largest instance type. After that, the way
out is partitioning or changing databases.

### Mitigations

* Do not use a provider-proprietary feature without recording the reason.
* Alert on connections, storage, commit latency, and replica lag.
* Rehearse the restore once a quarter and note the time in the runbook.

## Confirmation

* Instance and replica created with Terraform, with no console tweaks.
* Restore from a backup into a separate environment, within one hour,
before go-live.
* Forced failover in staging with Checkout under synthetic traffic.
* Capacity and availability alerts wired to the on-call channel.

## Review triggers

* Cost passes 1,500 dollars a month.
* A restore test or an incident blows the one-hour RTO.
* Sustained volume passes 300 orders per minute, or the database passes
500 GB.
* A required extension is not available on the provider.
0001-use-managed-postgresql.md
# ADR-0001: Use managed PostgreSQL for Checkout

* Status: Accepted
* Date: 2026-08-20
* Owner: Checkout Tech Lead
* Deciders: Checkout Tech Lead, Platform SRE, and Security
* Confidence: medium

## Context and problem

Checkout stores orders, payments, and state transitions. Charging twice or
recording a payment with no order are mistakes we cannot fix afterwards,
so we need transactions and referential integrity guaranteed by the
database.

The product requires an RTO of one hour, an RPO of five minutes, and
encryption at rest.

We are six engineers and none of us has ever run a database in production.
The launch is planned for eight weeks from now.

The projection is 40k orders per day, peaks of 300 per minute during
campaigns, and under 500 GB of data within twelve months. Confidence in
that number is medium: it comes from the product estimate, not from
observed traffic.

## Decision drivers

* ACID transactions and referential integrity guaranteed by the database.
* Meet the one-hour RTO without depending on someone being awake.
* Ship to production in eight weeks.
* Backup, restore, and failover the team can actually test.
* Stay close to what the team already knows how to operate.

## Considered options

### Managed PostgreSQL 16

* Replica in another zone, continuous backup, and point-in-time recovery
handled by the provider.
* Costs roughly three times an equivalent virtual machine.
* No superuser, and a closed list of extensions.

### Self-managed PostgreSQL 16

* Full control, lower raw cost, no blocked extensions.
* Puts patching, replication, major version upgrades, and middle-of-the-
night on-call on the same six people who are building Checkout.

### Managed MySQL 8

* Meets Checkout's transactional requirements without reservations.
* Ruled out with no claim of PostgreSQL being technically superior: the
team's other two services already run PostgreSQL, and keeping two
dialects would double migrations, restore scripts, and what each person
needs to know at three in the morning.

### Managed CockroachDB

* Survives the loss of a zone with no manual failover, scales horizontally
without sharding in the application, and speaks the PostgreSQL protocol.
* On the availability driver, it is the strongest option on the list.
* Nobody on the team has operated it. Execution plans, behavior under
contention, and cost per transaction differ enough that the first lesson
would arrive during a payment incident.

## Decision

We will use managed PostgreSQL 16, with a replica in another zone and
point-in-time recovery.

We will not run the database ourselves, because six people with no
database on-call experience cannot safely hold a one-hour RTO.

We will not use MySQL, because the choice would make the team maintain two
relational dialects without giving Checkout anything in return.

We will not adopt CockroachDB now. It solves a scale problem we do not
have yet, and the cost of learning to operate it would land right in the
eight weeks before launch. If the load trigger fires, it is the first
alternative to revisit.

## Consequences

### Positive

* Continuous backup, replica, and failover stay with the provider.
* Constraints, foreign keys, and transactions live in the database, not in
the application.
* The team reuses the migrations, monitoring, and on-call knowledge from
the other two services.

### Negative and accepted risks

* Around 900 dollars a month against 300 for an equivalent virtual
machine.
* No superuser: an extension outside the provider's list becomes a support
request or a plan change.
* Scaling is vertical up to the largest instance type. After that, the way
out is partitioning or changing databases.

### Mitigations

* Do not use a provider-proprietary feature without recording the reason.
* Alert on connections, storage, commit latency, and replica lag.
* Rehearse the restore once a quarter and note the time in the runbook.

## Confirmation

* Instance and replica created with Terraform, with no console tweaks.
* Restore from a backup into a separate environment, within one hour,
before go-live.
* Forced failover in staging with Checkout under synthetic traffic.
* Capacity and availability alerts wired to the on-call channel.

## Review triggers

* Cost passes 1,500 dollars a month.
* A restore test or an incident blows the one-hour RTO.
* Sustained volume passes 300 orders per minute, or the database passes
500 GB.
* A required extension is not available on the provider.

Notice what the ADR does not try to do: it does not document every table, does not describe migrations, does not teach how to configure the database, and does not replace the restore runbook.

And it does not define the one-hour RTO, it only obeys it.

What it records is why managed PostgreSQL was chosen and what has to remain true for the decision to stay valid.

A requirement enters the ADR as a constraint that squeezed the choice, not as a specification. If it changes, the change comes from the product, and the ADR's job is to trigger the review. The other side of that boundary is the subject of the article on requirements engineering.

Notice too the alternative that lost while holding the best technical argument.

CockroachDB served the availability driver better and still stayed out, because team capability is as real a constraint as latency or cost.


When ADRs turn into bureaucracy

An ADR is not automatically lightweight just because it lives in Markdown.

A bad process can turn even a fifty-line file into suffering.

Five anti-patterns show up often:

  • Documenting everything. The visible effect is a log that is hard to dig through. What kills the practice is the cost: when every local choice demands a document, writing ADRs becomes a tax, and the team stops writing even the ones that mattered.
  • Writing after everything is already decided. A retroactive document rationalizes the chosen option and forgets the doubts that actually existed. Start during the analysis. If the ADR really is historical, say so and state the uncertainties.
  • Mixing several decisions in one document. The epic ADR decides database, messaging, deployment, and authentication all at once. If those choices can change independently, they need separate, linked records.
  • Turning the ADR into central approval. If every decision waits for a board's monthly meeting, the team starts deciding outside the process and documenting only to tick a box.
  • Accepting without implementing. The ADR was approved, but it has no owner, no issue, no Pull Request, and no confirmation criterion. That is not an implemented architecture decision. It is an archived intention.

ADRs are not a magic fix

ADRs can improve context transfer and make discussions more explicit, but on their own they do not fix bad contracts, unclear ownership, or decisions spread across several systems.

The process only works when the records are easy to find, take part in the development flow, and come with clear guidance about what to document.

The 2024 study mentioned earlier also showed the other side of the coin:

Introducing ADRs improved documentation culture and knowledge transfer between teams.

Source: Ahmeti et al., ECSA 2024

It also found difficulty fitting ADRs into daily work. That is a good antidote to the hype: the format helps, but organization and culture still matter.


Start with the next decision

It is tempting to start by mapping everything decided over the last ten years.

Do not. You will spend weeks asking people who have already forgotten, or already left the company, and what comes out of it will sound true without being true.

Start small, and start with what is still open:

  1. Create the place. A docs/adr folder and a short README saying what goes in there and how to propose a record. The team needs to know where to write and where to look.
  2. Stay with the minimal template. Context, decision, and consequences do the job. Add a field when someone actually misses it, not because this article's model had it.
  3. Agree on what deserves a record. Use decisions from your own system as examples, not a generic yardstick. That conversation matters more than the file format.
  4. Write the next hard decision while it is happening. Open the Pull Request, bring in whoever will feel the impact, link the implementation. No chasing fifty old choices.
  5. Come back to it in a month. A field nobody filled in goes away, a stale index gets fixed. And if no ADR was consulted in that period, it is worth asking why before blaming the template.

The team can also write an ADR-0000 recording the decision to adopt ADRs: what goes in, where it lives, which statuses apply, and how one decision supersedes another. It has the charm of making the practice follow its own rule from day one.

Just do not let the ADR about ADRs turn into a three-month governance project.

Adopting the practice does not take a meeting or a planning quarter.

It takes a folder, a file, and the next decision that would make someone ask:

Why the hell did we do it this way?

That is when the practice starts paying for itself.


Before we close, five questions on what usually separates a useful ADR from a forgotten file:

Question1/5

Of the choices below, which one most likely deserves an ADR?


Conclusion

Code is excellent at showing how the system works.

It does not keep the alternatives that were discarded, nor the constraints of the time, nor the price the team agreed to pay.

And the rest of the toolkit does not cover that gap: a diagram shows components, a ticket tracks work, a Pull Request records a change.

The ADR keeps what is left over: the problem, the forces that were squeezing, the alternatives that were on the table, the choice, the price accepted, and what would make the team change its mind.

A good ADR is short, honest, and easy to find.

And it does not end in Markdown. It goes through the people who will feel the impact, points at the implementation, and gets confirmed in the real system. When the context changes, another one takes its place without erasing the previous one.

That is the core of it:

An ADR does not exist to prove the team was right. It exists to preserve why that choice made sense with the information available.

There is probably an open decision on your team right now, one of those that has come up in two conversations and still has not closed.

Before picking the technology, write down three things:

  1. which problem needs solving;
  2. which alternatives are genuinely viable;
  3. which negative consequence the team accepts taking on.

You will already have produced the most valuable part of an ADR.

On this page

Share

References

The original source of the ADR format and the structured template most used today.

Ednaldo Luiz
GitHubLinkedInPortfólio

Ednaldo Luiz

Software Architect and Engineer | Java & AI

Software Engineer focused on architecture and performance. I work with Java/Spring Boot, well-structured SQL, scalable services on AWS, and GenAI solutions with RAG (LangChain + vector databases). I value readable code and well-justified decisions.