Online Auction System Low-Level Design

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.

Scroll to Top