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
Auctionis created for anItemwith a starting price and a closing time. - A
Bidderplaces aBid; 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
Auctionmust not hold references to specific bidder types or notification channels (email, push, in-app) - adding a new notification method should not changeAuction.- 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.
- 1Alice's bid becomes the new highest; she is added as an observer.
- 2Bob outbids Alice; he too is registered as an observer of this auction.
- 3The auction never calls Alice's email provider directly - it just calls the observer interface.
- 4Alice reclaims the lead after being notified.
- 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
Code
Design decisions
- Notification is Observer, not
Auctioncalling aNotificationServicedirectly. A direct call would forceAuctionto know about email templates, push tokens, or whatever channel bidders prefer. Observer flips that -Auctiononly knows it has a list of things that want to hear about outbids, and eachBidderdecides for itself what "getting notified" means. - A
Biddersubscribes by bidding, not through a separatewatch()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. isClosedis checked at the top ofplaceBid, 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 letplaceBidreturn immediately, and closing on a timer needs a scheduler to callclose()rather than relying on every caller to check the clock themselves.
Common follow-ups
- What happens if
Bidder.notifyOutbidthrows (a flaky push provider, a dropped connection)? As written, one throwing observer would propagate the exception straight out ofplaceBidand abort the loop before the rest of the observers hear about the new high bid - and the bid itself has already been accepted by that point, so the state is now inconsistent with who got notified.Auction.placeBidneeds to wrap eachnotifyOutbidcall individually (the same fan-out failure isolationpub-sub-system.mdxuses forTopic.publish) so one bad observer can't silently swallow notifications to everyone bidding after them. - Two bidders submit what would be winning bids within milliseconds of each other -
what stops both from becoming the highest bid?
placeBid's read-compare-write onhighestBidis not atomic against a concurrent caller, so both bids could read the same prior high before either writes - the classic race also called out inmovie-booking.mdx's seat locking. The fix is the same shape: the compare-and-set onhighestBidneeds to happen as one atomic step, not a check followed by a separate assignment. - How would you support a "buy it now" price that ends the auction immediately? Add a
buyItNowPricetoItemand check it insideplaceBidright after the existing price-comparison check - if the bid meets or exceeds it, callclose()before returningtruerather than leaving the auction open for further bids. No change needed to the Observer wiring;notifyOutbidalready fires for whatever the new highest bid is. - How would you support proxy bidding (a bidder sets a max, the system auto-bids up to
it as needed)? Give
Bidderan optionalmaxAutoBidand, insideplaceBid, after accepting a new human bid, check whether any previously-outbidBidder'smaxAutoBidwould still beat it - if so, immediately place that bidder's next increment as a new bid before returning. The Observer relationship doesn't change; the auto-bid is just another call to the sameplaceBidmethod, triggered internally instead of by a person.
Check yourself
Why is notification handled through the Observer pattern rather than `Auction` calling a `NotificationService` directly?