Live Auction System Low-Level Design: Bid Processing, Real-Time Updates, and Reserve Price Logic

Problem Overview

A live auction system allows bidders to compete in real time, with each bid potentially triggering updates for hundreds of concurrent viewers. The core difficulty is handling a burst of bids at the moment a high-value item is closing, enforcing strict ordering of bids, and preventing fraudulent double-bids — all while pushing state updates to clients with sub-second latency.

Requirements and Constraints

Functional Requirements

  • Bidders submit bids on active auction items
  • Each valid bid becomes the new “current price” if it exceeds the previous high bid
  • All connected clients receive real-time updates when the price changes
  • Auctions have a hard end time; anti-sniping extends the auction if a bid arrives in the last N seconds
  • Reserve price logic: seller sets a hidden minimum; auction only completes if reserve is met
  • Bid history is fully auditable

Non-Functional Requirements

  • Bid acceptance latency under 100 ms p99
  • Support 10,000 concurrent viewers per auction item
  • Handle 500 bids/second per item during closing surge
  • Exactly-once bid processing (no duplicate bid credits)
  • Zero tolerance for accepting a bid below the current high bid

Core Data Model

CREATE TABLE auctions (
  auction_id      BIGINT PRIMARY KEY AUTO_INCREMENT,
  item_id         BIGINT NOT NULL,
  seller_id       BIGINT NOT NULL,
  start_price     DECIMAL(12,2) NOT NULL,
  reserve_price   DECIMAL(12,2),          -- hidden from bidders
  current_price   DECIMAL(12,2) NOT NULL,
  current_winner  BIGINT,
  end_time        TIMESTAMP NOT NULL,
  status          ENUM('active','closed','cancelled') DEFAULT 'active',
  anti_snipe_sec  INT NOT NULL DEFAULT 60,
  version         BIGINT NOT NULL DEFAULT 0  -- optimistic lock
);

CREATE TABLE bids (
  bid_id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  auction_id      BIGINT NOT NULL,
  bidder_id       BIGINT NOT NULL,
  amount          DECIMAL(12,2) NOT NULL,
  placed_at       TIMESTAMP(3) NOT NULL DEFAULT NOW(3),
  status          ENUM('accepted','rejected','cancelled') NOT NULL,
  idempotency_key VARCHAR(64) UNIQUE NOT NULL,
  INDEX idx_auction_amount (auction_id, amount DESC)
);

Key Algorithms and Logic

Bid Processing with Optimistic Concurrency Control

The bid acceptance path uses optimistic locking on the auctions table to prevent lost updates without holding a long lock:

  1. Read the current auction row: current_price, version, end_time, status.
  2. Validate: auction is active, end_time is in the future, bid.amount > current_price, bidder has sufficient funds on hold.
  3. Execute: UPDATE auctions SET current_price=?, current_winner=?, version=version+1, end_time=CASE WHEN end_time - NOW() < anti_snipe_sec THEN end_time + INTERVAL anti_snipe_sec SECOND ELSE end_time END WHERE auction_id=? AND version=?
  4. If 0 rows affected: a concurrent bid won the race. Re-read and retry up to 3 times; if still failing, reject with “outbid.”
  5. Insert the bid record with idempotency_key to prevent duplicate submissions.

This avoids SELECT FOR UPDATE while still preventing double-acceptance. Under very high contention, switch the hot row to a Redis-based compare-and-swap using WATCH / MULTI / EXEC on a Redis hash, and flush to the DB asynchronously via a dedicated writer thread.

Anti-Sniping

When a bid arrives within anti_snipe_sec of the auction's end_time, the UPDATE extends end_time by that interval. This is atomic with the bid acceptance in a single UPDATE statement, so there is no race between checking the time and extending it. The extension is capped at a maximum total extension (e.g., 30 minutes) to prevent infinite auctions.

Reserve Price Logic

The reserve_price column is never exposed via the API. At auction close, the system compares current_price against reserve_price. If current_price < reserve_price, the auction status is set to “reserve_not_met” and no transaction is initiated. Bidders may see a UI indicator “Reserve not met” without knowing the reserve value.

Real-Time Update Architecture

Clients connect via WebSocket. A bid acceptance service publishes a bid_accepted event to a Redis Pub/Sub channel keyed by auction_id. A WebSocket gateway service subscribes to the channel and fans out the event to all connected clients for that auction. The gateway is stateless; sticky sessions (via load balancer cookie) are not required because subscriptions are per-auction, not per-server.

For 10,000 concurrent viewers on a single item, each WebSocket gateway node can handle ~5,000 connections. Two gateway nodes per popular auction provide redundancy. The fanout is O(subscribers) per bid event — acceptable because bid events are rare compared to connection counts.

API Design

POST /v1/auctions/{auction_id}/bids
Body: { "bidder_id": "...", "amount": 1250.00, "idempotency_key": "uuid" }
Response 200: { "bid_id": 9821, "status": "accepted", "new_price": 1250.00, "end_time": "..." }
Response 409: { "status": "rejected", "reason": "outbid", "current_price": 1275.00 }

GET /v1/auctions/{auction_id}
Response: { "auction_id": ..., "current_price": 1250.00, "end_time": "...", "bid_count": 47 }

WebSocket: wss://live.example.com/auctions/{auction_id}
Server push: { "event": "bid_accepted", "new_price": 1250.00, "end_time": "...", "bid_count": 47 }

Scalability Considerations

  • Read fan-out: Auction detail reads are served from a Redis cache invalidated on each bid_accepted event. This offloads the DB entirely for viewers.
  • Write path isolation: Route all writes for a single auction_id to the same DB shard (auction_id % N shards) to avoid cross-shard transactions.
  • Fund hold service: Pre-authorize bidder funds before allowing bidding. Use a separate balance service with a reserved_amount field updated atomically. This prevents bid acceptance for bidders without sufficient balance without coupling to payment processing on the hot path.

Failure Modes and Mitigations

  • Duplicate bid submission: idempotency_key unique constraint causes the second INSERT to fail; the service returns the original bid result.
  • Gateway crash mid-auction: Clients reconnect; on reconnect, the server sends the current auction state immediately. No bids are lost because bids go through the bid service, not the gateway.
  • Clock skew: end_time comparisons happen on the DB server using NOW() inside the UPDATE, not on the application server, ensuring consistency regardless of application clock drift.

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Shopify Interview Guide

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

Scroll to Top