Design Auction System

Last Updated: December 19, 2025

Ashish

Ashish Pratap Singh

medium

In this chapter, we will explore the low-level design of an online auction system in detail.

Let's start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.

Here is an example of how a conversation between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • Allow users to create and list auction items with a title, description, starting price, and end time
  • Allow users to place bids on active auctions
  • Support concurrent bidding on multiple items by the same or different users
  • Determine the winner based on the highest bid at auction end; resolve ties by earliest bid.
  • Notify users when they are outbid or when the auction ends
  • Prevent bids once the auction has ended.
  • Maintain a complete bid history for each auction item.

1.2 Non-Functional Requirements

  • Concurrency: The system must be thread-safe to handle multiple simultaneous bids on the same auction without data integrity issues.
  • Modularity: The system should follow object-oriented design with clear separation of concerns.
  • Reliability: The mechanism for closing auctions at their specified end time must be reliable and function automatically.
  • Maintainability: The codebase should be clean, testable, and easy to enhance or debug.
  • Extensibility: The design should be modular, making it easy to add new features in the future, such as different auction types or user roles.
  • Simplified Interface: The system should provide a simple, high-level API for clients to perform key actions like creating an auction or placing a bid, hiding the underlying complexity.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., user, auction, bid, item, notification) and actions (e.g., create, bid, outbid, win, notify) from the functional requirements. These often translate directly into classes, enums, or interfaces in an object-oriented design.

Let’s walk through the functional requirements and extract the relevant entities:

1. Allow users to create and list auction items, and place bids on them.

This points to three fundamental entities:

  • User: Represents a participant in the auction, who can act as a bidder.
  • Auction: The central entity representing a single item up for auction. It holds the item's details, manages its state, and maintains a history of all bids.
  • Bid: A data object representing a single bid made by a User on an Auction, containing the amount and a timestamp.

2. The system must manage the auction lifecycle and prevent bids after it has ended.

An Auction has a clear lifecycle (e.g., active, closed). This is best represented by an AuctionState enum, which helps control what actions are permissible at different times.

3. Reliably close auctions at their end time and provide a simplified interface for the entire system.

To manage the lifecycle of multiple auctions and provide a clean entry point, a central service layer is necessary.

  • AuctionService: This class acts as a Facade and Singleton. It provides a simple, high-level API for all client interactions (like creating an auction or placing a bid), hiding the complexity of managing auction states, notifications, and scheduling.

These core entities define the essential abstractions of the Online Auction System and will guide the structure of your low-level design and class diagrams.

3. Designing Classes and Relationships

This section breaks down the system's architecture into its fundamental classes, their responsibilities, and the relationships that connect them. We also explore the key design patterns that provide robustness and flexibility to the solution.

3.1 Class Definitions

The system is composed of several types of classes, each with a distinct role.

Enums

AuctionState

Defines the discrete lifecycle stages of an auction: PENDING, ACTIVE, and CLOSED.

AuctionStatus

This enum is used for state management within the Auction class.

Data Classes

Bid

A data class representing a single bid made by a user.

Bid

It encapsulates the bidder, the amount, and a timestamp. It implements Comparable to allow for easy determination of the highest bid, first by amount and then by timestamp.

Core Classes

User

Represents a participant in the auction.

User

Crucially, it also acts as a concrete Observer by implementing the AuctionObserver interface. This allows a User object to be directly subscribed to an auction and receive notifications.

Auction

This is the central class representing a single auction event.

Auction

It manages all bids, the auction's state (AuctionState), and the end time. It acts as the Subject in the Observer pattern, maintaining a list of subscribed observers (Users) and notifying them of key events, such as when a new highest bid is placed or when the auction ends.

AuctionService (Singleton & Facade)

The primary entry point for the entire application.

AuctionService

It orchestrates all high-level operations, such as creating users and auctions, and processing bids. It manages the collections of all active users and auctions, and it uses a ScheduledExecutorService to handle the automatic closing of auctions when their end time is reached.

3.2 Class Relationships

The relationships between classes define the system's structure and data flow.

Composition

  • AuctionService "has-a" collection of Users and Auctions, managing their lifecycle within the system.
  • An Auction "has-a" list of Bids.

Association

  • An Auction (Subject) is associated with a set of AuctionObservers (which are Users).
  • A Bid is associated with the User who placed it.

