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
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
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
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
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
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
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).
- Page views per day: 2,000,000 x 20 = 40,000,000.
- Average browsing rate: 40,000,000 / 86,400 = 462.96, about 463 requests per second.
- Peak (5 times average): 463 x 5 = 2,315, about 2,300 requests per second.
- Average checkout rate: 500,000 / 86,400 = 5.79, about 5.8 per second.
- 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
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
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:
- The application server dies. Everything stops; no failover.
- The database host or disk fails. Everything stops; data may be gone except backups.
- The payment provider is slow or down. Checkout fails or hangs; browsing is unaffected.
- A lost provider reply leaves an order in
pending_paymentwith unknown payment status. - 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
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
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
- Load test at ten times the launch peak: about 23 browse requests and 0.6 checkouts per second. Read percentiles, not the average.
- Test the oversell guard: two sessions try to buy the last unit; exactly one succeeds.
- Restore a backup plus archived log to a fresh machine and compare row counts. A backup nobody has restored is a hope.
- 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.
- Kill the application process on purpose in staging and confirm it restarts.
- 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
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).
Related reading
- QuickCart Stage 2: Scale Reads — the next stage: load balancer, stateless servers, a cache and a CDN.
- QuickCart Stage 9: Survive Failure — this lesson's failure list turned into timeouts, failover and recovery targets.
- System Design: A Repeatable Approach — the 8-step method in general form.
- System Design: Requirements — the full non-functional checklist.
- System Design: Estimation — latency numbers, peak factors and sanity checks.
- System Design: Reliability Basics — redundancy and availability arithmetic.
- System Design: Observability and SLOs — SLIs, SLOs and burn-rate alerts in practice.
Official documentation
- Google SRE book: Service Level Objectives — definitions of SLI, SLO and SLA, and the preference for percentiles.
- Google SRE book: Embracing Risk — error budgets and release decisions.
- RFC 9110: HTTP Semantics — clients, servers and the stateless request/response model (sections 3.3 and 3.4).
- Azure Architecture Center: N-tier architecture style — layers versus physical tiers, open and closed layers.
- PostgreSQL documentation: Write-Ahead Logging — log before data, commit means flush, recovery by replay.
- PostgreSQL documentation: Transaction isolation — Read Committed behaviour for concurrent updates of one row.