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.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you safely process concurrent bids without overselling or race conditions?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Use an optimistic-locking pattern: each auction row carries a version counter. A bid UPDATE increments the version and sets the new high bid only WHERE current_high_bid < new_bid AND version = known_version. Zero rows affected means a concurrent bid won the race, and the requester retries. For extreme throughput, a single-threaded bid queue per auction item serializes bids without database contention.”
}
},
{
“@type”: “Question”,
“name”: “What is anti-sniping extension and how is it implemented?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Anti-sniping extends the auction deadline when a bid arrives within a threshold window (e.g. the last 30 seconds). The server checks bid_time > auction_end – threshold; if true, it pushes auction_end forward by a fixed increment (e.g. 2 minutes) up to a configurable maximum. The new deadline is broadcast via WebSocket so all active bidders see the updated countdown immediately.”
}
},
{
“@type”: “Question”,
“name”: “How do you design WebSocket fanout for a live auction with thousands of watchers?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Clients subscribe to a per-auction channel. A pub/sub broker (Redis Pub/Sub or Kafka topic per auction) receives each bid event from the bid service. Edge WebSocket servers subscribe to the relevant channel and push updates to their connected clients. This decouples bid processing from delivery and lets WebSocket nodes scale horizontally independent of the auction logic tier.”
}
},
{
“@type”: “Question”,
“name”: “How is reserve price logic enforced during and after an auction?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The reserve price is stored server-side and never exposed to clients. Bids are accepted and displayed normally regardless of reserve. At auction close, if the winning bid is below the reserve, the auction is marked NO_SALE and the seller is notified. Optionally, a second-chance workflow emails the highest bidder with the option to purchase at the reserve price.”
}
}
]
}

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