Core Entities
Auction: auction_id, seller_id, item_id, title, description, start_price, reserve_price (nullable, minimum price to sell), buy_now_price (nullable, instant purchase), start_time, end_time, status (PENDING, ACTIVE, ENDED, CANCELLED), current_bid, current_winner_id, bid_count. Item: item_id, seller_id, category, condition (NEW, USED, REFURBISHED), photos (JSON array), description. Bid: bid_id, auction_id, bidder_id, amount, placed_at, is_proxy (auto-bid), max_proxy_amount (for proxy bids). AuctionWatch: auction_id, user_id (users watching the auction for notifications).
Bid Placement with Concurrency
class AuctionService:
def place_bid(self, auction_id: int, bidder_id: int,
amount: Decimal, max_proxy: Decimal = None) -> Bid:
with self.db.transaction():
auction = self.db.lock_auction(auction_id) # SELECT FOR UPDATE
if auction.status != AuctionStatus.ACTIVE:
raise AuctionNotActiveError()
if datetime.now() > auction.end_time:
raise AuctionEndedError()
if amount = amount:
# Proxy auto-bids: counter with the minimum needed
counter = min(proxy.max_proxy_amount, amount + MIN_INCREMENT)
self.db.update_auction_bid(auction_id, counter, auction.current_winner_id)
bid = Bid(auction_id=auction_id, bidder_id=bidder_id,
amount=amount, is_proxy=False)
self.db.save(bid)
self.notify_outbid(bidder_id, auction_id, counter)
return bid
# New winning bid
self.db.update_auction(auction_id, current_bid=amount, current_winner_id=bidder_id)
if max_proxy:
self.db.save_proxy_bid(auction_id, bidder_id, max_proxy)
bid = Bid(auction_id=auction_id, bidder_id=bidder_id,
amount=amount, is_proxy=False, max_proxy_amount=max_proxy)
self.db.save(bid)
if auction.current_winner_id and auction.current_winner_id != bidder_id:
self.notify_outbid(auction.current_winner_id, auction_id, amount)
return bid
Proxy Bidding (Automatic Bidding)
Proxy bidding lets users set a maximum bid without revealing it. The system automatically increments their bid by the minimum increment when outbid, up to their maximum. Example: current bid is $10, increment is $1. User A sets a proxy max of $50. User B bids $15 — proxy auto-counters with $16 (just enough to beat). User B bids $55 — exceeds A’s max of $50, so B wins at $51. The proxy bid maximum is stored encrypted or hashed in the Bid record. Only the auction service reads it to compute proxy counters. Never expose another bidder’s proxy max to any user — it is confidential strategic information.
Auction End and Sniping Prevention
Sniping: placing a bid in the last seconds before auction end to win without giving others a chance to respond. Prevention: extend the auction end time by N minutes (e.g., 5 minutes) whenever a bid is placed in the last N minutes. This gives other bidders a fair chance to respond. Track extended_end_time separately from original end_time. The auction ends when extended_end_time is reached with no bids in the last N minutes.
Auction end processing: a scheduler fires at the auction’s end_time. Check if no bid was placed in the last N minutes (use extended_end_time). If auction.current_bid = reserve_price: auction succeeds. Notify the winner and seller. Create an order record. Process payment (charge the winner’s card via payment gateway). If payment fails: move to the next-highest bidder. Update auction status to ENDED.
Real-Time Bid Updates
All watchers and current participants need to see the current highest bid in real time. WebSocket for active bidders (users with the auction page open). When a new bid is placed: publish a bid event to Kafka, partitioned by auction_id. A broadcast service subscribes and pushes the update to all WebSocket connections watching that auction. Push notifications for outbid users (FCM/APNs). Email digest for watched auctions (one email per auction, not per bid — rate-limited). WebSocket server: each server holds a set of auction_id → {connection1, connection2}. When a bid event arrives for auction X, broadcast to all connections watching X. Use a pub/sub Redis channel per auction_id for multi-server setups.
Fraud Prevention
Shill bidding: the seller creates fake accounts to drive up the price. Detection: if a bidder account was created recently ( 50% of auctions fail to meet reserve.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does proxy bidding work and why is the maximum bid kept secret?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Proxy bidding lets users set a maximum bid they are willing to pay without revealing it. The system bids automatically on their behalf, incrementing just enough to stay in the lead, up to their maximum. Example: current price is $20, minimum increment is $1, user sets proxy max of $100. When another user bids $30: proxy automatically counters with $31. When someone bids $105: proxy loses (exceeds $100 max) — new winner pays $101. The proxy maximum is kept secret to prevent other bidders from strategically bidding just above it. It is stored server-side, encrypted, and never exposed in API responses. Only the auction engine reads it to compute counter-bids. Revealing the max would allow snipers to bid exactly $1 over your limit, eliminating the benefit of proxy bidding.”
}
},
{
“@type”: “Question”,
“name”: “How does sniping prevention work with auction time extension?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Sniping is placing a last-second bid that the current leader cannot respond to in time. Prevention: extend the auction end time whenever a bid is placed within a buffer window (e.g., 5 minutes of the scheduled end). If a bid arrives at T-3min, the auction now ends at T+5min (original end + buffer). If another bid arrives at T+3min, extend again to T+8min. The auction only ends when the buffer window passes with no new bids. This guarantees all bidders have at least 5 minutes to respond to any new bid. Track extended_end_time separately from the original end_time (for transparency — show users when the auction was extended and why). Most auction platforms (eBay calls it “buy-it-now” cutoff, others use similar mechanisms) use some form of this pattern.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle concurrent bids at auction close to prevent race conditions?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Two bidders may submit bids simultaneously as the auction ends. Without locking: both read the current_bid as $100, both compute their new bid as valid, both update to their bid amounts — one overwrites the other. Solution: SELECT … FOR UPDATE on the auction record at the start of the bid transaction. This row-level lock serializes all bid placements for a given auction. Only one bid transaction proceeds at a time; others wait. The lock is held for the duration of the transaction (validation + update + ledger entry). Transaction duration should be short (< 50ms) to minimize lock contention. For extremely high bid rates (flash sales): use a Redis-based distributed lock with a TTL as a pre-gate before hitting the database, to shed excess load."
}
},
{
"@type": "Question",
"name": "How do you process auction end and determine the winner reliably?",
"acceptedAnswer": {
"@type": "Answer",
"text": "A scheduler job fires at the auction's extended_end_time. Use a distributed lock (Redis SETNX auction_end:{auction_id}) to ensure only one worker processes the auction end even if the scheduler fires twice (clock skew, retry). Processing: load the auction and all bids within the transaction. Verify no new bids arrived after the end_time (recheck with a window). Determine the winner: highest bid. Check reserve price: if current_bid < reserve_price, the auction fails. On success: create an Order record, charge the winner's payment method. On payment failure: move to the second-highest bidder. Notify all parties (winner, seller, watchers). Update auction status to ENDED. The idempotency lock ensures this end-processing logic runs exactly once even under scheduler retry."
}
},
{
"@type": "Question",
"name": "How do you scale an auction platform to handle thousands of concurrent auctions?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Each auction is largely independent — horizontal scaling is natural. Partition auction data by auction_id in the database (or use a document store per auction). The hottest auctions (ending soon, celebrity items) attract the most concurrent bids. Rate limiting on bid submission: max N bids per user per auction per minute to prevent DoS. Connection server: thousands of users may have the auction page open watching live bid updates. Use a WebSocket connection server architecture (similar to the chat application design): each WebSocket server subscribes to a per-auction Redis pub/sub channel. Bid events are published to the channel; the server broadcasts to all watching connections. One auction can have 10,000 watchers — publish once to Redis, fan out to 10,000 WebSocket connections from multiple servers. CDN-cache the auction page HTML; only bid updates are real-time."
}
}
]
}
Asked at: Airbnb Interview Guide
Asked at: Shopify Interview Guide
Asked at: Coinbase Interview Guide
Asked at: Stripe Interview Guide