What Is an Online Auction System?
An online auction system allows sellers to list items and buyers to place competitive bids, with the highest bid winning when the auction closes. The core challenges are: handling concurrent bids without race conditions, preventing bid sniping (last-second bids), ensuring bid ordering fairness, and scaling to millions of simultaneous auctions (eBay hosts ~1.5 billion listings).
System Requirements
Functional
- Create auctions with start/end time, reserve price, and item description
- Place bids — must be higher than current highest bid
- View current highest bid in real time
- Notify bidders on being outbid
- Automatic proxy bidding (system bids up to user’s max automatically)
- Auction close: winner notification, payment processing, seller notification
Non-Functional
- Bid acceptance must be atomic — no two bids accepted simultaneously at same amount
- Read-heavy: thousands of viewers per active auction, few bidders
- Low latency bid submission feedback (<200ms)
- High availability — losing a bid due to system downtime is unacceptable
Core Data Model
auctions: id, seller_id, item_id, start_time, end_time, reserve_price, status, current_bid, current_bidder_id, version
bids: id, auction_id, bidder_id, amount, proxy_max, created_at, status (winning/outbid/rejected)
items: id, title, description, images
Handling Concurrent Bids — The Critical Problem
Two users submit bids simultaneously. Without coordination, both could read the same current_bid and both believe their bid is valid. Solution: optimistic locking.
-- Read current state
SELECT current_bid, version FROM auctions WHERE id = ?
-- Attempt to update only if version hasn't changed
UPDATE auctions
SET current_bid = ?, current_bidder_id = ?, version = version + 1
WHERE id = ? AND version = ? AND current_bid < ?
-- If 0 rows updated: another bid won the race — return "outbid" to user
The version column implements optimistic concurrency control (OCC). Only one concurrent bid wins; others are rejected and must resubmit. This avoids expensive row-level locks while preventing lost updates. For very high-bid-rate auctions (celebrity items), use a Redis-based atomic counter: SET auction:123:bid NX with Lua scripting to atomically check-and-update.
Proxy Bidding
eBay’s “automatic bidding”: a user sets their maximum price; the system bids the minimum necessary to keep them winning. Implementation:
- Store proxy_max in the bids table (encrypted at rest — never show max to others)
- When a new bid arrives: compare against current proxy max. If new bid < current proxy max, automatically outbid the new bidder by the minimum increment.
- If new bid > current proxy max, the new bidder wins; notify the previous proxy bidder they’ve been outbid.
All proxy bid logic runs within a database transaction to ensure atomicity.
Real-Time Bid Updates
Auction pages must show live bid updates to all viewers. WebSocket connections are ideal: establish a persistent connection when the user opens the auction page, push bid updates as they happen.
- Pub/sub: bid service publishes to Redis channel
auction:{id}:bids. WebSocket servers subscribe; fan out to connected clients. - Scaling WebSockets: WebSocket servers are stateless (subscribe to Redis, maintain client connections). Scale horizontally — route users to any server, all receive the same Redis messages.
- Fallback: clients poll every 5 seconds if WebSocket disconnects. Acceptable degradation.
Auction Close and Sniping Prevention
Bid sniping: bidding in the last few seconds to prevent others from responding. Two approaches:
- Automatic extension: if a bid is placed within the last N minutes (e.g., 5 minutes), extend the auction end time by N minutes. Used by eBay for some categories. Keeps extending until no bids in the final window — ensures fair competition.
- Hard close: auction ends at exactly the specified time. Simpler, but bidders complain about sniping. Acceptable for one-sided marketplaces.
Scheduled close: a cron job or delayed queue scans for auctions ending in the next minute, marks them closed, triggers winner notification, and initiates payment. Use a Kafka topic auction-closures with scheduled messages (Kafka’s delayed message pattern) or a job scheduler like Celery beat.
Read Scaling — Auction Pages
Active auction pages are read-heavy (thousands of viewers, few bidders). Cache aggressively:
- Auction metadata (title, images, seller info): CDN cache with long TTL (1 hour)
- Current highest bid: Redis with 1-second TTL, updated on every accepted bid. Serves 99% of reads.
- Bid history: paginated, cached in Redis with 30-second TTL. Acceptable staleness for history.
Payment and Fulfillment
When auction closes: initiate a payment hold (not charge) on the winner’s payment method. Payment captured after seller confirms shipment. Use Stripe payment intents (hold → capture pattern) or equivalent. Saga pattern for compensating transactions if fulfillment fails: release payment hold, re-list item or contact seller.
Interview Tips
- The bid atomicity problem is the core of this question — walk through optimistic locking carefully.
- Distinguish the read path (millions of viewers, aggressive caching) from the write path (few bidders, strict consistency).
- Proxy bidding is a differentiator — it shows you understand the product, not just the infrastructure.
- Mention bid sniping prevention — interviewers reward candidates who think about user experience edge cases.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you prevent race conditions when two users bid simultaneously?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Use optimistic concurrency control (OCC) with a version column. Read the auction's current_bid and version. Attempt: UPDATE auctions SET current_bid=?, version=version+1 WHERE id=? AND version=? AND current_bid < ?. If 0 rows updated, another bid won the race — return "outbid" to the user who lost. This avoids row-level locks (which would serialize all bidders on a lock) while preventing lost updates. For extremely popular items with high bid rates, use Redis Lua scripting: EVAL a script that atomically reads the current bid and updates it only if the new bid is higher — sub-millisecond latency.” }
},
{
“@type”: “Question”,
“name”: “What is proxy bidding and how do you implement it?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Proxy bidding (eBay's automatic bidding): a user sets their maximum price; the system automatically bids the minimum necessary to keep them winning, up to their max. Implementation: store proxy_max (encrypted) in the bids table alongside bid_amount. When a new bid arrives: (1) compare against current proxy_max. (2) If new_bid < proxy_max, automatically place a counter-bid at new_bid + min_increment and notify the new bidder they've been outbid. (3) If new_bid >= proxy_max, the new bidder wins; notify the proxy bidder they've been outbid. All proxy logic runs within a single database transaction for atomicity. Never expose proxy_max to other users — it's sensitive competitive information.” }
},
{
“@type”: “Question”,
“name”: “How do you scale an auction system to handle millions of concurrent viewers?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Separate the read path from the write path. The write path (bids) is low volume but requires strong consistency — hits the primary database with optimistic locking. The read path (viewing current bid, bid history) is high volume but tolerates slight staleness. Read path: cache current highest bid in Redis with 1-second TTL, updated synchronously on every accepted bid. Serve 99%+ of reads from Redis. For real-time updates: use WebSockets — when a bid is accepted, publish to Redis pub/sub channel auction:{id}. WebSocket servers subscribe and push updates to all connected viewers instantly. For auction metadata (title, photos, seller info): CDN cache with long TTL. This architecture handles thousands of concurrent viewers with minimal database load.” }
}
]
}