Inheritance

  • The User class implements the AuctionObserver interface, allowing it to act as an observer.

Dependency

  • The AuctionService (Facade) depends on Auction and User objects to carry out its operations.
  • The Auction class depends on the Bid class to record bidding activity.

3.3 Key Design Patterns

Observer Pattern

This is the core pattern for providing real-time updates to participants.

AuctionObserver

The Auction object acts as the Subject, and bidding Users act as Observers. When a significant event occurs (e.g., a user is outbid, the auction ends), the Auction automatically notifies all relevant observers, ensuring they are kept in sync with the auction's state.

Facade Pattern

The AuctionService class serves as a facade. It provides a simple, high-level API (createAuction, placeBid, endAuction) that hides the complex internal workflows of managing users, auctions, bidding logic, and scheduling. Clients interact with the system through this single, clean interface.

Singleton Pattern

AuctionService is implemented as a singleton to ensure there is a single, globally accessible point of control for the entire auction system. This centralizes the management of all auctions and users and prevents state inconsistencies.

3.4 Full Class Diagram

Online Auction System Class Diagram

4. Implementation

4.1 AuctionObserver (Observer Pattern)

Defines the Observer interface to receive updates about an auction’s state (e.g., when outbid or when the auction ends). All participants placing bids become observers automatically.

1class AuctionObserver(ABC):
2    @abstractmethod
3    def on_update(self, auction: 'Auction', message: str):
4        pass

4.2 User

The User class represents a participant, and crucially, it also acts as an "Observer" to receive notifications.

1class User(AuctionObserver):
2    def __init__(self, name: str):
3        self.id = str(uuid.uuid4())
4        self.name = name
5
6    def get_id(self) -> str:
7        return self.id
8
9    def get_name(self) -> str:
10        return self.name
11
12    def on_update(self, auction: 'Auction', message: str):
13        print(f"--- Notification for {self.name} ---")
14        print(f"Auction: {auction.get_item_name()}")
15        print(f"Message: {message}")
16        print("---------------------------\n")

Each user has a unique ID and name.

By implementing AuctionObserver, the User class can be directly subscribed to an Auction. The onUpdate method is the callback that the Auction will invoke to send notifications, such as "You have been outbid" or "The auction has ended." This decouples the auction logic from the user notification mechanism.

4.3 AuctionState

Represents the current lifecycle state of an auction.

1class AuctionState(Enum):
2    PENDING = "PENDING"
3    ACTIVE = "ACTIVE"
4    CLOSED = "CLOSED"

4.4 Bid

A data object representing a single bid, designed to be comparable for easily finding the highest bid.

1class Bid:
2    def __init__(self, bidder: User, amount: Decimal):
3        self.bidder = bidder
4        self.amount = amount
5        self.timestamp = datetime.now()
6
7    def get_bidder(self) -> User:
8        return self.bidder
9
10    def get_amount(self) -> Decimal:
11        return self.amount
12
13    def get_timestamp(self) -> datetime:
14        return self.timestamp
15
16    def __lt__(self, other: 'Bid') -> bool:
17        if self.amount != other.amount:
18            return self.amount < other.amount
19        return self.timestamp > other.timestamp
20
21    def __eq__(self, other: 'Bid') -> bool:
22        return self.amount == other.amount and self.timestamp == other.timestamp
23
24    def __le__(self, other: 'Bid') -> bool:
25        return self < other or self == other
26
27    def __gt__(self, other: 'Bid') -> bool:
28        return not self <= other
29
30    def __ge__(self, other: 'Bid') -> bool:
31        return not self < other
32
33    def __ne__(self, other: 'Bid') -> bool:
34        return not self == other
35
36    def __str__(self) -> str:
37        return f"Bidder: {self.bidder.get_name()}, Amount: {self.amount:.2f}, Time: {self.timestamp}"

Each Bid is timestamped and comparable by amount (higher is better) and time (earlier wins in case of tie). This makes sorting and comparison straightforward.

4.5 Auction

This is the central class for a single auction. It manages the bidding process, its own state, and acts as the "Subject" in the Observer pattern.

