CASE STUDY

Real-Time Online Auction (eBay / Live Auctions)

6 min read·1,122 words·Advanced

How to use this case study

SDE-2 / Mid

Explain the bid API, how the current highest bid is checked and updated safely, and how viewers see new bids live.

SDE-3 / Senior

Go deeper on concurrency control for bids (conditional writes or a single writer per auction), the final-seconds spike, closing the auction exactly on time, and anti-sniping.

Staff / Principal

Discuss hot auctions with millions of watchers, multi-region fairness, payment after winning, fraud, and auditability of every bid.


0) Problem Restatement

Design an online auction system. Sellers list items with a starting price and an end time. Buyers place bids, and every bid must be higher than the current highest bid. Everyone watching sees the current highest bid and the bid history update live. When time runs out, the highest valid bid wins and the winner pays.

Variants include eBay-style auctions (asked at Meta), auctions attached to a social media post (Meta, Instagram), and high-traffic live auctions (TikTok). The hard parts are many bids at once (especially in the last seconds) and closing fairly and exactly on time.

Asked at: Meta, TikTok — 4 candidate reports between Oct 2025 and Apr 2026.

1) Requirements

1.1 Functional

  • Create an auction (item, start price, minimum increment, end time).
  • Place bids, which are accepted only if higher than the current bid plus the increment.
  • See live updates of the highest bid and bid history.
  • Close the auction at the end time, pick the winner, and start payment.
  • Optional: proxy (automatic) bidding up to a max, and anti-sniping extensions.

1.2 Non-Functional

  • Correctness: never accept a lower bid over a higher one, and never lose an accepted bid.
  • Low latency: bid result in under ~200 ms, and viewers updated within ~1 second.
  • Spiky load: popular auctions get most of their bids in the final 30 seconds.
  • Auditable: every bid recorded with a timestamp.

1.3 Scale Estimates

  • 10M active auctions, 50M bids/day ≈ 600 bids/sec on average.
  • A hot auction: 5,000 bids/sec in its last seconds, and 1M watchers.
  • Bid records are small (~100 bytes), so storage is easy. Contention on one auction is the challenge.

1.4 API Design

  • POST /v1/auctions { item_id, start_price, min_increment, ends_at }
  • POST /v1/auctions/{id}/bids (Idempotency-Key) { amount }{ accepted: true, highest: 12500 } or { accepted: false, reason: "outbid", highest: 13000 }
  • WebSocket /v1/auctions/{id}/live{ highest, bidder_alias, bid_count, ends_at }


2) High-Level Architecture

2.1 Overview

  • Auction Service: create and read auctions (catalog, search).
  • Bid Service: validates and accepts bids. All bids for one auction are processed in order by one owner (see Deep Dive A).
  • Bid store: auction state (highest bid, version) plus an append-only bid log.
  • Real-time fan-out: pub/sub channel per auction → WebSocket servers → watchers.
  • Closer: ends auctions at their end time and triggers winner payment.
  • Payment Service: charges the winner, with a fallback to the next bidder if payment fails.

2.2 Architecture Diagram

Architecture Diagram

flowchart LR
    B["Bidders"] --> GW["API Gateway"]
    GW --> BS["Bid Service - owner per auction"]
    BS --> ST[("Auction state + bid log")]
    BS --> PS[("Pub/Sub - channel per auction")]
    PS --> WS["WebSocket servers"]
    WS --> W["Watchers"]
    CL["Auction Closer - timer"] --> ST
    CL --> PAY["Payment Service"]
    PAY --> N["Notify winner / seller"]

3) Data Model

auctions:  auction_id, seller_id, item_id, start_price, min_increment, starts_at, ends_at,
           status (scheduled, live, closing, closed), highest_amount, highest_bidder_id, version
bids:      auction_id, bid_id, bidder_id, amount, server_ts, status (accepted/rejected)   -- append-only

4) Key Flows

4.1 Placing a bid

  1. Check that the bidder is verified (has a payment method), the auction is live, and it's not the bidder's own auction.
  2. Atomically: accept only if amount >= highest_amount + min_increment and the auction hasn't ended. Update highest_amount, highest_bidder_id and version, and append the bid to the log.
  3. Publish { highest, bid_count } to the auction's channel. Watchers see it within a second.
  4. Notify the previous highest bidder that they were outbid.

4.2 Closing

  1. The closer has a timer per auction (a delayed queue or sorted set by ends_at).
  2. At ends_at, it sets status = closing. From now on, the atomic bid check rejects new bids, because it checks both status and time.
  3. It reads the final highest bid, sets closed, records the winner and starts payment.


5) Deep Dive A — Concurrency on a hot auction

Thousands of bids per second on one auction all want to update the same row.

  • Option 1: conditional write (optimistic locking): UPDATE auctions SET highest=?, version=version+1 WHERE id=? AND version=? AND highest < ?. It's simple, but under heavy contention many requests retry.
  • Option 2: single writer per auction (our choice for hot auctions): route every bid for an auction to one partition (e.g., a Kafka partition, or an in-memory actor on one server). It processes bids one at a time in arrival order, so no locks or retries are needed. It keeps state in memory and writes the bid log durably before replying. One partition easily handles thousands of simple comparisons per second.
  • Fairness in ordering: the server's receive time decides. Two bids with the same amount → the first one received wins.


6) Deep Dive B — The final seconds

  • Anti-sniping: if a bid arrives in the last 30 seconds, extend the end time by 30 seconds. This removes the incentive to bid at the last millisecond and reduces spikes.
  • Clock authority: only the server's clock decides whether a bid was on time. Clients show a countdown but never decide.
  • Fan-out at 1M watchers: two-level pub/sub (as in live comments). Updates are coalesced: send the latest highest bid at most every 200–500 ms rather than every bid.
  • Payment failure: if the winner's payment fails, offer the item to the second-highest bidder (at their bid), and flag the non-paying winner.


7) Trade-offs & Alternatives

DecisionChoiceWhyAlternative
Bid orderingSingle writer per auctionNo lost updates, no retries under contentionOptimistic locking: fine for quiet auctions
DurabilityAppend bid log before replyAudit trail, recoverable stateUpdate only the current max: no history
Live updatesPub/sub + coalesced pushesScales to 1M watchersPush every bid to everyone: floods clients
End of auctionServer-side closer + anti-snipingFair, predictableHard stop: sniping wars and spikes

8) Common Follow-up Questions

  • "Proxy bidding?" Store each bidder's secret max. On a new bid, the system automatically raises the leader's visible bid to just above the challenger, up to their max. The single writer makes this easy.
  • "What if the bid owner server crashes?" A new owner rebuilds the state from the durable bid log before accepting new bids.
  • "Search and browse?" A separate search index of live auctions, updated from auction events, sorted by ending soon or price.


9) Wrap-Up

Process all bids for an auction through one ordered owner (or optimistic conditional writes when traffic is low), accept a bid only if it beats the current highest by the increment and the auction is still open by server time, and append every bid to a durable log. Push coalesced updates through per-auction pub/sub to WebSocket watchers, close auctions with a server-side timer plus anti-sniping extensions, and fall back to the next bidder if payment fails.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →