What you'll learn: By the end of this lesson you will be able to turn vague goals into measurable quality scenarios, screen options with constraints before scoring them, compute a weighted score and test how far a weight must move before the ranking flips, tell a sensitivity point from a trade-off point, count cost as a quality, and write a short decision record with a trigger for revisiting it.
Choosing a flat means trading rent, commute, size and noise. Nobody finds the cheapest, biggest, quietest flat next to the office. A sensible buyer first says which limits are fixed (rent above a ceiling is out), then compares what is left, then notices which preference decides the choice, and writes down why, so that in two years the reason is still known.
Backend design is the same, with different columns: latency, throughput, availability, consistency, durability, cost, security, operability and simplicity. Improving one often costs another, so "which option is best?" has no answer until you say best for which scenario, within which limits.
The other side of this topic: the front end has the same method with other criteria (speed of the first view, layout stability, accessibility); see trade-off analysis for front-end decisions.
The Java program ran as a single-file program on JDK 26.0.1 on 2026-11-08. Scores, weights and targets are illustrative.
The core idea
A trade-off exists when improving one quality costs another. Trade-off analysis is a short, repeatable way to compare options against criteria taken from requirements, with evidence, and to write the result down with a condition for looking again. Three rules carry most of the value:
- Write qualities as scenarios with a measure, not as adjectives.
- Screen with constraints before scoring. A requirement that must hold is not a column to trade against cost.
- Test the ranking, then record it. Ask how far an input must move to change the answer, and write down the trigger.
Three examples run through the lesson: a minimal example (a confirmation email), a realistic example (reserving stock on ShopStream) and a failure example (a cache that traded away revocation).
Start from quality scenarios
"Fast", "reliable" and "scalable" cannot be compared. A scenario names a situation and a measure: "When 3,000 checkouts arrive in one minute, the reserve step answers within 300 ms for 99 percent of requests" (illustrative numbers). The words come from earlier lessons: latency and throughput, availability and durability, consistency in CAP and PACELC.
The Software Engineering Institute's Architecture Tradeoff Analysis Method (ATAM) works this way at full scale. It elicits quality attribute scenarios, arranges them in a utility tree, prioritizes them, analyses each architectural approach against the high-priority ones, and reports risks, sensitivity points and trade-off points. The SEI says an evaluation typically takes three to four days with a trained team. You need an hour, so this lesson keeps the shape and drops the ceremony.
The method
Figure 1. The method in seven steps. How to read this: the work runs left to right. The dotted arrow is the useful failure: if every option breaks a constraint, add or reshape options instead of weakening the requirement.
- Decision and drivers. One sentence, plus the force that makes it a decision now.
- Scenarios with measures. Two to five, from the requirements.
- Options. Two to four, including the plainest one as the baseline.
- Screen. Drop anything that breaks a constraint.
- Score. Rate the survivors on each scenario, weight the scenarios, add up.
- Test sensitivity. Find what would flip the result.
- Record. Short, with consequences and a trigger.
A minimal example: the confirmation email
After checkout, ShopStream sends a confirmation email. Option A calls the mail provider inside the checkout request. Option B puts a message on a queue and a worker sends it.
Figure 2. Two ways to send one email. How to read this: in Option A the checkout waits for the provider. In Option B it hands off and returns, and the worker does the slow step later.
| Scenario | A: direct call | B: queue and worker |
|---|---|---|
| Checkout p99 when the provider is slow | Includes the provider's latency; threads stay held (see Little's law) | Unaffected |
| Confirmation survives a restart | Lost unless retried by hand | Stays on the queue |
| Simplicity to run | One moving part | Queue, worker, monitoring |
| Duplicate emails | Rare | Possible, because a queue can deliver a message more than once, so the worker must tolerate repeats |
Option B wins on the first two and loses on the last two. Whether that is right depends on which scenario matters most, which is the weighting step.
Constraints first, then scores
A constraint must hold; an objective is better the more of it you get. Screening turns constraints into pass or fail before any arithmetic. Scoring after that compares what is left on objectives.
Realistic example: reserving stock on ShopStream. When a shopper checks out, the system reserves units of each product. One constraint: a confirmed reservation must survive the loss of any single node, and the system must never confirm more units than exist. Three options:
- A. One conditional update on the PostgreSQL primary (the approach in the data model lesson).
- B. Publish a reservation request to a queue; a worker applies it; the shopper is told the result a moment later.
- C. Keep counters in memory on the app servers and write them back to the database later.
C fails the screen: a crash before write-back loses confirmed reservations. A and B remain. Score them from 1 (poor) to 5 (good) on four scenarios, with weights 1 to 3. Scores are judgements and are illustrative.
| Scenario | Weight | A | B |
|---|---|---|---|
| Checkout latency at peak | 3 | 4 | 3 |
| Simplicity to run | 2 | 5 | 2 |
| Throughput on one hot product | 2 | 2 | 4 |
| Cost | 1 | 4 | 3 |
A scores 3×4 + 2×5 + 2×2 + 1×4 = 30. B scores 3×3 + 2×2 + 2×4 + 1×3 = 24. A wins. Before trusting that, ask how far each weight would have to move to flip the result. With a weight w for criterion i, the score gap changes by (A_i − B_i) per point, so the tie is at w = w_i − gap / (A_i − B_i).
public class TradeOff {
public static void main(String[] args) {
String[] criteria = {"checkout latency", "simplicity to run", "hot-item throughput", "cost"};
int[] weight = {3, 2, 2, 1};
int[] a = {4, 5, 2, 4}; // A: conditional update on the primary
int[] b = {3, 2, 4, 3}; // B: asynchronous reservation through a queue
int totalA = 0, totalB = 0;
for (int i = 0; i < criteria.length; i++) {
totalA += weight[i] * a[i];
totalB += weight[i] * b[i];
}
System.out.println("A = " + totalA + ", B = " + totalB);
for (int i = 0; i < criteria.length; i++) {
int slope = a[i] - b[i]; // how much each weight point moves A - B
if (slope == 0) continue;
double flipAt = weight[i] - (double) (totalA - totalB) / slope;
System.out.printf("%-20s weight %d -> A and B tie at weight %.1f%n", criteria[i], weight[i], flipAt);
}
}
}
Save as TradeOff.java and run java TradeOff.java. Output from a real run (JDK 26.0.1, 2026-11-08):
A = 30, B = 24
checkout latency weight 3 -> A and B tie at weight -3.0
simplicity to run weight 2 -> A and B tie at weight 0.0
hot-item throughput weight 2 -> A and B tie at weight 5.0
cost weight 1 -> A and B tie at weight -5.0
A negative tie weight means no weight from zero up could flip it. Only one input matters: if the team's weight on hot-item throughput grows from 2 to above 5, B overtakes A. The scores are ordinal guesses, so the lesson of the exercise is not "A scores 30". It is "the answer depends on how much we care about one hot product", which is a conversation worth having, and a number to watch.
Sensitivity points and trade-off points
ATAM's outputs include sensitivity points and trade-off points. The SEI page names them without defining them, so these are working definitions. A sensitivity point is a decision or parameter where a small change moves one quality a lot: here, the hot-item weight, or in a service, the thread pool size. A trade-off point is a decision that moves two qualities in opposite directions: the cache TTL (freshness against load), the number of synchronous replicas (durability against latency). Mark both in a review. They are where the design is fragile and where monitoring belongs.
Cost is a quality
Cost is a column like any other. The Google SRE book notes that an incremental improvement in reliability may cost 100 times more than the previous increment, that the cost has two parts (redundant resources and the engineers' time that could have gone to features), and that a user on a 99 percent reliable phone cannot tell 99.99 from 99.999 percent. Its answer is a target that balances, not a maximum, with an error budget that makes the trade explicit between reliability and feature speed. The Azure Well-Architected guidance adds the warning that a choice focused only on lower spending can undermine the workload's business goals. Put the money in the table, next to the scenarios it buys.
Recording the decision
Michael Nygard's short format for architecture decision records has five parts: a title (a short noun phrase), a status (proposed, accepted, deprecated or superseded), the context (the forces at play, in tension, in neutral language), the decision (in full sentences, "We will ..."), and the consequences (all of them, not only the good ones). He argues for one or two pages, because large documents go stale. Add one line the format does not require: the trigger, a measurable condition that reopens the decision.
Figure 3. A record has a life. How to read this: acceptance is not the end. A trigger that fires moves the old record to superseded and starts a new one, so the history of reasons survives.
Failure example: the optimisation that traded away revocation
A team caches authorization decisions in a gateway to cut latency on every call. It improves latency and throughput and passes review. Nobody wrote a scenario for revocation.
Figure 4. How a performance gain becomes a security gap. How to read this: the cache answers from a copy. The longer the copy lives and the weaker its invalidation, the longer a revoked user keeps access.
The Azure Well-Architected performance trade-offs page describes this pattern: cached authentication tokens or authorization decisions can permit access after a user is disabled or a role is revoked, and the window between revocation and cache expiry is a window of unauthorized access. This is a trade-off point nobody marked. The fix starts in the scenarios: "after a user is disabled, no request succeeds after N seconds" (a number set with security), then a short TTL or an invalidation event on revocation, then a test that disables a user and checks the next call.
In practice: stock reservation before a promotion
This is an educational scenario on ShopStream. All figures are illustrative.
This is a realistic scenario, not a case history.
The situation
A promotion discounts a handful of products at noon. Stock reservation must keep its 300 ms p99 at the peak of about 58 checkouts per second from the course brief, and must never oversell. The team must pick A or B above.
Starting architecture
Figure 5. The design under review. How to read this: every reservation is one statement on the single primary, so the database is the authority that stops overselling.
Options considered
| Option | Passes the screen? | Strength | Weakness |
|---|---|---|---|
| A. Conditional update on the primary | Yes | Simple; the database enforces the limit | One hot product means many updates to one row |
| B. Queue, worker, delayed answer | Yes | Absorbs bursts; more throughput on one product | More parts; the shopper waits for the answer |
| C. In-memory counters, write back later | No | Fast | A crash loses confirmed reservations |
The decision
Option A (score 30 against 24), with the sensitivity result in the record. Written as a short record:
Decision: We will reserve stock with one conditional update on the primary. Context: Constraint: never oversell and survive loss of one node. Score A 30, B 24; the ranking flips only if the weight on hot-item throughput rises above 5. Consequences: Simple to run; one hot product serializes its updates. Trigger: reserve p99 above 300 ms on any single product in the promotion load test, or the team's weight on hot items rising above 5.
Rollout and verification
Load-test the promotion shape: most reservations on a few products. Measure p99 of the reserve step per product, lock wait time, errors and rejections. The PostgreSQL manual says row-level locks block other writers to the same row until the transaction ends, so concurrent reservations for one product queue behind each other; the test shows whether that queue stays short enough.
The 2 a.m. failure
At 12:02 the page fires: reserve p99 on one product is far above target while others are fine. The trigger in the record has fired. Checks: lock waits on that product's row, in-flight reservations (rate times latency), and the share of traffic on the product. Mitigation: shed with a clear "try again" for that product only. Lasting fix: a new record, superseding the old, that evaluates Option B or splitting that product's stock across several rows for hot products only, scored with the same method.
Architect's notes
- Write the scenario before the option. Options argue for themselves; scenarios do not.
- Mark the sensitivity and trade-off points and put a metric on each.
- Keep the constraint list short and owned by someone who can change it.
- Spend analysis in proportion to the cost of reversing. A queue behind an interface is cheaper to undo than a public API or a data format.
- Record the loser too. The next team will propose it again.
Common mistakes
- Scoring before screening, so a high total hides a broken constraint.
- Choosing weights after choosing the winner.
- Treating scores as measurements.
- Optimising one quality and never writing the scenario for the one it harms.
- Recording the decision without a trigger.
Key takeaways
- A trade-off means improving one quality costs another; analysis compares options against scenarios from requirements.
- Write qualities as situations with measures.
- Screen with constraints first; score only what survives.
- Test how far a weight must move to flip the ranking; the answer is a number to watch.
- Sensitivity points are where one change moves one quality; trade-off points move two in opposite directions.
- Cost is a column, and reliability gets expensive in steps.
- A short record with consequences and a trigger keeps the reason alive.
Practice exercises
Exercise 1 (30 min). Rewrite "the API must be fast and reliable" as three scenarios with measures.
Exercise 2 (30 min). Change the lesson's scores so that B wins. Which single score is the smallest change that does it?
Exercise 3 (45 min). Run TradeOff.java with a fifth criterion, "operational skill needed", weight 2, A 4, B 2. What happens to the tie weights?
Exercise 4 (1 hour). Pick a cache you know. List its sensitivity point and its trade-off point, and write the scenario that would have caught the failure example.
Exercise 5 (1 hour). Write a record in Nygard's format for a decision you made recently, with a trigger.
Related reading
- Latency vs Throughput: the first two columns of most tables.
- Availability, Reliability and Durability: writing scenarios for availability.
- CAP theorem and PACELC: a trade-off point at the heart of data design.
- How to approach a design problem: where this method sits in a full design.
- Capacity and cost: cost as a quality.
- ShopStream data model: the conditional update used here.
Official documentation
- Software Engineering Institute, Architecture Tradeoff Analysis Method Collection.
- Nygard, Documenting Architecture Decisions.
- Google, Site Reliability Engineering: Embracing Risk.
- Microsoft, Performance Efficiency tradeoffs and Cost Optimization tradeoffs (Azure Well-Architected Framework).
- PostgreSQL 18 manual, Explicit Locking: row-level locks.