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

Source: https://www.techinterview.org/post/3233464171/lld-online-auction-system/
Updated: 2026-07-05 · techinterview.org

## 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 <= max_amount):
                # Already holding the lock: record the autobid inline
                bid = Bid(bidder_id=bidder_id, amount=next_bid, is_auto=True)
                auction.bids.append(bid)
                auction.current_price = next_bid
                print(f"Autobid: {bidder_id} auto-bid $" + f"{next_bid:.2f} "
                      f"on {auction.item.title}")

    def close_auction(self, auction_id: str) -> 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](https://techinterview.org/shopify-interview-guide/)

🏢 Asked at: [Coinbase Interview Guide](https://techinterview.org/coinbase-interview-guide/)

🏢 Asked at: [Stripe Interview Guide](https://techinterview.org/stripe-interview-guide/)

🏢 Asked at: [Atlassian Interview Guide](https://techinterview.org/atlassian-interview-guide/)

🏢 Asked at: [LinkedIn Interview Guide 2026: Social Graph Engineering, Feed Ranking, and Professional Network Scale](https://techinterview.org/linkedin-interview-guide/)

Asked at: [Airbnb Interview Guide](https://techinterview.org/airbnb-interview/)

Asked at: [Snap Interview Guide](https://techinterview.org/snap-interview/)
