SwadhyAI Notes

All System Design notes

Case study

Design a flash sale

Imagine Prime Day or a sneaker drop: at 12:00:00, a million shoppers open the same deal page and mash Buy. To them it feels like one checkout. Under the hood you must stay fast, never oversell a scarce SKU, and clean up when payment succeeds but inventory says no.

A flash sale is inventory locking under a stampede. Protect browse with a CDN, protect purchase with , reject sold-out in milliseconds with a , then run the real commit as a .

What happens on Buy?

1

Get a short-lived pass to checkout.

2

Atomic decrement; reject sold-out fast.

3

Order → pay → reserve, async pending.

4

Refund, cancel, restock on failure.

Try it: flash-sale inventory under burst traffic

A sale needs a reserved counter or queue ahead of the database so oversell stops at the hot path.

  1. Client
  2. Edge
  3. Rate limit
  4. Inventory counter
  5. Order DB
Highlighted hops show where the last action moved traffic. A stopped hop means backpressure, rejection, or a consistency guard blocked further progress on that path.
Attempts
0
Orders
0
Mode
open
Path hop
0

Hot key

counter

0/5 units sold.

Protection

rate limit

0 attempts; bots shed at the edge.

Consistency

oversell

Never trust a cached “in stock” flag alone for the final decrement.

Hammer buy attempts while inventory remains, then watch sold-out reject before the DB.

Go deeper on the building blocks

These notes pair with the diagram above.

What actually happens

Most people are only browsing. The CDN serves images and sale HTML. When someone joins the waiting room, issues a signed token so bots and overload stay at the edge. A signed waiting-room token proves that the holder was admitted under a particular policy; it does not by itself establish fair ordering or prevent replay. Bind it to the account or session, event, expiry, and allowed attempt budget. Track issued admissions against inventory and payment capacity, and serve queue status from a cheap path so waiting users do not overload the service protecting checkout.

On Buy, the gateway runs a . If stock is gone, return immediately: no payment attempt. If the counter says yes, start a and return pending. The orchestrator drives order, payment, and inventory over the broker. Each service uses an so events are not lost after a crash.

If inventory fails after payment, refunds and restocks. An on checkout keeps double-clicks from starting two sagas. Compensation is a new operation with its own durable state and retries. Refunding payment does not erase the original charge, and restocking must occur only if that checkout actually owned an unreleased reservation. If compensation fails, retain a visible pending-recovery state and alert an owner. Otherwise an apparently cancelled order can leave both money and inventory inconsistent.

How to choose the flash-sale design

1. Separate looking from buying? Yes. Cache browse aggressively. Put a waiting room and rate limits on checkout so 10K tries/sec do not melt the orchestrator.

2. Sync stock or async commit? Use Redis only for a fast no. The real commit is an async across order, payment, and inventory databases.

3. Pending or block until done? Prefer pending. Blocking the HTTP thread until payment and inventory finish does not survive a stampede.

Simple rule of thumb

Tell the story in order: edge protects origin, queue protects purchase, Redis rejects sold-out in one millisecond, runs the real checkout with in every service, and fixes the messy middle when step three fails after step two succeeded.

In simple terms: do not invent a giant distributed transaction. Invent a fair line, a fast no, and an undo plan.

The map for the rest of this note

The reference architecture uses a , an in each microservice, a message broker for async commands, and Redis for the millisecond stock check. The diagram below is the map; architecture layers, capacity math, and saga scenarios deepen every box.

High-level design diagram

Follow the arrows top to bottom, then open each architecture layer in the next section (same order as the drawing).

Flash sale architecture: clients, CDN, WAF, load balancer, API gateway with Redis pre-check, saga orchestrator with intake replay and reply queues, Kafka topics to order payment and inventory services with outbox tables, compensation and observability
Sync call

Waits for an answer before moving on

Async command

Fire-and-forget over the broker

Reply

Service answers the orchestrator

DB write

Rows saved in one local transaction

Outbox publish

Event leaves the database reliably

Architecture layer by layer

Each block matches the HLD diagram. Expand any layer for interview talking points.

01
Clients and edge

Keep the crowd from crushing the origin.