1class Auction:
2    def __init__(self, item_name: str, description: str, starting_price: Decimal, end_time: datetime):
3        self.id = str(uuid.uuid4())
4        self.item_name = item_name
5        self.description = description
6        self.starting_price = starting_price
7        self.end_time = end_time
8        self.bids: List[Bid] = []
9        self.observers: Set[AuctionObserver] = set()
10        self.state = AuctionState.ACTIVE
11        self.winning_bid: Optional[Bid] = None
12        self._lock = threading.RLock()
13
14    def place_bid(self, bidder: User, amount: Decimal):
15        with self._lock:
16            if self.state != AuctionState.ACTIVE:
17                raise Exception("Auction is not active.")
18            if datetime.now() > self.end_time:
19                self.end_auction()
20                raise Exception("Auction has already ended.")
21
22            highest_bid = self.get_highest_bid()
23            current_max_amount = self.starting_price if highest_bid is None else highest_bid.get_amount()
24
25            if amount <= current_max_amount:
26                raise ValueError("Bid must be higher than the current highest bid.")
27
28            previous_highest_bidder = highest_bid.get_bidder() if highest_bid is not None else None
29
30            new_bid = Bid(bidder, amount)
31            self.bids.append(new_bid)
32            self.add_observer(bidder)
33
34            print(f"SUCCESS: {bidder.get_name()} placed a bid of ${amount:.2f} on '{self.item_name}'.")
35
36            if previous_highest_bidder is not None and previous_highest_bidder != bidder:
37                self.notify_observer(previous_highest_bidder, 
38                    f"You have been outbid on '{self.item_name}'! The new highest bid is ${amount:.2f}.")
39
40    def end_auction(self):
41        with self._lock:
42            if self.state != AuctionState.ACTIVE:
43                return
44
45            self.state = AuctionState.CLOSED
46            self.winning_bid = self.get_highest_bid()
47
48            if self.winning_bid is not None:
49                end_message = f"Auction for '{self.item_name}' has ended. Winner is {self.winning_bid.get_bidder().get_name()} with a bid of ${self.winning_bid.get_amount():.2f}!"
50            else:
51                end_message = f"Auction for '{self.item_name}' has ended. There were no bids."
52
53            print(f"\n{end_message.upper()}")
54            self.notify_all_observers(end_message)
55
56    def get_highest_bid(self) -> Optional[Bid]:
57        if not self.bids:
58            return None
59        return max(self.bids)
60
61    def is_active(self) -> bool:
62        return self.state == AuctionState.ACTIVE
63
64    def add_observer(self, observer: AuctionObserver):
65        self.observers.add(observer)
66
67    def notify_all_observers(self, message: str):
68        for observer in self.observers:
69            observer.on_update(self, message)
70
71    def notify_observer(self, observer: AuctionObserver, message: str):
72        observer.on_update(self, message)
73
74    def get_id(self) -> str:
75        return self.id
76
77    def get_item_name(self) -> str:
78        return self.item_name
79
80    def get_bid_history(self) -> List[Bid]:
81        return self.bids.copy()
82
83    def get_state(self) -> AuctionState:
84        return self.state
85
86    def get_winning_bid(self) -> Optional[Bid]:
87        return self.winning_bid
  •  The Auction class is the "Subject." It maintains a set of observers (bidders). When a significant event occurs (a new highest bid, auction ends), it calls its notification methods.

4.6 AuctionService

This class acts as a central Singleton and Facade, providing a simplified API for clients to interact with the entire auction system.

