Skip to main content

Online Auction

An auction is a broadcast problem wearing an e-commerce costume. The moment someone outbids the current leader, every other bidder watching that item needs to find out - and the Auction class shouldn't need to know who's watching or how many of them there are.

Requirements

Functional

  • An Auction is created for an Item with a starting price and a closing time.
  • A Bidder places a Bid; it's only accepted if it's higher than the current highest bid and the auction hasn't closed.
  • Every bidder who has previously bid on the item gets notified when a new highest bid comes in, without polling.
  • When the closing time passes, the auction closes and the highest bidder wins; no further bids are accepted.

Non-functional

  • Auction must not hold references to specific bidder types or notification channels (email, push, in-app) - adding a new notification method should not change Auction.
  • Rejecting a low bid or a bid on a closed auction should be immediate and never mutate auction state.

Design

Auction is the Subject in an Observer relationship: every Bidder who places a bid becomes a subscriber, and Auction.placeBid calls notifyOutbid on all of them the instant a new high bid lands - it has no idea whether a Bidder is a person, a bot, or a proxy bidding service, only that it implements AuctionObserver.

AliceBobAuctionplaceBid($100)1placeBid($120)2notifyOutbid($120)3placeBid($130)4notifyOutbid($130)5
  1. 1Alice's bid becomes the new highest; she is added as an observer.
  2. 2Bob outbids Alice; he too is registered as an observer of this auction.
  3. 3The auction never calls Alice's email provider directly - it just calls the observer interface.
  4. 4Alice reclaims the lead after being notified.
  5. 5Bob is notified in turn - the notification list grows with every new bidder, unknown to Auction itself.

Closing the auction is a one-way transition - Auction.close() sets a flag that placeBid checks first, so a bid racing the closing time either lands cleanly before close or is rejected cleanly after it, never half-applied.

Class diagram

«interface»AuctionObserver+ notifyOutbid(newHighBid: Bid)Auction- item: Item- highestBid: Bid- observers: List<AuctionObserver>- closesAt: datetime- closed: bool+ placeBid(bidder, amount): bool+ close()Item- id: string- title: string- startingPrice: doubleBid- bidder: Bidder- amount: double- placedAt: datetimeBidder- id: string- name: string+ notifyOutbid(newHighBid: Bid)
implementsuses
Auction is the Subject; every Bidder that has bid is an AuctionObserver notified on each new high bid.

Code

import java.time.LocalDateTime;
import java.util.*;
 
class Item {
final String id;
final String title;
final double startingPrice;
 
Item(String id, String title, double startingPrice) {
this.id = id;
this.title = title;
this.startingPrice = startingPrice;
}
}
 
class Bid {
final Bidder bidder;
final double amount;
final LocalDateTime placedAt;
 
Bid(Bidder bidder, double amount) {
this.bidder = bidder;
this.amount = amount;
this.placedAt = LocalDateTime.now();
}
}
 
interface AuctionObserver {
void notifyOutbid(Bid newHighBid);
}
 
class Bidder implements AuctionObserver {
final String id;
final String name;
 
Bidder(String id, String name) {
this.id = id;
this.name = name;
}
 
public void notifyOutbid(Bid newHighBid) {
System.out.printf("%s: you were outbid, new high is $%.2f by %s%n",
name, newHighBid.amount, newHighBid.bidder.name);
}
}
 
class Auction {
final Item item;
private final LocalDateTime closesAt;
private Bid highestBid;
private boolean closed = false;
private final Set<AuctionObserver> observers = new LinkedHashSet<>();
 
Auction(Item item, LocalDateTime closesAt) {
this.item = item;
this.closesAt = closesAt;
}
 
boolean placeBid(Bidder bidder, double amount) {
if (closed) return false;
if (highestBid != null && amount <= highestBid.amount) return false;
 
Bid previousHigh = highestBid;
highestBid = new Bid(bidder, amount);
observers.add(bidder);
 
for (AuctionObserver observer : observers) {
if (observer != bidder) observer.notifyOutbid(highestBid);
}
return true;
}
 
void close() {
closed = true;
}
 
Optional<Bid> winningBid() {
return Optional.ofNullable(closed ? highestBid : null);
}
}

Design decisions

  • Notification is Observer, not Auction calling a NotificationService directly. A direct call would force Auction to know about email templates, push tokens, or whatever channel bidders prefer. Observer flips that - Auction only knows it has a list of things that want to hear about outbids, and each Bidder decides for itself what "getting notified" means.
  • A Bidder subscribes by bidding, not through a separate watch() call. Coupling subscription to the act of bidding matches how real auctions work - you don't get outbid alerts for an item you've never bid on - and it means there's no separate watch-list state that could drift out of sync with who's actually bidding.
  • isClosed is checked at the top of placeBid, before the price comparison. Checking price first and closed-status second would let a technically-valid high bid slip through a few milliseconds after closing time under the wrong ordering; closed-status first means a closed auction rejects every bid uniformly, no matter how good it is.
  • What's missing for a real system: notifications here fire synchronously inside placeBid, which means a slow observer (a flaky push provider) blocks the bid itself - a production system would queue notifications and let placeBid return immediately, and closing on a timer needs a scheduler to call close() rather than relying on every caller to check the clock themselves.
0%0 of 122 pages studied