Think of it like: Like opening extra doors at a stadium: most people never reach the main hall.

  • CDN caches images, JS, and sale HTML close to users.
  • WAF and DDoS filters block bad traffic before it hits your servers.
  • L7 load balancer spreads healthy traffic across gateway pods.

02
API gateway

The bouncer plus the quick stock check.

Think of it like: Checks your queue ticket, then peeks at a sticky note on the wall (Redis) before starting paperwork.

  • Auth, rate limits, and signed admission tokens from the waiting room.
  • Sync call to Redis: Lua script atomically checks and decrements stock.
  • If sold out, return immediately. No saga, no payment attempt.

Redis provides an early admission estimate, while authoritative inventory decides whether a sale can be confirmed. A stale cache may reject an available item or allow a request that later loses the reservation race; it must never create stock by itself. Track each cache reservation and release by a stable operation ID so duplicate compensation cannot increment the counter twice. Reconcile the estimate against durable inventory after failures.

lua
-- stock:sku:{id}  (Lua runs atomically on Redis)
local left = tonumber(redis.call('GET', KEYS[1]) or '0')
if left < tonumber(ARGV[1]) then return 0 end
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1

03
Saga orchestrator

The stage manager who remembers every step.

Think of it like: One person with a clipboard: who paid, who reserved stock, what to undo if step 3 fails.

  • Intake queue: new checkouts from the gateway.
  • Reply queue: OrderCreated, PaymentCharged, InventoryReserved, and failures.
  • Replay queue: retries and compensation commands with backoff.
  • Orchestrator DB: saga id, current step, audit trail for support.
Read the full pattern note

04
Message broker

The post office between teams.

Think of it like: Orchestrator drops letters in labeled boxes; each service picks up only its mail.

  • Topics: order, payment, inventory (Kafka or RabbitMQ).
  • Partition by order_id so one order’s messages stay in order.
  • Replies on a dedicated topic the orchestrator consumes.

05
Order, payment, inventory

Three small databases, one big story.

Think of it like: Each team keeps its own filing cabinet and slips a copy of every change into an outbox tray for the mail room.

  • Every change: business row + outbox row in one DB commit.
  • Outbox processor or CDC publishes events so crashes cannot lose messages.
  • Payment talks to an external card gateway; inventory holds rows authoritatively.
, , and sagas explained

06
Observability

When 10K things happen per second, you need one id to follow a single order.

Think of it like: Like a package tracking number that works across warehouse, truck, and front desk.

  • Logs: same saga_id on gateway, orchestrator, and all three services.
  • Metrics: queue depth, Redis reject rate, saga success %, payment timeouts.
  • Tracing: Jaeger or Zipkin across the full checkout path.
  • Alerts: replay queue or DLQ spikes during the event window.

Why shows up three times in the diagram

  1. Order service saves PENDING order and outbox OrderCreated together.
  2. Payment service saves CHARGED and outbox PaymentCharged together.
  3. Inventory service saves RESERVED and outbox InventoryReserved together.

What the shopper experiences

Maps the architecture above to a story interviewers remember.

1

Browse the drop

You: You open the sale page. Product photos and prices may be a few seconds old. That is fine.

Behind the scenes: CDN serves static pages. Catalog reads hit cache. No heavy writes yet.

2

Join the virtual line

You: The site says “You are #4,521 in line.” You wait, then get a short-lived pass to checkout.

Behind the scenes: Admission control protects the purchase path. Bots and overload stay at the edge.

3

Tap Buy

You: You see “Processing…” while the store checks stock and runs payment behind the scenes.

Behind the scenes: Gateway does a fast Redis check, then starts a saga. You get pending status, not instant success.

4

Confirmed or clearly failed

You: Either “Order confirmed” or “Sold out / payment failed” with money safe.

Behind the scenes: Orchestrator finishes all steps or runs compensation (refund, cancel, restock).

Requirements

Clarify these before capacity math and APIs.

Functional

  • Flash-sale landing page and SKU list (cache-friendly)
  • Virtual waiting room with fair admission
  • Short checkout path for admitted users only
  • Multi-step purchase: order → pay → reserve stock → confirm
  • Automatic undo on failure (refund, cancel, restock)
  • Order status: pending, confirmed, or failed

