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
- Check that the bidder is verified (has a payment method), the auction is live, and it's not the bidder's own auction.
- Atomically: accept only if
amount >= highest_amount + min_incrementand the auction hasn't ended. Updatehighest_amount,highest_bidder_idandversion, and append the bid to the log. - Publish
{ highest, bid_count }to the auction's channel. Watchers see it within a second. - Notify the previous highest bidder that they were outbid.
4.2 Closing
- The closer has a timer per auction (a delayed queue or sorted set by
ends_at). - At
ends_at, it setsstatus = closing. From now on, the atomic bid check rejects new bids, because it checks both status and time. - 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
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Bid ordering | Single writer per auction | No lost updates, no retries under contention | Optimistic locking: fine for quiet auctions |
| Durability | Append bid log before reply | Audit trail, recoverable state | Update only the current max: no history |
| Live updates | Pub/sub + coalesced pushes | Scales to 1M watchers | Push every bid to everyone: floods clients |
| End of auction | Server-side closer + anti-sniping | Fair, predictable | Hard 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.