1class AuctionService:
2    _instance = None
3    _lock = threading.Lock()
4
5    def __init__(self):
6        if AuctionService._instance is not None:
7            raise Exception("This class is a singleton!")
8        self.users: Dict[str, User] = {}
9        self.auctions: Dict[str, Auction] = {}
10        self.scheduler = ThreadPoolExecutor(max_workers=10)
11        self._shutdown = False
12
13    @staticmethod
14    def get_instance():
15        if AuctionService._instance is None:
16            with AuctionService._lock:
17                if AuctionService._instance is None:
18                    AuctionService._instance = AuctionService()
19        return AuctionService._instance
20
21    def create_user(self, name: str) -> User:
22        user = User(name)
23        self.users[user.get_id()] = user
24        return user
25
26    def get_user(self, user_id: str) -> User:
27        return self.users[user_id]
28
29    def create_auction(self, item_name: str, description: str, starting_price: Decimal, end_time: datetime) -> Auction:
30        auction = Auction(item_name, description, starting_price, end_time)
31        self.auctions[auction.get_id()] = auction
32
33        delay = (end_time - datetime.now()).total_seconds()
34        if delay > 0:
35            self.scheduler.submit(self._scheduled_end_auction, auction.get_id(), delay)
36
37        print(f"New auction created for '{item_name}' (ID: {auction.get_id()}), ending at {end_time}.")
38        return auction
39
40    def _scheduled_end_auction(self, auction_id: str, delay: float):
41        time.sleep(delay)
42        if not self._shutdown:
43            self.end_auction(auction_id)
44
45    def view_active_auctions(self) -> List[Auction]:
46        return [auction for auction in self.auctions.values() if auction.is_active()]
47
48    def place_bid(self, auction_id: str, bidder_id: str, amount: Decimal):
49        auction = self.get_auction(auction_id)
50        auction.place_bid(self.users[bidder_id], amount)
51
52    def end_auction(self, auction_id: str):
53        auction = self.get_auction(auction_id)
54        auction.end_auction()
55
56    def get_auction(self, auction_id: str) -> Auction:
57        auction = self.auctions.get(auction_id)
58        if auction is None:
59            raise KeyError(f"Auction with ID {auction_id} not found.")
60        return auction
61
62    def shutdown(self):
63        self._shutdown = True
64        self.scheduler.shutdown(wait=True)
  • Facade Pattern: The service provides a simple, high-level API (createAuction, placeBid) that hides the complexity of object creation, scheduling, and direct interaction with Auction objects.
  • Singleton Pattern: A single AuctionService instance manages all users and auctions, acting as the central nervous system of the application. The getInstance() method is synchronized to ensure thread-safe lazy initialization.

4.7 AuctionSystemDemo

This driver class simulates a real-world bidding scenario, validating the end-to-end functionality of the system.

1class AuctionSystemDemo:
2    @staticmethod
3    def main():
4        auction_service = AuctionService.get_instance()
5
6        alice = auction_service.create_user("Alice")
7        bob = auction_service.create_user("Bob")
8        carol = auction_service.create_user("Carol")
9
10        print("=============================================")
11        print("        Online Auction System Demo           ")
12        print("=============================================")
13
14        end_time = datetime.now() + timedelta(seconds=10)
15        laptop_auction = auction_service.create_auction(
16            "Vintage Laptop",
17            "A rare 1990s laptop, in working condition.",
18            Decimal("100.00"),
19            end_time
20        )
21        print()
22
23        try:
24            auction_service.place_bid(laptop_auction.get_id(), alice.get_id(), Decimal("110.00"))
25            time.sleep(0.5)
26
27            auction_service.place_bid(laptop_auction.get_id(), bob.get_id(), Decimal("120.00"))
28            time.sleep(0.5)
29
30            auction_service.place_bid(laptop_auction.get_id(), carol.get_id(), Decimal("125.00"))
31            time.sleep(0.5)
32
33            auction_service.place_bid(laptop_auction.get_id(), alice.get_id(), Decimal("150.00"))
34
35            print("\n--- Waiting for auction to end automatically... ---")
36            time.sleep(2)
37        except Exception as e:
38            print(f"An error occurred during bidding: {e}")
39
40        print("\n--- Post-Auction Information ---")
41        ended_auction = auction_service.get_auction(laptop_auction.get_id())
42
43        if ended_auction.get_winning_bid() is not None:
44            print(f"Final Winner: {ended_auction.get_winning_bid().get_bidder().get_name()}")
45            print(f"Winning Price: ${ended_auction.get_winning_bid().get_amount():.2f}")
46        else:
47            print("The auction ended with no winner.")
48
49        print("\nFull Bid History:")
50        for bid in ended_auction.get_bid_history():
51            print(bid)
52
53        print("\n--- Attempting to bid on an ended auction ---")
54        try:
55            auction_service.place_bid(laptop_auction.get_id(), bob.get_id(), Decimal("200.00"))
56        except Exception as e:
57            print(f"CAUGHT EXPECTED ERROR: {e}")
58
59        auction_service.shutdown()
60
61
62if __name__ == "__main__":
63    AuctionSystemDemo.main()

5. Run and Test

Files7
core
entities
enum
observer
auction_system_demo.py
main
auction_system_demo.pymain
Output

6. Quiz

Design Online Auction System - Quiz

1 / 21
Multiple Choice

Which entity is primarily responsible for holding the list of bids in an Online Auction System?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script