Non-functional

  • No oversell on flash SKUs (harder rule than fresh catalog text)
  • Purchase path highly available; browse may slow down
  • Full audit trail per checkout for support and finance
  • Idempotent retries so double-clicks do not double-charge

Numbers that drive the design

You do not need perfect math in an interview. You need numbers that force a decision.

1M+
Shoppers online

Most are browsing, not buying

10K/s
Checkout tries/sec

Peak on a few hot SKUs

50K/s
Edge requests/sec

CDN absorbs most reads

<500ms
Checkout p99

After user leaves the queue

Why a waiting room? Without admission control, 10K checkout attempts per second would crush the orchestrator and inventory DB. The queue turns a stampede into a steady trickle of real buyers.

Browse load: 1M shoppers × 5 refreshes/min = 5M requests/min, or about 83K RPS. CDN hit rate must be high enough that origin sees only a small fraction.
Origin fallback: 83K RPS × (1 - 0.98 cache hit rate) ≈ 1.7K RPS to origin for cached pages and product reads.
Checkout queue: 10K checkout tries/sec × 120 sec sale surge = 1.2M intake messages. If workers finish 6K/sec, backlog grows at 4K/sec.
Time to drain: 480K backlog / (12K/sec scaled workers - 10K/sec arrivals) = 240 sec after scaling, so users need honest pending status.
Stock counter state: 100K hot SKUs × 150 bytes/counter × 3 replicas ≈ 45MB raw; hot-key throughput, not memory, is the problem.
Saga storage: 1.2M started sagas × 2KB state/audit = 2.4GB raw for a two-minute event before indexes, outbox rows, and retention.

Case-study upgrade

Flash Sale HLD Math

A flash sale is a burst of checkout attempts on a tiny hot SKU set. Browse and catalog can lag; inventory reservation, payment, and saga state must stay correct under 10K+ attempts/sec.

Assumptions and formulas

  • 1M+ clients during the event window; only a fraction completes checkout.
  • Peak checkout attempts: 10K/sec on a handful of SKUs (extreme hot keys).
  • Fast path: Redis atomic counters for pre-check; durable inventory in the inventory service DB.
  • Saga steps: create order → charge payment → reserve inventory → confirm; compensate on failure.

Concrete math

  • Browse/API: 50K peak QPS at edge; only admitted users reach checkout (virtual waiting room cuts effective load).
  • Redis DECR: 10K atomic ops/sec per hot SKU shard; Lua script keeps check-and-decrement one round trip.
  • Orchestrator intake: 10K saga starts/sec → partitioned intake queue; DB stores saga state (~2 KB/row).
  • Outbox: 10K writes/sec x 3 services x ~500 bytes/event ≈ 15 MB/sec async publish load (CDC lowers poll cost).
  • Inventory holds: 10K/sec x 10 min hold ≈ 6M live holds if every attempt reserves (admission control lowers this).

Design decisions forced by numbers

  • Never expose Redis inventory counters to clients; gateway or inventory service owns the fast check.
  • Orchestrate with a central saga engine when flows branch and need compensation visibility.
  • Outbox every command side-effect so crash between DB write and publish cannot lose events.
  • Idempotent consumers and compensations so retries and duplicate events are safe.

Scaling to millions

  • At 100K users: single-region Redis + one orchestrator + three services is enough for practice.
  • At 10M users: shard orchestrator DB, partition Kafka by order_id, isolate hot SKU keys in Redis.
  • At 100M+ users: waiting room, per-SKU admission tokens, regional edge, and replay queues with DLQ alerts.

Readable flow

Edge admit
Gateway + Redis pre-check
Saga start
Order
Payment
Inventory
Reply / compensate

Interview follow-ups to expect

  • What happens if payment succeeds but inventory is gone?
  • How do you restock Redis when compensation runs?
  • How do you debug one stuck saga across ten services?

Operational readiness checklist

After the math, explain how this capacity plan is measured, protected, launched, and evolved.

Observability

  • Attach each capacity estimate to a dashboard metric: p99 latency, queue age, cache hit rate, shard skew, error rate, and freshness lag.
  • Alert on rate-of-change and saturation, not only hard failures; backlogs and hot keys usually warn before an outage.

Security and abuse

  • Budget for auth checks, rate limits, quota isolation, audit logging, privacy filters, and abuse-review side paths.
  • Separate trusted internal events from user-controlled input so retries and backfills cannot bypass policy checks.

Degraded modes

  • Define the fallback when a dependency is slow: stale cache, partial results, read-only mode, delayed async work, or admission control.
  • Protect the invariant first; reduce freshness, personalization, or analytics quality before risking duplicate or corrupt state.

Testing and rollout

  • Test average, peak, and hot-object traffic; include retry storms, cold caches, lagging workers, and regional dependency loss.
  • Use canaries, shadow reads, dual-write validation, feature flags, and explicit rollback criteria for risky path changes.

Rollout, backfill, and runbooks

  • Make migrations and backfills idempotent, resumable, rate-limited, and visible through progress and error metrics.
  • Write the runbook before launch: owner, dashboards, paging thresholds, mitigation switches, replay steps, and repair commands.

10x evolution

  • For flash sale hld math, state which number forces the next move: cache memory, shard count, write amplification, queue lag, egress, or failover reserve.
  • Add the next layer only when the metric forces it: sharding, regional reads, async replay, hot-key isolation, or stricter backpressure.

APIs worth naming in the interview

GET/flash-sale/{eventId}SKU list (cached at CDN)
POST/flash-sale/{eventId}/join-queueQueue position or admission token
POST/checkoutStart purchase; requires Idempotency-Key + admission token
GET/orders/{orderId}pending | confirmed | failed

Internal events (orchestrator ↔ broker)

Not HTTP. JSON messages on Kafka (or RabbitMQ) topics, keyed by order_id.

OrchestratorMessage brokerServices

Commands

Orchestrator → broker → service

The orchestrator publishes the next step. One service consumes and acts.

CreateOrderorder

Order service·Insert PENDING order + outbox

ChargePaymentpayment

Payment service·Call payment gateway

ReserveInventoryinventory

Inventory service·Authoritative stock hold

Replies

Service → broker → orchestrator

Success or failure replies unblock the saga state machine.

OrderCreatedreply

Orchestrator·Move to payment step

PaymentChargedreply

Orchestrator·Move to inventory step

InventoryReservedreply

Orchestrator·Mark saga COMPLETED

PaymentFailedreply

Orchestrator·Start compensation

InventoryFailedreply

Orchestrator·Refund + cancel

Compensation commands

Orchestrator → replay queue → broker

Published when a forward step fails after earlier steps succeeded.

RefundPaymentpayment

Payment service·Idempotent refund

CancelOrderorder

Order service·Status → CANCELLED

ReleaseInventoryinventory

Inventory service·Release hold + Redis INCRBY

Rule of thumb: commands and replies use different topics so the orchestrator never confuses “charge card” with “payment succeeded.” Partition by order_id so one checkout stays ordered. A partition key orders records inside that topic partition, not across several command and reply topics. The orchestrator must still validate the expected saga state, step ID, and version when replies arrive. A delayed payment response cannot advance an order that has already been cancelled without following the reconciliation policy. Persist state transitions before scheduling the next command.

Use an idempotency key on the public checkout API so retries do not start two sagas. On the consumer side, use so duplicate broker messages are harmless.

The checkout saga (forward and undo)

After the architecture makes sense, study how steps succeed, fail, and compensate.

Forward path (happy path)

Each service commits in its own database. There is no global rollback, only local commits.

Orchestrator

Saga orchestrator

Runs the workflow

Service

Orders

Database

Orders DB

committed

order saved

charge card
Service

Payments

Database

Payments DB

committed

card charged

reserve item
Service

Inventory

Database

Stock DB

committed

stock held

When inventory fails

Stock is unavailable. Undo earlier steps with refund and cancel order.

Item out of stock: payment and order were already committed in their own databases.

Orchestrator

Saga orchestrator

Runs the workflow

Service

Orders

Database

Orders DB

cancel order

undo step 1

cancel order
Service

Payments

Database

Payments DB

refund

undo step 2

refund
Failed
Service

Inventory

Database

Stock DB

out of stock

nothing reserved

End state: money returned, order cancelled. Customer is not charged for an unbooked product.

Dotted arrows show message direction. Animation suggests messages in flight (respects reduced-motion settings).

Happy path (sequence)

