Courses › System Design › Question Breakdowns
Design an Online Auction
Atomic bid acceptance, proxy bidding, live price fan-out, ending on time, anti-sniping and hot auctions.
An online auction (eBay-style) lets sellers list items with an end time and buyers place ever-higher bids, watching the current price update live. It's a question about contention on a single value (the highest bid), real-time fan-out to everyone watching, and time — especially the last few seconds, when most bids arrive.
Requirements
Functional
- Sellers can create an auction with a starting price and end time
- Buyers can place a bid; it's accepted only if it beats the current highest bid
- Watchers see the current highest bid update in real time
- When the auction ends, the highest bidder wins and both parties are notified
Out of scope: payment and shipping, search and recommendations, seller ratings.
Non-functional
- Strong consistency for bids: two bids can never both be accepted as the highest; the winner is unambiguous
- Durability: every accepted bid is recorded and auditable
- Low latency: bid result in < 200 ms; watchers see new bids in < 1 s
- Scale: ~10M active auctions, ~100M bids/day, hot auctions with 100k watchers and hundreds of bids in the final minute
- Fairness at the end: bids received before the end time count, bids after don't
[!TIP] The insight that frames the design Bids on one auction must be serialized, but different auctions are completely independent. So partition everything by auction ID and make each auction's bid acceptance a single atomic check — the system scales with the number of auctions, and the only hard case is one very hot auction.
Capacity estimates
Core Entities
- Auction — id, seller, item, start price, current highest bid, highest bidder, end time, status (active / ended), version
- Bid — id, auction, bidder, amount, (max proxy amount), timestamp, status (accepted / rejected)
- User — buyers and sellers
API
POST /v1/auctions { item, startPrice, endsAt } → { auctionId }
GET /v1/auctions/{id} → auction, current bid, endsAt (server time)
POST /v1/auctions/{id}/bids { amount, idempotencyKey }
→ 201 { accepted: true, currentBid: 120 } or 409 { accepted: false, currentBid: 130 }
GET /v1/auctions/{id}/stream (SSE) → bid updates, end-of-auction eventHigh-Level Design
user = client "Bidders &\nWatchers" @0,1
gw = service "API Gateway" @1,1
bid = service "Bid Service" @2,0
db = store "Auctions + Bids DB\n(sharded by auctionId)" @3,0
cdc = queue "Bid Events" @3,1
fan = service "Fan-out / SSE Servers" @2,2
closer = service "Auction Closer" @4,0
notif = service "Notifications" @4,1
user -> gw
gw -> bid
bid -> db : conditional update
db --> cdc : accepted bids
cdc -> fan
fan --> user : live bids
closer -> db : end auctions
closer -> notif
cdc -> notif- Bid: the bid service validates the bid and applies it with one conditional write on the auction row.
- Record: the bid (accepted or rejected) is stored in the same transaction for auditing.
- Publish: accepted bids are published (via an outbox or CDC) to a stream partitioned by auction ID.
- Fan out: SSE servers subscribed to that auction push the new price to watchers.
- Close: at the end time, the closer marks the auction ended and notifies winner and seller.
Deep Dives
1. Accepting bids without races
Two bidders send $120 and $125 at the same moment, both having seen $110.
UPDATE auctions
SET current_bid = :amount, high_bidder = :user, version = version + 1
WHERE id = :auction AND status = 'active' AND ends_at > now() AND current_bid < :amount;
-- 1 row updated → accepted; 0 rows → rejected, return the current bidThe same statement enforces "still active" and "before the end time." Insert the bid record in the same transaction. See Dealing with Contention.
2. Proxy (automatic) bidding
eBay-style bidding lets a user enter a maximum and have the system bid for them in increments.
- Store each bidder's max privately. The visible price is the second-highest max plus one increment.
- A new bid triggers resolution inside the same transaction: compare the new max with the current leader's max and set the visible price accordingly.
- This also reduces load: competing proxies are resolved in one step rather than many back-and-forth bids.
3. Real-time updates to watchers
- Watchers open an SSE connection (one-way updates are enough; bids go through the normal API). See Real-time Updates.
- SSE servers subscribe to the auction's topic in a pub/sub layer (Redis pub/sub or Kafka partitioned by auction ID) and push to local connections.
- For an auction with 100k watchers, spread its watchers across many SSE servers; each receives the update once and fans out locally. Coalesce updates — send at most a few per second with the latest price.
- On reconnect, the client fetches the current state, so a missed update never leaves a stale price.
4. Ending the auction exactly on time
[!WARNING] Don't rely on a cron job to reject late bids If ending depends on a job that runs "around" the end time, bids can slip in after it. The bid's own conditional check (ends_at > now() using the database's clock) is what enforces the deadline. The closer only finalizes.
- The closer uses a time-indexed queue of upcoming end times (a sorted set or a delayed queue) and marks auctions
endedwithin a second or two of their end time. - Finalization is idempotent: set status
endedwith a conditional write, then create the order and notify both parties. - Show the countdown using server time sent with the auction, not the device clock.
5. Sniping and soft endings
Most bids arrive in the last seconds ("sniping"). Options:
- Hard end (eBay): simple and predictable; proxy bidding makes sniping less effective.
- Soft end / anti-sniping (many auction sites): any bid in the last N minutes extends
ends_atby N minutes. Implement it in the same conditional update (SET ends_at = GREATEST(ends_at, now() + interval '2 minutes')) and publish the new end time to watchers.
6. The hot auction
Hundreds of bids per second on one row is fine for a single database primary. Beyond that:
- Put a per-auction queue partition in front of the auction and have a single consumer apply bids in order; the bidder gets the result asynchronously over their SSE connection.
- Or keep the live auction state in Redis and accept bids with a Lua script that checks and sets atomically, writing accepted bids durably through a log. The database is updated asynchronously, and on Redis failover the state is rebuilt from the log.
What's expected at each level
- Mid-level: a correct bid flow without races, storage for auctions and bids, and some real-time update mechanism.
- Senior: atomic conditional updates or serialization per auction, SSE fan-out with pub/sub, a correct ending mechanism, and idempotent bids.
- Staff: proxy bidding, anti-sniping, hot auctions beyond a single row's throughput, fairness and clock concerns at the deadline, and auditability.