Skip to content

    System Design

    QuickCart Stage 1: Requirements, Estimates and the First Design

    Turn a one-paragraph product idea into requirements, back-of-envelope numbers, SLOs and a first two-tier design for QuickCart, an online grocery marketplace.

    By Published 24 min read

    Share

    What you'll learn: By the end of this stage you will be able to explain client-server and N-tier architecture, split a product idea into functional and non-functional requirements, estimate traffic, storage and bandwidth with exact arithmetic, tell latency from throughput and availability from reliability and durability, write SLIs, SLOs and an error budget, and use an 8-step method to produce QuickCart's first design: one application server and one database.

    Before a builder draws a house, they ask how many people will live there, how cold the winters get and what the budget is. A plan drawn without those answers may be beautiful and still wrong.

    This course follows one system, QuickCart, through eleven stages. QuickCart is an online grocery marketplace in one city region. Shoppers browse and search products from nearby stores, fill a cart, check out, pay and track a courier delivering the order. Stores manage stock and prices and accept orders. Couriers are assigned deliveries. It is an educational reference design: it describes no real company, and every number in it is an illustrative assumption chosen so the arithmetic is easy to check.

    Stage 1 produces the first design and fixes the shared numbers every later stage reuses.

    Where QuickCart is now

    QuickCart does not exist yet: there is an idea, a launch date and a small team. The pain is ambiguity. Nobody has written down what "working" means, how big the system must become or how much failure is acceptable.

    The targets the course builds toward (all illustrative):

    Quantity Value at the end of the project
    Monthly active users 10 million
    Daily active users 2 million
    Orders per day 500,000
    Products per region 100,000
    Stores 5,000
    Page views per daily user per day 20

    Launch is far smaller: 10,000 monthly active users at Stage 1, then 100,000 (Stage 2), 1,000,000 (Stage 3) and 5,000,000 growing to 10,000,000 (Stages 4 to 10). That is one-thousandth of the final scale, so the design should look modest.

    Figure 1. The problem: who and what QuickCart must serve, before any design exists

    QuickCart context: three kinds of client and two outside providers, system not yet designedShoppers to QuickCart (browse, order); Stores to QuickCart (stock, accept); Couriers to QuickCart (delivery status); QuickCart to Payment provider (charge card); QuickCart to Email, SMS and push (notify)Shoppers: web and mobile appShoppersweb and mobileappStores: tablet appStorestablet appCouriers: phone appCouriersphone appQuickCart: to be designedQuickCartto be designedPayment providerPaymentproviderEmail, SMS and pushEmail, SMS andpushbrowse, orderstock, acceptdelivery statuscharge cardnotify

    How to read this: the left boxes are QuickCart's users, the muted boxes are companies it depends on but does not control, and the accent box is the thing to design. Every requirement below comes from one of these arrows.

    The core idea

    A design is a chain of decisions, and the first link is not "which database" but "what problem, for whom, at what size, and how well". This course uses an 8-step method at every stage (general form: System Design: A Repeatable Approach). Stage 1 meets all eight at once.

    Figure 2. The 8-step method used across the course

    flowchart LR accTitle: The 8-step system design method accDescr: Eight steps in order, from requirements to trade-offs, with a loop back from trade-offs to requirements. S1["1. Requirements"] --> S2["2. Estimates"] S2 --> S3["3. API"] S3 --> S4["4. Data model"] S4 --> S5["5. High-level design"] S5 --> S6["6. Deep dives"] S6 --> S7["7. Failures"] S7 --> S8["8. Trade-offs"] S8 -.->|"new facts"| S1

    How to read this: work left to right once, then loop. A trade-off found in step 8 often sends you back to change a requirement in step 1.

    Step QuickCart's output in Stage 1
    1. Requirements Functional list, quality targets, exclusions
    2. Estimates Requests per second, storage, bandwidth
    3. API An endpoint list
    4. Data model A handful of tables
    5. High-level design One application server, one database
    6. Deep dives The checkout transaction
    7. Failures A list of single points of failure
    8. Trade-offs A decision record

    The shape of a web system

    Client-server architecture

    A client starts a conversation by sending a request. A server waits for requests and answers them. The words name roles, not machines: RFC 9110 says they "refer only to the roles that these programs perform for a particular connection". A phone is the client when it asks QuickCart for products, and QuickCart's server is a client when it asks the payment provider to charge a card.

    HTTP is a request/response protocol and is stateless: each request's meaning can be understood in isolation (RFC 9110, section 3.3). If the server must know who you are, the request carries that, for example a session cookie or token. Stateless servers are easy to copy, which Stage 2 relies on.

    Figure 3. One browse request, from client to database and back

    sequenceDiagram accTitle: A shopper browses products accDescr: The shopper's app sends a request to the application server, which queries the database and returns the product list. participant C as Shopper app participant A as App server participant D as Database C->>A: GET /products?store=17&q=milk A->>D: SELECT products and prices for store 17 D-->>A: rows A-->>C: 200 OK with JSON product list Note over C,A: The client started it. The server cannot call the client unprompted.

    How to read this: time runs downward. The client always initiates; the server cannot push a message on its own, so order tracking needs repeated asking or a long-lived connection, which a later stage covers.

    For QuickCart the server owns the rules (a user can modify their app, so prices, stock checks and payment amounts are decided server-side), and one API serves browser, phone and tablet. In peer-to-peer designs every participant is both; a marketplace needs one authoritative place for stock and orders, so client-server fits.

    N-tier architecture

    A layer is a logical slice of code with one responsibility (HTTP handlers, business rules, data access). A tier is a physical slice: machines or processes that run separately and talk over a network. The Azure Architecture Center describes N-tier as logical layers and physical tiers, and notes several layers can share one tier. Layers organise code; tiers organise deployment, scaling and security.

    Counting conventions differ. This course counts the server-side tiers you deploy and operate; browsers and phone apps are clients.

    Pattern What runs where Fits when Strain point
    Client straight to database (classic two-tier) Logic runs on each user's machine, talking SQL to a shared database A few trusted users, an internal tool Credentials in every client; rules in many copies
    Two tiers (QuickCart Stage 1) One application tier (web handling plus logic) and a data tier Small team, modest traffic, a first launch One machine of each is a single point of failure
    Three tiers Web tier (HTTPS, static files, forwarding), application tier, data tier Web and logic layers must scale or be secured apart More machines and network hops
    N tiers Three tiers plus a cache, search or queue tier Parts have different scaling or failure needs A request crosses many systems; harder to test and monitor

    Figure 4. A three-tier layout, for comparison with QuickCart's two tiers

    Three-tier layout: web tier, application tier, data tierBrowser or app to Web server (HTTPS); Web server to Application server (forward); Application server to Database (SQL)WEB TIERAPPLICATION TIERDATA TIERBrowser or appBrowser or appWeb server: TLS, static filesWeb serverTLS, static filesApplication server: business logicApplicationserverbusiness logicDatabaseDatabaseHTTPSforwardSQL

    How to read this: each shaded zone is one tier, and the stacked box shows the application tier running several copies. Each tier can be scaled, secured and replaced on its own. QuickCart reaches this shape in Stage 2.

    Add a tier for a named problem, not because the textbook shows three: a middle tier that only passes calls along adds latency and a failure point.

    Figure 5. Choosing how many tiers

    flowchart TD accTitle: Choosing the number of tiers accDescr: A decision flow from whether clients can touch the database, through whether the web and logic layers must scale apart, to whether extra tiers are needed. Q1{"Can every client be trusted with direct database access?"} -->|"yes: a few internal users"| T1["Classic two-tier: client plus database"] Q1 -->|"no: the usual case"| Q2 Q2{"Must the web layer and the logic layer scale, secure or change separately?"} -->|"no"| T2["Two tiers: app server plus database"] Q2 -->|"yes"| Q3 Q3{"Do some jobs need their own scaling or failure handling?"} -->|"no"| T3["Three tiers: web, application, database"] Q3 -->|"yes"| TN["N tiers: add cache, search or queue tiers"]

    How to read this: QuickCart at launch answers "no" to the first question (shoppers are untrusted) and "no" to the second (small team, small load), so it lands on two tiers. Later stages answer "yes" to the second and third.

    Step 1: Requirements

    Functional vs non-functional requirements

    A functional requirement says what the system does, a behaviour a user can see. A non-functional requirement says how well: speed, availability, durability, security, cost. The first decides which boxes exist; the second decides how many copies and how they connect. See System Design: Requirements for the checklist.

    A requirement is testable only when it names an action, a number, a measurement point and a window. "Checkout must be fast" is a wish; "95% of checkouts complete in 3 seconds, measured at the application server over 30 days" is a requirement.

    Actor Functional requirements
    Shopper Browse and search nearby stores' products; manage a cart; check out and pay; see order status and courier progress; receive notifications
    Store See and accept orders; update stock and prices
    Courier See assigned deliveries; report pickup and delivery
    Quality Non-functional requirement (illustrative)
    Latency Browsing feels instant; checkout may take longer because it includes payment
    Availability Browsing tolerates more downtime than checkout; stores must receive orders
    Durability An order the shopper was told succeeded is never lost
    Consistency Stock and payment exact at checkout; a product page may lag by seconds
    Security Shoppers see only their own orders; card data never touches QuickCart's servers

    Write the out-of-scope list too: at Stage 1, no multi-region operation, recommendations, promotions or refunds.

    Step 2: Estimates

    Back-of-envelope estimation

    A back-of-envelope estimate is arithmetic with rounded numbers, done to find the order of magnitude before choosing a design: "one machine" or "a fleet". The general method is in System Design: Estimation. Here it is worked once for QuickCart; later stages reuse the results.

    Figure 6. The estimation chain from users to requests and bytes

    flowchart LR accTitle: From users to requests per second and bandwidth accDescr: Two chains. Browsing goes from monthly users to daily users, page views, average and peak requests per second and peak bandwidth. Orders go from orders per day to average and peak checkouts per second. M["10,000,000
    monthly users"] --> D["2,000,000
    daily users"] D --> P["x 20 views
    40,000,000 per day"] P --> A["/ 86,400 s
    about 463 per s"] A --> K["x 5 for peak
    about 2,300 per s"] K --> B["x 50 KB
    115 MB per s"] O["500,000
    orders per day"] --> OA["/ 86,400 s
    about 5.8 per s"] OA --> OP["x 10 for peak
    about 58 per s"]

    How to read this: the top chain sizes browsing, the bottom sizes checkout. Each arrow is one multiplication or division, so every number can be checked by hand. The peak factors (5 and 10) are assumptions; checkout's is higher because orders cluster at meal times.

    Traffic. There are 86,400 seconds in a day (24 x 60 x 60).

    1. Page views per day: 2,000,000 x 20 = 40,000,000.
    2. Average browsing rate: 40,000,000 / 86,400 = 462.96, about 463 requests per second.
    3. Peak (5 times average): 463 x 5 = 2,315, about 2,300 requests per second.
    4. Average checkout rate: 500,000 / 86,400 = 5.79, about 5.8 per second.
    5. Peak (10 times average): 57.9, about 58 checkouts per second.

    One page view counts as one back-end request here (a real page may fire several). Reads dominate: 2,315 / 57.9 = 40, so at peak there are about 40 browsing requests per checkout, so Stage 2 scales reads first.

    For the launch, daily users are 20% of monthly (2,000,000 / 10,000,000), so 2,000; one in four orders (500,000 / 2,000,000), so 500 orders a day. Page views: 2,000 x 20 = 40,000 a day, 0.46 per second, peak about 2.3 per second. Checkouts: 500 / 86,400 = 0.0058 per second, peak 0.058, about one every 17 seconds. One modest server handles that.

    Storage. Count times size. Every size is an illustrative assumption; units are decimal (1 KB = 1,000 bytes).

    Data Assumption Arithmetic Result
    Orders, one day 2 KB per order (row, about 8 line items, events) 500,000 x 2 KB 1 GB
    Orders, one year Constant volume 1 GB x 365 365 GB
    With overhead 2x for indexes 365 GB x 2 730 GB
    Users 1 KB per monthly user 10,000,000 x 1 KB 10 GB
    Inventory 2,000 products per store, 100 bytes per row 5,000 x 2,000 = 10,000,000 rows x 100 bytes 1 GB
    Images (files, not rows) 3 per product, 300 KB each, originals 100,000 x 900 KB 90 GB

    The database holds orders, users, inventory and a catalogue of about 0.1 GB (100,000 products x 1 KB): 730 + 10 + 1 + 0.1 = 741.1 GB, about 740 GB after year one. Images add 90 GB, kept as files on the server's disk at launch (Stage 3 moves them to object storage). Five years at constant volume: 5 x 730 GB = 3.65 TB. At launch orders add 500 x 2 KB = 1 MB a day.

    Bandwidth. Assume an average response of 50 KB (data, no images) and 10 thumbnails of 30 KB (300 KB of images) per page.

    • API data at peak: 2,300 x 50 KB = 115,000 KB/s = 115 MB/s = 115 x 8 = 920 Mbit/s.
    • Images at peak: 2,300 x 300 KB = 690 MB/s = 5,520 Mbit/s, about 5.5 Gbit/s.
    • Per day: API 40,000,000 x 50 KB = 2 TB; images 40,000,000 x 300 KB = 12 TB; total 14 TB.
    • Cross-check: 14 TB / 86,400 s = 162 MB/s average; times 5 is 810 MB/s, matching 805 (rounding of 2,315 to 2,300).

    Images are 12 of the 14 TB, six times the API data, and they never change, which is why Stage 2 adds a CDN. At launch everything is 1/1,000 of this (14 GB a day, 0.8 MB/s at peak).

    Measuring performance

    Latency vs throughput

    Latency is how long one request takes, from sending to response. Throughput is work completed per unit time, such as requests per second. A delivery van takes one hour to carry 100 parcels: latency one hour, throughput 100 an hour. A second van doubles throughput and leaves latency alone. Making the van wait until it is full raises throughput but delays every parcel: batching trades latency for throughput.

    Report latency as percentiles: p95 means 95% of requests were faster, p99 shows the tail. The Google SRE book prefers percentiles because an average hides the distribution.

    A worked example (all numbers illustrative). A product-list request takes 40 ms (20 ms CPU, 20 ms waiting for the database) and a worker handles one request at a time.

    • One worker: 1 / 0.040 = 25 requests per second. A server with 8 workers: 8 x 25 = 200.
    • Final peak: 2,300 / 200 = 11.5, so at least 12 servers at 100% utilisation.
    • Queues form when requests arrive randomly. In the simple M/M/1 model (random arrivals, random service times, one worker) average time in the system is service time / (1 - utilisation):
    Utilisation Average time in system Calculation
    50% 80 ms 40 / 0.5
    80% 200 ms 40 / 0.2
    95% 800 ms 40 / 0.05

    Real servers are not one M/M/1 queue, but the shape carries over: latency stays near service time at low load and climbs steeply near saturation. So plan for headroom: at 50% utilisation a server handles 100 per second and the final peak needs 2,300 / 100 = 23 servers.

    Little's law says average requests in flight equal arrival rate times average time in the system: with an assumed 0.1 s (illustrative), 2,300 x 0.1 = 230 at the final peak, but 2.3 x 0.1 = 0.23 at launch.

    Measuring reliability

    Availability, reliability and durability

    Availability is the fraction of time the system can serve requests, written in nines.

    Availability Downtime in 30 days (43,200 minutes) Per year (about)
    99% 432 minutes 3.65 days
    99.5% 216 minutes 1.8 days
    99.9% 43.2 minutes 8.8 hours
    99.99% 4.32 minutes 52.6 minutes

    Each added nine divides the allowance by ten (0.001 x 43,200 = 43.2).

    Reliability is the probability the system performs its function correctly over a period. A system can be up and wrong: if checkout answers every request but sometimes charges a card twice, it is available and unreliable. MTBF (mean time between failures) and MTTR (mean time to repair) give availability as MTBF / (MTBF + MTTR). With an illustrative MTBF of 720 hours and MTTR of 1 hour: 720 / 721 = 99.86%. Halve the repair time: 720 / 720.5 = 99.93%. Repairing faster helps as much as failing less.

    Durability is the promise that data acknowledged as saved is not lost. A database can be down an hour and lose nothing (durable, unavailable), or be up while dropping writes (available, not durable, worse). QuickCart's requirement: when the shopper sees "order confirmed", that order exists tomorrow.

    PostgreSQL shows how one server delivers this. With write-ahead logging, changes to data files are written only after the log records describing them are flushed to permanent storage, and a transaction counts as committed once its log record is flushed. By default (synchronous_commit is on) a commit waits for that local flush, and after a crash the server replays the log. So a process crash or power cut does not lose an acknowledged order. Losing the disk does: with one copy, only backups help. Disabling fsync risks unrecoverable corruption.

    Availability in series. A request needing several components succeeds only when all work, so availabilities multiply. Assume the application server, the database and the payment provider are each 99.9% (illustrative):

    • Browsing needs two: 0.999 x 0.999 = 0.998001, about 99.8%, or 0.002 x 43,200 = 86.4 expected minutes down in 30 days.
    • Checkout needs three: 0.999 cubed = 0.997003, about 99.7%, or 129.6 minutes in 30 days (about 26 hours a year).

    This is a ceiling, not a forecast: it ignores bad deploys and bugs. One copy of everything cannot honestly promise 99.9% for checkout. System Design: Reliability Basics shows how redundancy changes this.

    SLA, SLO and SLI

    The definitions follow the Google SRE book.

    • A service level indicator (SLI) is "a carefully defined quantitative measure of some aspect of the level of service that is provided". The SRE Workbook writes it as good events divided by valid events.
    • A service level objective (SLO) is "a target value or range of values for a service level that is measured by an SLI". An internal goal: "99.5% of valid checkouts succeed over 30 days".
    • A service level agreement (SLA) is a contract with users that includes consequences of meeting or missing the objectives. The book's test: with no explicit consequence, you are almost certainly looking at an SLO.

    Figure 7. SLI, SLO, error budget and SLA

    flowchart LR accTitle: How SLI, SLO, error budget and SLA relate accDescr: An SLI measurement is compared with an SLO target. The gap is the error budget, which drives release decisions. An SLA is a looser external contract built on top of the SLO. SLI["SLI: the measurement
    good requests / valid requests"] --> SLO["SLO: the internal target
    99.5% over 30 days"] SLO --> EB["Error budget
    the 0.5% allowed to fail"] EB --> DEC["Decision
    ship features or fix reliability"] SLO -.->|"set looser, with consequences"| SLA["SLA: the contract
    for example a service credit"]

    How to read this: the solid chain is the daily routine: measure, compare, spend or save the budget, decide. The dotted arrow shows an SLA is derived from the SLO but kept looser, so the team notices trouble before a customer is owed anything.

    An SLI should describe what users experience and count only valid requests: a card declined by the bank is a correct outcome, and a malformed request is not QuickCart's failure. Illustrative SLOs, measured at the application server over a rolling 30 days; the launch column is what the 99.8% and 99.7% ceilings allow:

    Journey SLI (good / valid) Launch SLO Target SLO
    Browsing availability Browse requests without server error 99.5% 99.9%
    Browsing latency Browse requests under 500 ms 95% 99%
    Checkout availability Checkouts not failing on QuickCart's side 99.5% 99.9%
    Checkout latency Checkouts under 3 s, including payment 99% 99%

    The launch checkout SLO (99.5%) sits under the 99.7% ceiling. Promising 99.9% on day one would be promising to miss.

    The error budget. An SLO of 99.5% lets 0.5% of events fail. That is the error budget, reliability you may spend. The SRE book frames it as the unreliability a service may have in a period: while budget remains, releases ship; when it is exhausted, releases are temporarily halted while the team invests in reliability. It can be counted as time or as requests (the book describes both).

    SLO Full-outage minutes in 30 days Failed requests, Stage 1 volume Failed requests, target volume
    Checkout 99.5% 216 75 75,000
    Checkout 99.9% 43.2 15 15,000
    Browsing 99.5% 216 6,000 6,000,000
    Browsing 99.9% 43.2 1,200 1,200,000

    Checkouts in 30 days: 500 x 30 = 15,000 at launch, 500,000 x 30 = 15,000,000 at target. Page views: 40,000 x 30 = 1,200,000 and 40,000,000 x 30 = 1,200,000,000. Take 0.5% or 0.1% of each.

    An SLA comes from outside. If QuickCart promised stores 99% monthly availability of the order tablet app with a service credit (illustrative), the internal SLO should be tighter, say 99.5%, so the budget burns visibly long before a credit is owed. The payment provider's own SLA bounds what QuickCart can promise about checkout.

    Steps 3 to 8: the first design

    Steps 3 and 4: API and data model

    Method and path Used by Purpose
    GET /products?store=17&q=milk Shopper Browse and search
    PUT /cart/items/4021 Shopper Set an item's quantity
    POST /orders Shopper Check out
    GET /orders/9001 Shopper Status (the client asks again to track)
    PATCH /stores/17/inventory/4021 Store Update stock or price
    POST /stores/17/orders/9001/accept Store Accept an order
    POST /deliveries/55/status Courier Report pickup or delivery

    A checkout body names items and quantities; prices come from the server:

    {
      "storeId": 17,
      "items": [
        { "productId": 4021, "quantity": 2 },
        { "productId": 77, "quantity": 1 }
      ],
      "paymentToken": "tok_example_123"
    }

    The paymentToken is a placeholder from the payment provider, so card numbers never reach QuickCart. The schema is a PostgreSQL sketch: money is integer cents, never floating point, and one order belongs to one store.

    CREATE TABLE stores (
      id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
      name TEXT NOT NULL
    );
    
    CREATE TABLE products (
      id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
      name        TEXT NOT NULL,
      description TEXT
    );
    
    CREATE TABLE inventory (
      store_id    BIGINT  NOT NULL REFERENCES stores(id),
      product_id  BIGINT  NOT NULL REFERENCES products(id),
      price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
      stock       INTEGER NOT NULL CHECK (stock >= 0),
      PRIMARY KEY (store_id, product_id)
    );
    
    CREATE TABLE orders (
      id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
      shopper_id  BIGINT      NOT NULL,
      store_id    BIGINT      NOT NULL REFERENCES stores(id),
      status      TEXT        NOT NULL DEFAULT 'pending_payment',
      total_cents INTEGER     NOT NULL,
      created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
    );
    
    CREATE TABLE order_items (
      order_id         BIGINT  NOT NULL REFERENCES orders(id),
      product_id       BIGINT  NOT NULL REFERENCES products(id),
      quantity         INTEGER NOT NULL CHECK (quantity > 0),
      unit_price_cents INTEGER NOT NULL,
      PRIMARY KEY (order_id, product_id)
    );

    Price lives in inventory because each store sets its own, and is copied into order_items at purchase so later changes do not rewrite history. The CHECK (stock >= 0) makes the database itself refuse to sell what is not there.

    Steps 5 and 6: the high-level design and the hardest part

    The design is one application server and one database, in one region. The server terminates HTTPS, serves the API for all clients and holds the business rules; the database holds everything durable; payments and messages go to outside providers. In Figure 5's terms, two tiers.

    The deep dive is checkout: two shoppers must not both buy the last carton of eggs. With one relational database that is cheap. Do the check and the decrement in one statement:

    BEGIN;
    
    -- One statement per line item: take the stock only if enough remains.
    UPDATE inventory
       SET stock = stock - 2
     WHERE store_id = 17 AND product_id = 4021 AND stock >= 2;
    -- psql prints UPDATE 1 on success. UPDATE 0 means not enough stock:
    -- the application issues ROLLBACK and tells the shopper.
    
    -- (remaining items, then:)
    INSERT INTO orders (shopper_id, store_id, total_cents)
    VALUES (88, 17, 1348);
    
    COMMIT;

    PostgreSQL's default isolation level is Read Committed. If two transactions update the same row, the second waits for the first to commit or roll back, then re-evaluates its WHERE clause against the new row version. If the first buyer took the last two cartons, the second's stock >= 2 is now false and zero rows update, so there is no oversell. One database means one transaction can cover stock and order rows (the provider call stays outside it). Later stages give this up on purpose, and Stages 4 to 6 deal with the cost.

    Figure 8. Checkout on the first design

    sequenceDiagram accTitle: Checkout on QuickCart Stage 1 accDescr: The shopper app posts an order. The server reserves stock and records a pending order in one database transaction, charges the payment provider, then marks the order paid and replies. participant S as Shopper app participant A as App server participant D as Database participant P as Payment provider S->>A: POST /orders A->>D: BEGIN, take stock, insert order as pending_payment, COMMIT D-->>A: committed A->>P: charge with payment token P-->>A: approved A->>D: UPDATE order status to paid A-->>S: 201 Created, order confirmed Note over A,P: If the reply from the provider is lost, the outcome is unknown

    How to read this: the first database step is one atomic transaction. The provider call is deliberately outside it, because a transaction should not stay open while waiting for a third party. The note marks a real gap: a lost reply leaves the charge unknown. The first design accepts it; Stage 6 closes it with idempotency keys and a ledger.

    Steps 7 and 8: failures and trade-offs

    The honest failure list at Stage 1:

    1. The application server dies. Everything stops; no failover.
    2. The database host or disk fails. Everything stops; data may be gone except backups.
    3. The payment provider is slow or down. Checkout fails or hangs; browsing is unaffected.
    4. A lost provider reply leaves an order in pending_payment with unknown payment status.
    5. A traffic spike or a bad deploy hits the only copy.

    The trade-off: Stage 1 buys simplicity at the price of every item on that list. Each later stage removes some.

    Decisions and trade-offs

    Option Gain Cost When it is the wrong choice
    Apps talk to the database directly No server to run Rules in every client; database exposed; totals can be forged Untrusted users or money, which is QuickCart
    One app server plus one database (chosen) Fewest parts; ACID transaction covers stock, orders, payments; quick to launch Single points of failure; one machine's capacity When launch must already beat about 99.7% checkout availability
    Three tiers with a load balancer, two app servers, a replica from day one Survives one server loss; room to grow Spend and operations for traffic that does not exist yet When load is 1/1,000 of target and the team is small
    Microservices from day one Independent deploys and scaling Many services before any boundary is understood When the team is small and the domain unstable

    The chosen option is the best design for the launch numbers, not in general.

    In practice: QuickCart at this stage

    NOTE

    This is a realistic scenario, not a case history. It is a composite built from common patterns; it describes no real company and every number is an illustrative assumption.

    The situation

    A four-person team has eight weeks to launch QuickCart in one city, with a clickable prototype and 30 partner stores. Expected traffic is the Stage 1 estimate: 2,000 daily users, 500 orders a day, a peak of 2.3 browse requests per second and a checkout every 17 seconds. Nobody has been on call before.

    Starting architecture

    The prototype is built fast: the apps talk straight to a hosted database with credentials stored in the app, and the shopper app computes the total and calls the payment provider itself.

    Figure 9. The prototype: every app talks straight to the database

    Prototype: apps talk straight to a hosted databaseShopper app to Payment provider (charge card); Shopper app to Database (SQL); Store tablet app to Database (SQL); Courier app to Database (SQL)Shopper app: totals computed hereShopper apptotals computedhereStore tablet appStore tabletappCourier appCourier appDatabase: keys in the appsDatabasekeys in the appsPayment providerPaymentprovidercharge cardSQLSQLSQL

    How to read this: every arrow ends at the database, so every app holds its credentials and its own copy of the rules. Anyone who extracts the credentials can read every order, a modified app can send a total of zero, and a rule change means shipping three apps.

    Options considered

    The team weighs the four options in the trade-offs table. Microservices go first: no boundaries are known. The direct connection goes next, for the forged-total risk. Three tiers go because capacity is not the problem: one worker handles 25 requests per second at 40 ms, so a launch peak of 2.3 is about 9% of one worker (2.3 / 25 = 0.092) and a server with 8 workers sits near 1%.

    Availability is the real constraint: the series ceiling for checkout is 99.7%, so the launch SLO is 99.5%.

    The decision

    Field Decision record
    Context Launch in eight weeks at about 1/1,000 of target scale; four engineers; untrusted clients; money moves.
    Decision One application server with all business rules, one PostgreSQL database, payments through the provider's token flow, one region. Launch SLOs: availability 99.5% for browsing and checkout, browse p95 under 500 ms, checkout 99% under 3 s.
    Consequences, good Fast launch; one transaction covers stock, order and payment status; the team can reason about the whole system.
    Consequences, bad Every component is a single point of failure; no headroom beyond one server; the lost-reply gap exists.
    Revisit when Monthly users pass 100,000 (Stage 2), the error budget is spent twice in a quarter, or peak browsing passes about 20 requests per second.

    Rollout and verification

    1. Load test at ten times the launch peak: about 23 browse requests and 0.6 checkouts per second. Read percentiles, not the average.
    2. Test the oversell guard: two sessions try to buy the last unit; exactly one succeeds.
    3. Restore a backup plus archived log to a fresh machine and compare row counts. A backup nobody has restored is a hope.
    4. Add a synthetic probe on another host that every minute browses a product and places a test order, and pages after two failures. It measures the SLI from outside.
    5. Kill the application process on purpose in staging and confirm it restarts.
    6. Launch to a few stores and watch the SLIs for two weeks first.

    The 2 a.m. failure

    At 02:10 the probe fails twice and pages the on-call engineer: "synthetic checkout failing for 2 minutes". The operating system killed the application process for using too much memory, and nothing restarted it. Detection worked because the SLI is measured from outside the failed machine. The first three checks:

    # 1. Is the process running?
    systemctl status quickcart-app --no-pager
    
    # 2. What did it log around the time of the failure?
    journalctl -u quickcart-app --since "02:00" --no-pager
    
    # 3. Did the kernel kill it for memory? (dmesg may need sudo)
    dmesg -T | grep -i "killed process"

    Check 3 confirms an out-of-memory kill. Mitigation: systemctl restart quickcart-app, then run the probe by hand. Outage: about 9 minutes, 9 / 216 = 4% of a 99.5% SLO's 30-day budget. Follow-ups: automatic restart after a crash, fix the memory growth, alert before the limit, and add a second application server in Stage 2.

    Architecture after this stage

    Stage 1 is the starting point, so the figure shows the whole design. Later stages keep this layout and add only what is new.

    Figure 10. QuickCart after Stage 1: one application server and one database

    QuickCart after Stage 1: one application server, one database, two outside providersShoppers to App server (HTTPS); Stores to App server (HTTPS); Couriers to App server (HTTPS); App server to PostgreSQL (SQL); App server to Payment provider (charge card); App server to Email and SMS provider (send)ShoppersShoppersStoresStoresCouriersCouriersApp server: API and rulesApp serverAPI and rulesPostgreSQL: orders and stockPostgreSQLorders and stockPayment providerPaymentproviderEmail and SMS providerEmail and SMSproviderHTTPSHTTPSHTTPSSQLcharge cardsend

    How to read this: all three kinds of client reach one application server, the only component that talks to the database and the outside providers. Every box is a single copy, the weakness Stage 2 starts to fix.

    What QuickCart looks like now

    Component Why it exists What it costs
    One application server Holds the API and rules so clients are not trusted Single point of failure; one machine's capacity
    One PostgreSQL database One transaction covers stock, orders and payments; durable via its write-ahead log One copy of the data; becomes the bottleneck as load grows
    Backups and archived log Recovery from disk loss Restores must be tested; the newest data may be lost
    Payment provider (outside) Moves card handling out of QuickCart In the checkout path; its availability multiplies into ours
    Email and SMS provider (outside) Order notifications Its failure must not break checkout
    Synthetic probe and SLO dashboard Measures the SLIs from outside A small piece of infrastructure to keep alive

    Architect's notes

    • Decide the numbers before the boxes. The estimate (2.3 requests per second at launch, 2,300 at target) justifies both the simplicity now and the redesign later.
    • Set the SLO below what the architecture can deliver. Multiply the availabilities in the path; if the result is below the target, change one of them.
    • Measure from outside, and test the restore.

    Common mistakes

    • Writing "fast" and "reliable" without numbers. If it cannot be tested, it cannot be alerted on.
    • Forgetting units. Mixing bytes and bits, or per day and per second, breaks an estimate by 8 or 86,400.
    • Sizing for the average. The busiest second is several times the mean; state the peak factor.
    • Setting an SLO of 100%. It leaves no error budget.
    • Holding a database transaction open across a third-party call. Commit, call out, then record the result.

    Key takeaways

    • Client-server: clients start requests, servers answer; HTTP is stateless request/response and the server owns the rules.
    • Tiers are physical, layers are logical. Stage 1 has two server-side tiers.
    • Functional requirements decide which boxes exist; non-functional requirements decide the copies and connections. Each needs a number, a measurement point and a window.
    • The shared numbers: 40,000,000 views a day, about 463 per second, about 2,300 at peak; 500,000 orders a day, about 5.8 per second, about 58 at peak; about 40 browses per checkout.
    • Latency is what one request feels; throughput is what the system completes. Latency climbs steeply near saturation.
    • Availability, reliability and durability differ. Series availability multiplies: three components at 99.9% give about 99.7%.
    • SLIs measure, SLOs are targets, SLAs are contracts. The error budget turns reliability into a decision.
    • The 8-step method produced a first design of one application server and one database, with its weaknesses written down.

    Practice exercises

    Exercise 1 — Re-run the estimate (30 min). Daily users are 25% of monthly and each views 30 pages. Recompute page views per day, average and peak requests per second (peak factor 5) and peak API bandwidth at 50 KB.

    Exercise 2 — SLOs for the store tablet (45 min). Define an availability SLI and a latency SLI for "a store sees a new order on its tablet", propose SLOs, and compute the 30-day error budget in minutes and in requests at 500,000 orders a day.

    Exercise 3 — Resize the servers (1 hour). Assume 25 ms service time and 8 workers per server. Compute one server's capacity and the servers needed for 2,300 requests per second at 100% and at 50% utilisation.

    Exercise 4 — Try the oversell guard (2 hours). Run PostgreSQL locally, create the schema, insert one store, one product and one inventory row with stock 2, and use two psql sessions to run the checkout UPDATE for 2 units in both, committing the first only after the second has started. Explain what the second session does.

    Exercise 5 — Draw the three-tier version (1 hour). Redraw Figure 10 with a web tier, two application servers and a database standby, and list what each addition fixes from the failure list. Compute checkout availability if the application tier becomes two servers at 99.9% each (1 - 0.001 x 0.001).

    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