Return pending only after the checkout command and stable operation ID are durably stored. The shopper polls or subscribes to that operation for confirmed, failed, or recovery-required status. A lost initial response should lead a retry to the existing operation, not another saga. Also expire queued intent that is no longer useful so old checkout requests do not unexpectedly purchase items long after the sale.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
sequenceDiagram
    autonumber
    participant U as Shopper
    participant G as API gateway
    participant R as Redis stock
    participant O as Saga orchestrator
    participant K as Message broker
    participant Ord as Order service
    participant Pay as Payment service
    participant Inv as Inventory service

    U->>G: Buy now (admission token)
    G->>R: Lua check and decrement
    R-->>G: ok
    G->>O: Start saga (async)
    G-->>U: 202 accepted, order pending

    O->>K: CreateOrder
    K->>Ord: command
    Ord-->>O: OrderCreated reply

    O->>K: ChargePayment
    K->>Pay: command
    Pay-->>O: PaymentCharged reply

    O->>K: ReserveInventory
    K->>Inv: command
    Inv-->>O: InventoryReserved reply

    O-->>U: Order confirmed (push or poll)

When inventory fails after payment

This is the money question. runs on the replay queue; Redis stock goes back up.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
sequenceDiagram
    autonumber
    participant O as Saga orchestrator
    participant Pay as Payment service
    participant Inv as Inventory service
    participant R as Redis stock
    participant Q as Replay queue

    Note over O, Inv: Payment already succeeded
    O->>Inv: ReserveInventory
    Inv-->>O: InventoryFailed

    O->>Q: Schedule compensation
    O->>Pay: RefundPayment
    Pay-->>O: Refunded
    O->>R: INCRBY restock counter
    O-->>O: Saga FAILED, user notified

From the pattern note

Saga scenarios for this flash sale

The flash-sale design uses orchestration (a central coordinator). Below are the same teaching scenarios as the Saga pattern page, rewritten with checkout examples so you can stay on this note while the ideas land.

Full saga pattern note

Choreography vs orchestration

Flash sales almost always pick orchestration. Here is why.

Choreography (event chain)

Order service emits OrderCreated → payment listens → payment emits PaymentCharged → inventory listens. No central brain.

Order→Payment→Inventory
  • ✓ Teams stay decoupled
  • ✗ Hard to see one checkout end-to-end during an incident
  • ✗ No single place to answer “what step failed for order 8821?”

OK for 2–3 step flows between mature teams.

Orchestration (this design)

One orchestrator owns the script, queues, and saga row. Matches the diagram's intake / reply / replay queues.

Forward

  1. OrderService.create(orderId): ok
  2. PaymentService.charge(orderId, amount): ok
  3. InventoryService.reserve(orderId):failed (stop forward path)

(reverse)

  1. PaymentService.refund(orderId): ok
  2. OrderService.cancel(orderId): ok
  3. Redis INCRBY restock flash SKU counter

Saga ends failed but consistent: no stock reserved, money returned, order cancelled, shopper sees a clear failure (not a silent hang).

  • ✓ One saga_id in logs and traces
  • ✓ Replay queue for retries and compensation
  • ✗ Orchestrator must be highly available (replicated)

Flash-sale checkout saga, step by step

Same idea as the ride-booking walkthrough on the Saga page, but for a timed product drop.

1. Admit shopper (queue token)2. Redis pre-check stock3. Create order (PENDING)4. Charge payment5. Reserve inventory (authoritative DB)6. Confirm order + notify

If step 5 fails (inventory gone at the DB)

Even if Redis pre-check passed milliseconds ago, the authoritative DB can still reject (race or reconciliation lag). Compensate: refund payment → cancel order → bump Redis stock → notify shopper “Sold out, refund issued.”

10K/s

checkout attempts at peak

3+

services + orchestrator per purchase

Designing compensations for checkout

Every forward step in the saga needs a safe undo story.

1

Idempotent

Refund(orderId) twice must still produce one refund. Store idempotency keys on payment and compensation handlers. See .

2

Commutative

Refund before release stock vs release before refund should end in the same business state.

3

Retryable

PSP timeouts get exponential backoff. Permanent failures go to a for humans.

4

Semantic undo

Mark order CANCELLED; do not delete rows finance must audit. Email cancellation is not a DB rollback.

