Skip to content

    System Design

    Trade-Off Analysis: Choosing Between Backend Designs on Evidence

    Compare backend options with quality scenarios, constraint screening, weighted scores and a sensitivity check, then record the decision and when to revisit it.

    By Published 12 min read

    Share

    Standard lessonintermediateBackend System Design

    Before this: A Repeatable Approach

    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:

    1. Write qualities as scenarios with a measure, not as adjectives.
    2. Screen with constraints before scoring. A requirement that must hold is not a column to trade against cost.
    3. 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

    flowchart LR accTitle: The trade-off analysis method accDescr: Name the decision and drivers, write scenarios, list options, screen with constraints, score what remains, test sensitivity, and record the decision with a trigger. A dotted arrow returns to options if none survive the screen. D["1 Decision and drivers"] --> S["2 Scenarios with measures"] S --> O["3 Options, including do nothing"] O --> C["4 Screen with constraints"] C --> M["5 Score what remains"] M --> T["6 Test sensitivity"] T --> R["7 Record and set a trigger"] C -.->|"none survive"| O

    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.

    1. Decision and drivers. One sentence, plus the force that makes it a decision now.
    2. Scenarios with measures. Two to five, from the requirements.
    3. Options. Two to four, including the plainest one as the baseline.
    4. Screen. Drop anything that breaks a constraint.
    5. Score. Rate the survivors on each scenario, weight the scenarios, add up.
    6. Test sensitivity. Find what would flip the result.
    7. 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.

    Confirmation email: send inside the request or through a queueCheckout service to Mail provider (waits for it); Checkout service to Message queue (publish); Message queue to Email worker; Email worker to Mail provider (sends)Checkout serviceCheckoutserviceMail provider: Option A: called directlyMail providerOption A: calleddirectlyMessage queue: Option BMessage queueOption BEmail workerEmail workerMail providerMail providerwaits for itpublishsends

    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.

    stateDiagram-v2 accTitle: Life of a decision record accDescr: A record starts as proposed, becomes accepted, and is later marked superseded when a trigger fires and a new record replaces it, or deprecated when it no longer applies. [*] --> Proposed Proposed --> Accepted: agreed Accepted --> Superseded: trigger fires, new record Accepted --> Deprecated: no longer applies Superseded --> [*] Deprecated --> [*]

    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.

    sequenceDiagram accTitle: A cached authorization decision outlives a revocation accDescr: An admin disables a user. The gateway still holds a cached allow decision for that user, so the user's next request succeeds until the cache entry expires. participant A as Admin participant I as Identity service participant G as Gateway cache participant U as User G->>G: cache allow for user (time to live starts) A->>I: disable the user U->>G: request G-->>U: allowed from cache Note over G,U: access continues until the entry expires

    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

    Reservation path with Option A: one conditional update on the primaryShoppers to Checkout service (check out); Checkout service to PostgreSQL primary (reserve); PostgreSQL primary to Standby (replicate)ShoppersShoppersCheckout serviceCheckoutservicePostgreSQL primary: stock rowsPostgreSQLprimarystock rowsStandby: synchronousStandbysynchronouscheck outreservereplicate

    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.

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments