Problem Statement
Design an online auction system like eBay. Sellers list items with a starting price, auction duration, and reserve price. Bidders place bids. The system enforces that each bid must exceed the current highest bid, tracks bid history, automatically extends the auction if a bid is placed in the final minutes, and declares the winner when the auction closes.
Core Entities
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional
import uuid
import threading
class AuctionStatus(Enum):
PENDING = "pending" # Listed but not started yet
ACTIVE = "active" # Bidding is open
EXTENDED = "extended" # Extended due to last-minute bid
CLOSED = "closed" # Bidding ended, winner determined
CANCELLED = "cancelled" # Seller cancelled before close
@dataclass
class Item:
item_id: str
title: str
description: str
seller_id: str
image_urls: list[str] = field(default_factory=list)
@dataclass
class Bid:
bid_id: str = field(default_factory=lambda: str(uuid.uuid4()))
bidder_id: str = ""
amount: float = 0.0
timestamp: datetime = field(default_factory=datetime.now)
is_auto: bool = False # Placed by autobid system
@dataclass
class Auction:
auction_id: str = field(default_factory=lambda: str(uuid.uuid4()))
item: Optional['Item'] = None
starting_price: float = 0.0
reserve_price: float = 0.0 # Minimum price to sell
current_price: float = 0.0
bid_increment: float = 1.0 # Minimum raise per bid
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
status: AuctionStatus = AuctionStatus.PENDING
bids: list[Bid] = field(default_factory=list)
winner_id: Optional[str] = None
EXTENSION_MINUTES: int = 5 # Extend if bid in final 5 minutes
EXTENSION_SECONDS: int = 300
@property
def current_winner(self) -> Optional[str]:
return self.bids[-1].bidder_id if self.bids else None
@property
def minimum_bid(self) -> float:
return self.current_price + self.bid_increment
def reserve_met(self) -> bool:
return self.current_price >= self.reserve_price
Auction Service with Concurrency Control
class AuctionService:
def __init__(self):
self._auctions: dict[str, Auction] = {}
self._items: dict[str, Item] = {}
self._users: dict[str, str] = {} # user_id -> name
# Per-auction locks: prevent concurrent bid processing on same auction
self._locks: dict[str, threading.Lock] = {}
# Autobid: bidder_id -> {auction_id -> max_amount}
self._autobids: dict[str, dict[str, float]] = {}
def create_auction(self, item: Item, seller_id: str,
starting_price: float, reserve_price: float,
duration_hours: int, bid_increment: float = 1.0) -> Auction:
self._items[item.item_id] = item
start_time = datetime.now()
auction = Auction(
item = item,
starting_price = starting_price,
current_price = starting_price,
reserve_price = reserve_price,
bid_increment = bid_increment,
start_time = start_time,
end_time = start_time + timedelta(hours=duration_hours),
status = AuctionStatus.ACTIVE,
)
self._auctions[auction.auction_id] = auction
self._locks[auction.auction_id] = threading.Lock()
print(f"Auction created: {item.title} | Start: {starting_price} | "
f"Reserve: {reserve_price} | Ends: {auction.end_time}")
return auction
def place_bid(self, auction_id: str, bidder_id: str,
amount: float) -> tuple[bool, str]:
"""
Returns (success, message).
Thread-safe: only one bid processed at a time per auction.
"""
auction = self._auctions.get(auction_id)
if not auction:
return False, "Auction not found"
with self._locks[auction_id]:
now = datetime.now()
# Validate auction state
if auction.status not in (AuctionStatus.ACTIVE, AuctionStatus.EXTENDED):
return False, f"Auction is {auction.status.value}"
if now > auction.end_time:
self._close_auction(auction)
return False, "Auction has ended"
if amount < auction.minimum_bid:
return False, f"Bid must be at least {auction.minimum_bid}"
if auction.current_winner == bidder_id:
return False, "You are already the highest bidder"
# Record bid
bid = Bid(bidder_id=bidder_id, amount=amount)
auction.bids.append(bid)
auction.current_price = amount
# Auto-extend: if bid placed in final EXTENSION_MINUTES
time_remaining = (auction.end_time - now).total_seconds()
if time_remaining <= auction.EXTENSION_SECONDS:
auction.end_time = now + timedelta(seconds=auction.EXTENSION_SECONDS)
auction.status = AuctionStatus.EXTENDED
print(f"Auction extended to {auction.end_time}")
print(f"Bid placed: {bidder_id} bid $" + f"{amount:.2f} "
f"on {auction.item.title}")
# Trigger autobid for other bidders
self._process_autobids(auction, bidder_id)
return True, f"Bid of $" + f"{amount:.2f} placed successfully"
def set_autobid(self, auction_id: str, bidder_id: str, max_amount: float):
"""Register autobid: system will bid automatically up to max_amount."""
if bidder_id not in self._autobids:
self._autobids[bidder_id] = {}
self._autobids[bidder_id][auction_id] = max_amount
print(f"Autobid set for {bidder_id}: up to $" + f"{max_amount:.2f}")
# Immediately bid if currently losing
auction = self._auctions.get(auction_id)
if auction and auction.current_winner != bidder_id:
next_bid = auction.minimum_bid
if next_bid <= max_amount:
self.place_bid(auction_id, bidder_id, next_bid)
def _process_autobids(self, auction: Auction, just_bid_by: str):
"""After a new bid, check if other bidders have autobids to trigger."""
for bidder_id, auto_limits in self._autobids.items():
if bidder_id == just_bid_by:
continue
if auction.auction_id not in auto_limits:
continue
max_amount = auto_limits[auction.auction_id]
next_bid = auction.minimum_bid
if (auction.current_winner != bidder_id
and next_bid Optional[str]:
auction = self._auctions.get(auction_id)
if not auction:
return None
with self._locks[auction_id]:
return self._close_auction(auction)
def _close_auction(self, auction: Auction) -> Optional[str]:
if auction.status in (AuctionStatus.CLOSED, AuctionStatus.CANCELLED):
return auction.winner_id
auction.status = AuctionStatus.CLOSED
if auction.bids and auction.reserve_met():
auction.winner_id = auction.current_winner
print(f"Auction closed. Winner: {auction.winner_id} "
f"with $" + f"{auction.current_price:.2f}")
else:
reason = "no bids" if not auction.bids else "reserve not met"
print(f"Auction closed with no sale ({reason}). "
f"Reserve: $" + f"{auction.reserve_price:.2f}, "
f"Final bid: $" + f"{auction.current_price:.2f}")
return auction.winner_id
def get_bid_history(self, auction_id: str) -> list[dict]:
auction = self._auctions.get(auction_id)
if not auction:
return []
return [
{"bidder": b.bidder_id, "amount": b.amount,
"timestamp": b.timestamp.isoformat(), "auto": b.is_auto}
for b in auction.bids
]
Usage Example
def demo():
service = AuctionService()
item = Item("I001", "Vintage Guitar", "1960s Fender Stratocaster",
seller_id="S001")
auction = service.create_auction(item, "S001",
starting_price=500.0,
reserve_price=800.0,
duration_hours=24,
bid_increment=25.0)
aid = auction.auction_id
# Bidders compete
service.place_bid(aid, "B001", 525.0) # Must be >= 500 + 25 = 525
service.place_bid(aid, "B002", 550.0)
service.set_autobid(aid, "B001", 900.0) # B001 will autobid up to $900
service.place_bid(aid, "B002", 700.0) # Triggers B001 autobid to 725
service.close_auction(aid)
print(service.get_bid_history(aid))
demo()
Design Decisions
- Per-auction locking: Only bids on the same auction contend for the lock. Auctions on different items proceed concurrently. Prevents concurrent bids from both “winning” at the same price.
- Auto-extension: Prevents auction sniping — bidding at the last second to win without competition. Common in real platforms (eBay extends by 5 minutes when a bid is placed in the final 5 minutes of an eBay Motors auction).
- Autobid (proxy bidding): Bidder sets a maximum; the system bids on their behalf in minimum increments. Only reveals the minimum necessary amount to be highest bidder. Real platforms (eBay) support this natively.
- Reserve price: Seller sets a confidential minimum. If no bid meets the reserve, the item doesn’t sell. The current price shows only if the reserve is met.
- Bid increment: Prevents penny-increment bidding wars. The minimum raise is set at auction creation (e.g., $25 increments for a $500+ item).
Interview Checklist
- Entities: Item, Auction, Bid, autobid configuration
- State machine: PENDING → ACTIVE → EXTENDED → CLOSED/CANCELLED
- Concurrency: per-auction lock prevents double-win at same price
- Auto-extension: extend if bid in final N minutes (anti-sniping)
- Reserve price: confidential minimum; no sale if reserve not met
- Autobid: proxy bidding with max amount ceiling
- Production: DB row-level locking; optimistic concurrency with bid sequence numbers
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you prevent concurrent bid conflicts in an auction system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Concurrent bid conflicts occur when two bidders submit bids simultaneously and both appear to be the new highest bidder. Without concurrency control, both bids could be accepted at the same price, which is incorrect. Solutions by scope: (1) In-memory (single process): use a threading.Lock per auction_id. When processing a bid, acquire the lock, re-validate that the bid is still the highest (check inside the lock — not outside), record the bid, release. Per-auction locking allows concurrent bids on different auctions while serializing bids on the same auction. (2) Database (production): use SELECT FOR UPDATE to lock the auction row during bid processing. The transaction: BEGIN; SELECT current_price FROM auctions WHERE id=X FOR UPDATE; IF :new_bid > current_price + increment: INSERT INTO bids …; UPDATE auctions SET current_price=:new_bid; COMMIT. Only one transaction holds the row lock at a time — the other waits and then rechecks. (3) Optimistic concurrency: don’t lock, but include the expected current_price in the UPDATE: UPDATE auctions SET current_price=:new WHERE id=X AND current_price=:old. If this UPDATE affects 0 rows, another bid won — retry with the new current price. Optimistic concurrency is better for low-contention auctions (most auctions); pessimistic locking is simpler to reason about.”}},{“@type”:”Question”,”name”:”What is proxy bidding (autobid) and how do you implement it?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Proxy bidding (used by eBay) allows a bidder to set a maximum amount; the system automatically bids on their behalf in minimum increments to maintain their position as the highest bidder. The bidder’s maximum is kept confidential — only the current price (the minimum needed to win) is shown. Implementation: store {bidder_id: {auction_id: max_amount}} as the autobid configuration. Trigger: when a new bid is placed that exceeds an autobidder’s current price, check if the autobidder’s max_amount > new_bid + increment. If yes, immediately place an autobid at new_bid + increment. Cascade: this new autobid might trigger another bidder’s autobid, etc. This cascading continues until one bidder’s max is exhausted. Example: Alice sets max $100, Bob sets max $80. Bob bids $50. Alice autobids $51. Bob autobids $52. … Bob exhausted at $80, Alice wins at $81 (one increment above Bob’s max). Implementation detail: process autobids synchronously within the bid handler (inside the lock) to avoid race conditions. The autobid chain terminates when no remaining autobidder can outbid the current price. Limit cascade depth to prevent infinite loops — in practice, the two-bidder cascade resolves in O(max_amount / increment) steps, which is bounded.”}},{“@type”:”Question”,”name”:”How does anti-sniping (auction extension) work?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Auction sniping is placing a bid in the final seconds of an auction to win without giving other bidders time to respond. Professional snipers use automated tools to submit bids 1-2 seconds before close. Anti-sniping extension: if a bid is placed within the final N minutes of the auction, automatically extend the auction end time by N minutes. This gives other bidders a chance to respond. Implementation: when recording a bid, compute remaining_seconds = (end_time – now).total_seconds(). If remaining_seconds <= EXTENSION_SECONDS (e.g., 300 = 5 minutes), set end_time = now + EXTENSION_SECONDS and status = EXTENDED. The auction can theoretically extend multiple times if bids keep arriving in the final window. Production considerations: (1) Notify all active bidders when an extension occurs (push notification, email). (2) Set a maximum total extension (e.g., no more than 2 hours of extensions). (3) Record extension events in the audit log. (4) eBay uses this for eBay Motors (5-minute extensions); regular eBay uses hard ending times. The tradeoff: extensions increase revenue (more competition) but may frustrate buyers who want a clear end time. Some platforms (Poshmark, Whatnot) use live auction formats that explicitly extend for any bid, trading predictability for higher final prices."}}]}
🏢 Asked at: Shopify Interview Guide
🏢 Asked at: Coinbase Interview Guide
🏢 Asked at: Stripe Interview Guide
🏢 Asked at: Atlassian Interview Guide
🏢 Asked at: LinkedIn Interview Guide 2026: Social Graph Engineering, Feed Ranking, and Professional Network Scale
Asked at: Airbnb Interview Guide
Asked at: Snap Interview Guide