A short, immutable, numbered document. One decision per record. It is superseded rather than edited, so the sequence becomes a readable history of why the system is shaped the way it is.
The shape
# ADR-014: Use outbox pattern for order events
Status: Accepted
Date: 2026-03-11
Deciders: Order team, Platform architecture
Supersedes: —
## Context
Order state changes must reach the fulfilment and analytics consumers
exactly once. The order service writes to PostgreSQL and publishes to
Kafka. A dual write can commit the database transaction and then fail to
publish; we have seen this three times in staging under broker failover.
## Decision
Write events to an `outbox` table inside the same transaction as the state
change. A separate relay reads the outbox via CDC and publishes to Kafka.
## Options considered
1. Dual write with retry — rejected: no atomicity, retries can duplicate
without solving loss.
2. Event sourcing the order aggregate — rejected: correct, but a rewrite of
the order service that this decision does not have budget for.
3. Transactional outbox — chosen.
4. Kafka transactions across DB and broker — rejected: PostgreSQL is not an
XA participant in our managed configuration.
## Consequences
+ Atomic with the state change; no lost events.
+ Relay failure delays delivery but does not lose it.
- Adds a CDC connector to operate and monitor.
- Consumers must be idempotent; at-least-once, not exactly-once.
- Outbox table needs a retention job or it grows without bound.
## Compliance
Fitness function: integration test asserts no event is published without a
committed row. Alert on relay lag > 60s.
When you produce it
At the moment of decision, not at the end of the project. An ADR written retrospectively records the justification rather than the reasoning, and the difference is obvious to anyone reading it later.
Write one when the decision is expensive to reverse, when it constrains other teams, or when you can already imagine somebody asking "why on earth did they do that?" in two years.
Who reads it
The next architect. That is genuinely the primary audience, and it is worth writing for them explicitly. Also review boards, who should be reviewing the record rather than requesting a deck.
What good looks like
- Rejected options are named with their reason. A record with one option is a press release.
- Consequences include the negative ones. If there are no downsides listed, the record is not honest and no one will trust the next one.
- Status is real and maintained: proposed, accepted, deprecated, superseded.
- It lives in the repository next to the code it constrains.
- One page. Two at the absolute outside.
Common mistakes
- Writing them all at the end of the project. The most common failure.
- Editing an accepted record. Supersede it; the history is the value.
- Recording implementation detail. "We used a factory here" is a code comment, not an architecture decision.
- No consequences section, which is the only part anyone reads later.