Forward stepCompensate
CreateOrder → status PENDINGCancelOrder → status CANCELLED (keep row for audit)
ChargePayment($amount)RefundPayment($amount) with idempotency key
ReserveInventory(qty)ReleaseInventory(qty) + INCRBY Redis counter
SendConfirmationEmail()SendCancellationEmail() (best effort)

Irreversible steps (pivot transactions)

Once the warehouse ships, you cannot “compensate” with a DB rollback. In flash sales, keep ship / fulfill last, or use a pending state until payment and inventory both succeed. Email can stay best-effort.

Saga state and crash recovery

saga_idtypestepstatus
saga-abc123FlashCheckout3COMPENSATING
saga-def456FlashCheckout5COMPLETED
saga-ghi789FlashCheckout2RUNNING

Recovery after orchestrator crash

  1. On startup: load sagas where status is RUNNING or COMPENSATING.
  2. Resume from current_step (idempotent handlers make re-run safe).
  3. Re-publish stuck commands or continue compensation from replay queue.

Managed options (Temporal, Step Functions) persist this history for you. Custom orchestrators need the sagas table plus disciplined replay workers.

Outbox when a service crashes mid-publish

The dual-write trap

Order saved in Postgres, then crash before Kafka publish → orchestrator never hears OrderCreated → shopper stuck on “Processing…” forever.

sql
BEGIN;
  INSERT INTO orders (id, status) VALUES ('ord-99', 'PENDING');
  INSERT INTO outbox (id, topic, payload)
    VALUES ('evt-1', 'order-events', '{"type":"OrderCreated","orderId":"ord-99"}');
COMMIT;
-- Relay or CDC publishes evt-1; crash after COMMIT is OK

Flash sale runs this pattern in all three services. That is why the design shows dashed outbox arrows on the diagram. See on the distributed transactions note for polling vs CDC.

Timeouts and stuck sagas during the event

Per-step timeoutChargePayment: 30s. No reply → retry up to 3 times → then compensate. Shoppers still see “pending,” not success.
Overall saga timeoutFlashCheckout max 5 minutes. If not COMPLETED, force compensation on all finished forward steps.
Dead saga sweepBackground job: RUNNING and older than 1 hour → alert, auto-compensate, or move to DLQ for manual review.

What to watch during go-live

  • Metrics: saga_started, saga_completed, saga_failed, saga_compensated
  • One trace id = saga_id across gateway, orchestrator, and services
  • Alert if compensation rate spikes above ~5% (upstream inventory or PSP issue)

Traps to avoid

These mistakes show up fast under flash-sale load.

Non-idempotent refund

Retry compensation → double refund. Always key refunds by orderId + saga step.

Missing compensation mapping

Charge without Refund defined → money stuck. Enforce 1:1 forward/compensate in code review.

Showing “success” too early

UI green check before saga COMPLETED → angry users. Show pending until orchestrator confirms.

Skipping outbox

Publish then DB, or DB without outbox → lost events under load. Flash sale traffic exposes this fast.

Data model (what to store)

sagas

Support and ops read this first.

sql
id, order_id, user_id
state, current_step
compensation_status JSON
created_at, updated_at

orders

Business record for the shopper.

sql
id, user_id, status
total_amount, idempotency_key
created_at

outbox (×3 services)

Reliable bridge to the broker.

sql
id, aggregate_id
event_type, payload
published, created_at

inventory_reservations

Authoritative stock hold.

sql
sku_id, order_id, qty
status, expires_at, version

payments

Match PSP receipts.

sql
order_id, gateway_ref
amount, status, idempotency_key

processed_events

Dedupe at-least-once delivery.

sql
event_id PK
handler, processed_at

Admission control (do not skip this)

The difference between a busy site and a melted site.

Virtual waiting room

Users see queue position. Gateway only accepts checkout with a signed token that expires in minutes.

Rate limits

Per user and per IP on checkout. Stops bots from burning Redis and saga capacity.

Graceful degradation

Turn off recommendations and heavy cart features. Keep the buy button path warm.

Load shedding

Return 503 with Retry-After on non-critical APIs before the orchestrator intake queue explodes.

Hard part: no stock oversell

The fast counter protects latency, but the inventory database protects truth.

Invariant

Confirmed quantity for a SKU must never exceed sellable quantity: sum(confirmed) <= stock - safety_buffer.

Counter drift

If an illustrative 10,000 admitted checkouts/s creates one provisional counter reservation each, and 1% needs release, the compensation stream sees 100 releases/s. Five minutes without processing produces 30,000 pending corrections. That estimate depends on actual successful reservations, not raw clicks. Release records need deduplication, and restored workers need spare capacity beyond live arrivals to clear the backlog.

Fencing check

Inventory reserve uses WHERE available >= qty AND version = expected; stale workers cannot confirm old holds. Version checks prevent a worker from applying a decision based on stale inventory, but the version and quantity checks must execute in the same authoritative write. A hold should have its own identity, owner, status, and expiry. Confirmation consumes that exact active hold; expiry and cancellation transition it once. This prevents both overselling and duplicate release under retried saga messages.

Deep-dive drill

  1. Gateway decrements the fast stock counter and starts a saga, returning pending to the user.
  2. Payment succeeds, but inventory reserve fails because the authoritative row is already sold out.
  3. Orchestrator marks the saga failed, emits an idempotent refund command, and sends a restock correction only if the fast counter was decremented.
  4. Reconciliation compares initial_stock - confirmed - holds with the fast counter and writes bounded corrections.
  5. If drift exceeds a threshold, pause admission for the SKU and serve sold-out until inventory is reconciled.

Bottlenecks and how to talk about them

Hot SKU in Redis

Risk: One key sees 10K decrements per second.

Mitigation: Shard counters per SKU, coalesce on gateway, or split stock into N logical buckets.

Orchestrator DB

Risk: Every saga step writes state.

Mitigation: Partition by event_id, batch audit logs async, scale writers horizontally.

Payment gateway limits

Risk: PSP throttles you mid-sale.

Mitigation: Bounded parallel payment workers; honest “try again” UX.

Second-zero thundering herd

Risk: Everyone arrives at once.

Mitigation: Warm CDN and Redis, stagger admission waves, load-test replay queue before go-live.

When parts break (degraded modes)

Redis is downFall back to inventory service sync check only. Slow down admission so DB survives.
Broker lag is highPause new sagas. Show “high load” instead of silent spinners. Scale consumers.
Payment provider outageStop intake. Do not burn Redis stock for orders you cannot charge.
Compensation stuckMessages land in . Page on-call; reconcile payment processor vs order DB.

Production readiness

How to operate the sale without relying on luck.

SLOs and metrics

  • Admitted checkout p99 <500ms until pending response; order finalization p95 <2 min.
  • No confirmed oversell; saga success rate, compensation age, queue lag, counter drift, and payment timeout rate are primary dashboards.
  • Error budget is scoped to checkout, not cached browse pages.

Alerting

  • Page on oversell invariant breach, replay/DLQ growth, payment capture mismatch, or inventory reserve error spike.
  • Warn on queue wait estimate drift, CDN origin miss surge, hot-key saturation, and compensation retries.
  • Alert per sale event and SKU so one hero SKU does not hide systemic failure.

Runbook and degraded mode

  • Fast counter down: lower admission and use authoritative inventory only for a smaller buyer pool.
  • Broker lag high: pause new sagas, keep queue positions, and show "processing delayed" instead of accepting blind orders.
  • Payment outage: stop checkout intake for affected methods and keep browse/queue alive.

Security and threat model

  • Assets: admission tokens, payment intents, stock counters, inventory rows, and order status.
  • Actors: bots, token replay, compromised accounts, cart hoarding, and fake callbacks.
  • Controls: signed short-lived tokens, per-account/device limits, idempotency keys, callback signatures, and immutable audit logs.

Test plan

  • Load test second-zero arrival, hot SKU key pressure, worker scale-up, and queue wait accuracy.
  • Fault inject payment success plus inventory failure, lost outbox publish, duplicate broker delivery, and restock replay delay.
  • Run reconciliation tests that prove confirmed units never exceed initial stock.

Rollout, DR, and 10x scale

  • Dry run sale events with shadow traffic, prewarm cache/counters, and freeze risky deploys during the sale window.
  • DR uses event-level write ownership; do not multi-write one SKU across regions without quota partitioning.
  • At 10x: split stock into regional or logical buckets, isolate hero SKUs, preallocate dedicated worker pools, and move analytics off the checkout path.

Beginner vs advanced answers

Beginner (5 minutes)

CDN for reads, queue for overload, Redis to reject sold-out fast, then order → pay → stock with rollback if stock fails.

Advanced (deep dive)

The deeper design traces one checkout through admission, durable acceptance, authoritative reservation, payment outcome, confirmation, and compensation. Outbox publishing preserves recoverable step requests, while consumer deduplication and state versions reject repeated or stale replies. Reconciliation compares the order, inventory hold, payment provider reference, and provisional cache reservation. Monitor oldest pending saga age and repair progress so uncertain outcomes do not disappear behind a healthy request rate.

Choosing and explaining flash sale checkout

A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.

Explain the core idea

Separate “looking” from “buying”: cache browse, protect purchase with admission + saga.

Sync Redis only for a fast no; async saga for the real commit across three databases.

Outbox + orchestrator = every step auditable; compensation when payment and inventory disagree.

When it belongs

Traffic spikes 100× on a few SKUs for minutes, not hours.

They ask how you avoid overselling without one giant database transaction.

They want saga, queue, cache, and failure handling in one coherent story.

How it fails

Charged card, inventory says no, refund job stuck in replay queue.

Double checkout click starts two sagas (missing idempotency key).

Redis counter and DB quantity diverge after partial compensation.

Trade-offs to weigh

Orchestration vs choreography: orchestration is easier to explain and debug in interviews.

Redis pre-check vs saga-only: faster rejects, but you must restock Redis on undo.

Showing “pending” vs blocking until done: pending scales; blocking melts thread pools.

Common misunderstandings

Letting the mobile app call Redis stock keys directly.

Publishing to Kafka before the local database commit.

No waiting room on second zero of the sale.

Deeper walkthrough

Flash sale deeper walkthrough

A flash sale is inventory locking under a stampede: admit users fairly, reject sold-out in milliseconds, run a saga with outbox-backed services, and compensate when any step fails.

Requirement refinements

How many SKUs, max queue wait, refund SLA, single-SKU only or cart?

Invariants: no oversell on flash lines; one charge per idempotency key; saga state machine is auditable.

API design details

Checkout requires Idempotency-Key and admission token; all internal events carry saga_id.

Expose order status matching orchestrator states, not raw service logs.

Data model and storage

Orchestrator sagas table is ops source of truth; links order_id and idempotency_key.

Per-service outbox + processed_events for dedupe; Redis mirrors hot SKU availability.

Scaling strategy

Partition broker and saga shards by order_id; dedicated Redis shard per hero SKU.

Autoscale orchestrator on intake queue depth, not CPU alone.

Reliability, recovery, and operations

Replay queue with backoff and DLQ; nightly reconciliation: Redis vs inventory DB vs payment captures.

Dashboards: admission rate, redis reject %, saga success, compensation latency, broker lag.

Key takeaway

Draw the HLD, then tell a story: edge protects origin, queue protects purchase, Redis says no in one millisecond, runs the real checkout over Kafka with in every service, and on the replay queue fixes the messy middle when step three fails after step two succeeded. That is a complete flash-sale answer.

Primary sources

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Protect a scarce inventory invariant with admission, fair queueing, reservations, idempotency, bot controls, and reconciliation under extreme skew.

Explain the decision

Why is a distributed lock per purchase not the complete inventory design?

Read the reasoning

The authoritative conditional decrement/reservation and durable order linkage prove stock; locks add contention and can expire around stalled clients.

Apply it to a production scenario

One SKU has 10,000 units and 5 million buyers arrive in a minute. What is the path?

Read one approach

Edge admission and bot checks, fair virtual queue, bounded reservation service, conditional inventory updates, hold expiry, and honest sold-out state.

Work through the numbers

Five million arrivals/minute equals what average ingress QPS?

Read the worked calculation

About 83,333 QPS; only a capacity-sized fraction should reach scarce inventory or payment dependencies.

Reason through a failure

What is the stock invariant?

Read the recovery reasoning

Confirmed plus unexpired reserved units never exceeds available units, across retries, timeout ambiguity, expiry, cancellation, and recovery.