Requirements
- Users can create auction listings with a start price, reserve price, and end time
- Users place bids; highest bid wins when auction ends
- Auto-bidding (proxy bidding): user sets maximum bid; system bids automatically up to that max
- Real-time bid notifications to all watchers
- Prevent race conditions: two users bidding simultaneously — only one wins
- 1M concurrent auctions, 10K bids/second
Data Model
Auction(auction_id UUID, seller_id, title, description, start_price DECIMAL,
reserve_price DECIMAL, current_price DECIMAL, current_winner_id,
start_time, end_time, status ENUM(PENDING,ACTIVE,ENDED,CANCELLED),
bid_count INT)
Bid(bid_id UUID, auction_id, bidder_id, amount DECIMAL, bid_type ENUM(MANUAL,AUTO),
proxy_max DECIMAL, placed_at, status ENUM(WINNING,OUTBID,INVALID))
AuctionWatcher(auction_id, user_id, notify_on ENUM(NEW_BID,OUTBID,ENDING_SOON))
Bid Placement (Preventing Race Conditions)
Two users bidding at the same millisecond. Without locking, both could see current_price = $50 and both place bids at $60, creating two “winning” bids.
def place_bid(auction_id, bidder_id, amount):
# Optimistic locking with version check
BEGIN TRANSACTION
SELECT auction_id, current_price, current_winner_id, version
FROM Auction WHERE auction_id = :id FOR UPDATE
if auction.status != 'ACTIVE':
ROLLBACK; raise AuctionNotActive
if now() > auction.end_time:
ROLLBACK; raise AuctionEnded
if amount <= auction.current_price:
ROLLBACK; raise BidTooLow(min_bid=auction.current_price + 0.01)
UPDATE Auction SET current_price = :amount,
current_winner_id = :bidder_id, bid_count = bid_count + 1,
version = version + 1 WHERE auction_id = :id
INSERT INTO Bid(auction_id, bidder_id, amount, status) VALUES (...)
# Mark previous winning bid as OUTBID
UPDATE Bid SET status = 'OUTBID' WHERE auction_id = :id
AND status = 'WINNING' AND bid_id != :new_bid_id
COMMIT
publish_bid_event(auction_id, amount, bidder_id)
The SELECT FOR UPDATE serializes concurrent bids on the same auction. Only one transaction holds the lock at a time; the second waits, then sees the updated current_price and fails the amount check if it bid the same amount.
Auto-Bidding (Proxy Bidding)
User sets a maximum proxy bid. System bids on their behalf, incrementing by the minimum bid step each time they are outbid, up to their max. Logic: when a new manual bid comes in at amount A, check if any proxy bid has proxy_max > A. If yes: outbid amount = min(proxy_max, A + bid_step). The proxy bidder wins at the minimum amount needed to beat the manual bidder. Store proxy_max encrypted in the Bid table; never expose it to other users. Process proxy bids synchronously within the same transaction as the incoming bid.
Real-time Notifications
On each bid event published to Kafka: the notification service consumes and pushes to all watchers via WebSocket (for web clients) or APNs/FCM (for mobile). WebSocket server maintains a mapping of auction_id → [connected client WebSocket handles]. On bid event: broadcast to all handles for that auction_id. Scale WebSocket servers horizontally: use Redis Pub/Sub to fan out bid events across WebSocket server instances. Each WebSocket server subscribes to Redis channel auction:{auction_id} for auctions its clients are watching.
Auction Ending
Auction end is time-based. Options: (1) Cron job that runs every second, queries auctions WHERE end_time <= NOW() AND status='ACTIVE', marks them ENDED, triggers winner notification. At 1M auctions, this query is expensive. (2) Delayed job queue (preferred): when an auction is created, schedule a job at end_time using a delayed queue (Redis ZADD with score=end_time, a worker polls ZRANGEBYSCORE). On job fire: mark auction ENDED, charge winner's payment method, notify seller and winner. (3) Soft ending: an auction is only ended when queried after its end_time — lazy evaluation. Simpler but notifications are delayed until someone looks.
Sniping Prevention
Auction sniping: a bid placed in the last second. Solution: soft closing — if a bid is placed within the last 5 minutes of an auction, extend the end_time by 5 minutes. Update end_time in the DB; push updated end_time to all watchers. This mirrors how eBay-style auctions work.
Key Design Decisions
- SELECT FOR UPDATE on auction row — serializes concurrent bids, prevents race conditions
- Proxy bidding processed in same transaction as incoming bid — atomic, consistent
- Delayed job queue for auction ending — avoids expensive cron scan at scale
- Redis Pub/Sub for WebSocket fan-out — decouples bid events from WebSocket server count
- Soft closing (end time extension) — prevents last-second sniping
Shopify system design covers e-commerce and transaction systems. See common questions for Shopify interview: auction and e-commerce system design.
Stripe system design covers payment flows for auction and marketplace systems. Review patterns for Stripe interview: payment processing for auction systems.
Coinbase system design covers order matching and real-time bidding. See design patterns for Coinbase interview: real-time bidding and order matching design.