Low-Level Design: Online Auction System (OOP Interview)

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

🏢 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

Scroll to Top