The problem
An architecture recommendation often ends with a list of benefits. It explains why the chosen option is the right one, without saying what it costs. It then looks one-sided, even when the comparison was done, and the reader goes looking for the missing costs alone.
Take an online shop split into services. The recommendation: publish the order events through an outbox table, written in the same transaction as the order. It solves lost events, but it has a price that the text does not mention.
That price is discovered later, in one of these three ways.
The cost arrives as a surprise. The outbox
relay publishes an event again after a stop, and the
stock service reserves the goods of the same
order twice. The duplicate was a known property of the
choice, and it shows up as an incident.
An accepted cost can no longer be told from an
oversight.
The shipping service receives orders with a
slight delay, the time it takes the relay to come round.
Nothing says whether this delay was weighed when choosing:
every delay then calls the whole choice into question.
Nothing says when to revisit the choice. No threshold says at what delay the outbox stops being suitable. The choice is then reviewed too early, on a hunch, or too late, when the delay already bothers the shop's customers.
The options
No trade-off section
- What it solves: the recommendation stays short and reads quickly.
- What it costs: the costs are discovered in production. The reader has to work them out alone, and the recommendation loses its credibility at the first cost discovered.
A generic list of risks
The recommendation ends with a "Risks" section: "increased complexity", "learning curve", "impact on performance".
- What it solves: it shows that risks were considered.
- What it costs: these lines fit any choice. None of them can be checked in the system, and none says when to revisit the decision.
A list of risks belongs at the scale of a whole system: arc42 devotes a section to it, with the risks ordered by priority. It complements the trade-offs of a decision, it does not replace them.
Written trade-offs, each with its review signal
- What it solves: each cost is named, assigned and bounded in time. A signal says when the choice must be reviewed.
- What it costs: time to analyse and to write. Measurable signals require a query, a dashboard or an alert.
Our recommendation
Every recommendation writes down its trade-offs: what we lose, who bears it, until when, what limits it, and the review signal.
In our method, the "trade-offs we
accept" section follows the one-sentence recommendation and
comes before the implementation plan. This section changes
the nature of the cost. The duplicate received by
stock is no longer an incident: it is expected
behaviour, contained by an
idempotent consumer.
The section also makes the recommendation credible. An option without a downside does not exist: a text that shows none suggests the comparison was not done. Recognised architecture decision formats provide for it, under different names:
- Michael Nygard's format ends each decision with its consequences, positive, negative and neutral;
- the MADR template lists the consequences as "Good, because" and "Bad, because", and compares the options with their pros and cons;
- Olaf Zimmermann's Y-statement holds a decision in one sentence, which ends with "accepting that": the price accepted;
- the SEI's ATAM looks for tradeoff points: where a decision improves one quality of the system at the expense of another.
Finally, the signal makes the decision revisable. Martin Fowler advises noting in the decision the changes of context that should trigger its re-evaluation. Without a signal, a trade-off has no end date.
The articles in this series apply the rule. Our article on the RabbitMQ topology accepts a queue that mixes the events of one service. It also names the signal that should get this queue split: a measured delay on critical messages.
The trade-offs we accept
This practice has its price too.
One more section to write and to read. The recommendation gets longer. We limit it to the trade-offs that change the behaviour of the system or its operating cost, not everything that could happen.
The recommendation shows its weaknesses. The costs appear on the first reading, well before the benefits, which only show in production. A written cost can make another option preferable: better to know it before implementation than after.
Signals to instrument. A measurable signal requires a query, a dashboard or an alert. Others are facts to observe, such as a consumer unable to deduplicate: they only help if the trade-offs are reread with every new need.
The signal that should trigger a review of this practice: trade-offs copied from one recommendation to the next, unrelated to the choice. Or signals that nothing ever measures. The section has then become a ritual: shorten it, or instrument its signals.
Implementation
1. Start from the rejected options
The costs of the chosen option show in the comparison with the others. What a rejected option did better is often what we lose. Publishing right after the commit sends the event at once: the outbox loses that immediacy.
For each rejected option, ask one question: what did it bring that the recommendation does not? Each answer is a candidate trade-off. Keep only those that change the behaviour of the system or its operation.
2. Name what we lose
What we lose is an observable property of the system: a guarantee, a delay, a capability, operational simplicity. "Some complexity" is not one. "One more table and one more process to monitor" is one.
The test: can the loss be observed in the system or in the code? If not, the wording is still too vague.
3. Say who bears it, and until when
A cost is always borne somewhere: a service, an activity (development, operations, support) or the shop's users. Name that bearer. A cost without a designated bearer is only dealt with once it is discovered in production.
"Until when" takes a condition rather than a date: "as long as the relay polls the table", or "until v2 of the contract". A permanent cost is declared as such: it will not go away with time.
4. Write down what limits the cost
An accepted trade-off is not a cost endured: write down what contains it. For duplicates, idempotent consumers; for latency, an alert on the age of the oldest unpublished row. If nothing limits it, write that down too.
5. Set the review signal
The signal says when the choice must be reviewed. When possible, it is measurable: a measure, its source, and a threshold written in the recommendation. The threshold comes from the business need, not from a default value.
For the outbox latency, the measure is read from the table itself:
-- Publication delay over the last hour (outbox table of the order service)
SELECT
max(published_at - occurred_at) AS max_delay,
percentile_cont(0.95) WITHIN GROUP (ORDER BY published_at - occurred_at) AS p95_delay
FROM outbox
WHERE published_at IS NOT NULL
AND occurred_at > now() - interval '1 hour';
Unpublished rows are not counted: a stopped relay is caught by the alert on the oldest of them.
Otherwise, the signal describes a fact: a consumer whose effect leaves its database (e-mail, HTTP call), or a recurring need to replay. This kind of signal is checked with every new need, by rereading the trade-offs.
A signal triggers a review, not an automatic change. The review ends with an explicit request for a decision: confirm the choice, adjust it or replace it.
The MADR template provides a "Confirmation" section: how to check that the decision is actually applied. The review signal asks the next question: does the decision still fit?
6. A template to fill in
Trade-off: <short title>
What we lose: <an observable property of the system>
Who bears it: <a service, an activity or the users>
Until when: <a condition, or "permanent">
What limits it: <the safeguard in place, or "nothing">
Review signal: <a measure and its threshold, or an observable fact>
Where to read it: <the query, the dashboard or the alert>
7. Three examples
The first two come from an outbox recommendation to publish
order.order.placed.v1. The third comes from an
API built with API Platform, where the resources are DTOs
separate from the Doctrine entities.
Trade-off: possible duplicates
What we lose: the single delivery of each event
Who bears it: the consuming services, stock and shipping
Until when: permanent, the database and the broker share no transaction
What limits it: a table of processed events in each consumer
Review signal: an effect applied twice, or a consumer
whose effect leaves its database without an idempotency key
Where to read it: a query looking for two reservations per order line
Trade-off: publication latency
What we lose: the immediate publication of the event
Who bears it: shipping and the order tracking screen
Until when: as long as the relay polls the table
What limits it: an alert on the oldest unpublished row
Review signal: the delay between occurred_at and published_at
exceeds the threshold written in the recommendation
Where to read it: the delay query on the outbox table
Trade-off: more classes per resource
What we lose: the conciseness of an entity exposed directly
Who bears it: the development of each new API resource
Until when: as long as the mappers are written by hand
What limits it: the same structure from one resource to the next
Review signal: mappers that copy field by field
resources almost identical to their entities
Where to read it: the mappers' code, with each new resource
The third signal cannot be measured: it is observed when reading the code. It is still enough to say when automatic mapping becomes an option again.
8. Review without rewriting
When a signal fires, we write a new recommendation that refers to the old one. The old one stays readable, with the status "superseded". Nygard and Fowler describe this status: we know which decision applied, and for how long.
An index of the recommendations, with their status, shows which ones are in force. The trade-offs of each one remain available there, with their signals.
Checklist
- A "trade-offs we accept" section in every recommendation.
- Each rejected option reread: what it did better appears among the trade-offs.
- Each trade-off names an observable property, not "some complexity".
- Each trade-off says who bears it: a service, an activity or the users.
- Each trade-off says until when, or declares itself permanent.
- What limits each cost is written down, or its absence is stated.
- One review signal per trade-off, with a written threshold when it is measurable.
- Each measurable signal has its query, its dashboard or its alert.
- A review produces a new recommendation, which supersedes the old one without rewriting it.
Sources
- Michael Nygard, Documenting Architecture Decisions (Cognitect, 2011): the context, decision, status, consequences format, and the "superseded" status.
- Architectural Decision Records: definitions and formats for recording architecture decisions.
- MADR, version 4.0.0: Consequences, Confirmation and Pros and Cons of the Options sections.
- arc42, section 9, Architecture Decisions: list all the consequences, document the rejected options.
- arc42, section 11, Risks and Technical Debt: a list of risks ordered by priority, with the measures that limit them.
- Olaf Zimmermann, Design Practice Repository, Y-Statement: a decision in one sentence, ending with the price accepted.
- Martin Fowler, Architecture Decision Record (2026): supersede a decision rather than modify it, and note what should trigger its re-evaluation.
- SEI, Architecture Tradeoff Analysis Method: risks, sensitivity points and tradeoff points.
-
PostgreSQL,
aggregate functions:
percentile_contover an interval.