This document describes how our auction system works. The system is split into two main parts: the Auction Distributor (which is responsible for handling and maintaining the socket connection. It Enables communication b/w Client and Auction Processor; Any kind of communication b/w auction processor and client is done via distributor) and the Auction Processor (which is responsible for generating & updating the auction entries, updating the prices and also handling the bids). Below, you'll find details about the high-level design, challenges faced, and a detailed flow of the entire process.
The auction system is designed for high concurrency, real-time updates, and fault tolerance. It leverages a microservices architecture to separate connection handling from business logic.
- Client: The end-user interface that connects via WebSockets to receive real-time updates (price changes, bids, sold/unsold events).
- API Gateway: Acts as the entry point for HTTP requests and potentially WebSocket connections, routing traffic to the appropriate services.
- Auction Distributor (Socket Conn):
- Manages persistent WebSocket connections with clients.
- Broadcasts events (price updates, sold items) to specific rooms.
- Forwards user actions (like
placeBid) to the message broker. - Scalable horizontally to handle thousands of concurrent connections.
- Message Broker (Redis Pub/Sub):
- Decouples the Distributor and Processor.
- Facilitates real-time event propagation (e.g., when the Engine updates a price, it publishes to Redis, which the Distributor consumes and sends to clients or when client places a bid, it publishes to Redis, which the Engine consumes and make product sold and make order).
- Auction Engine (Processor):
- The core business logic unit.
- Auction Engine: Calculates price drops, manages auction state (QUEUED, RUNNING, SOLD, UNSOLD), and handles transitions.
- Fault Tolerance (Recovery Manager): Ensures the system can recover from crashes by restoring state from the database and Redis.
- Internal Queue: Manages local processing of bids and unsold item handling. Each of Unsold or PlaceBid is placed in Queued to prevent any last second bid race condition where unsold and sold transition is having race condition.
- Cache (Redis):
- Stores ephemeral state for high-speed access:
liveAuctionMeta_<roomId>: Current price, status, product ID.auctionId: List of auction items.
- Used for distributed locking (Redlock) to prevent race conditions during bidding.
- Stores ephemeral state for high-speed access:
- Database (MongoDB):
- Persists permanent data: Auctions, AuctionItems, Products, and Orders.
- Source of truth for system recovery.
- Scheduler Service (QStash):
- Triggers the start of scheduled auctions.
- Ensures auctions start precisely at their designated times.
Challenge: Multiple users might try to bid on the same item at the exact same millisecond. Without control, two users could "win" the same item, or the price could update incorrectly. Solution: Implemented Distributed Locking using Redlock (Redis) and a Internal Queueing of State transition. When a bid is placed it is first placed in internal queue to prevent any last second race condition of product state changes then the system acquires a lock on the specific auction item. Only the process holding the lock can validate and accept the bid. Other concurrent requests are rejected or queued, ensuring data integrity.
Challenge: Keeping thousands of clients in sync with the rapidly changing auction price (decrementing every few seconds) and status without overloading the server. Solution:
- Decoupled Architecture: Separated the Auction Distributor (socket handling) from the Auction Processor (logic).
- Redis Pub/Sub: The Processor publishes state changes (price updates, sold events) to Redis. The Distributor subscribes to these channels and broadcasts updates only to the relevant rooms. This allows the heavy logic to run independently of the connection load.
Challenge: If the Auction Processor crashes mid-auction, the active auction state (current item, current price, timer) could be lost, ruining the user experience. Solution:
- State Persistence: Critical state is constantly updated in Redis (Cache) and MongoDB (Persistent DB).
- Recovery Manager: On startup, the system checks for "orphaned" active auctions in the database. It reconstructs the in-memory state from Redis/DB, recalculates the correct current price based on the time elapsed, and resumes the auction seamlessly.
Challenge: A monolithic app would struggle to handle both heavy computation (price logic) and thousands of open socket connections. Solution: Split into Distributor and Processor services. The Distributor can be scaled horizontally (adding more instances) to handle more users, while the Processor can be scaled independently based on the number of active auctions.
-
Auction Start Trigger When the auction start endpoint is hit (e.g.,
GET /start/:auctionId), the following happens:- Router calls:
schedulerService.unscheduleAuction(auctionId)to ensure there are no existing schedules. for the given auctionId.auctionEngine.startAuctionInRoom(auctionId, roomId)(Typically,roomIdis the same asauctionId.)
- Router calls:
-
Starting the Auction (AuctionEngine.startAuctionInRoom)
- AuctionEngine.startAuctionInRoom is invoked by the router. Inside this
method:
- It calls
auctionService.getOngoingAuctions()to check if the auction is already running. - If not running:
- It calls
auctionService.insertOngoingAuction(auctionId)to mark it as active. - It calls
auctionService.queueInAuctionItemsInDBAndCache(auctionId)to fetch auction items from the database, set their product status toQUEUED, and cache them. - It calls
auctionMetadataService.insertAuctionMetaForDay(auctionId)to create initial auction metadata.
- It calls
- Then it retrieves auction configuration data via
auctionService.getAuctionInfoById(auctionId). - It publishes an "auction start" event with
redisEventHandler.publishAuctionStart(roomId)which ultimately publishes the event that to the client that auction is about to start. - Finally, it schedules the first product for the auction by calling
prepareNextProductInRoomIdWithDelay(roomId, auctionId, delayOptions).
- It calls
- AuctionEngine.startAuctionInRoom is invoked by the router. Inside this
method:
-
Preparing the Next Product
- AuctionEngine.prepareNextProductInRoomIdWithDelay
- After a short fixed delay, it publishes a "preparing next" event using
redisEventHandler.publishPreparingNext(roomId). - Then it calls
prepareNextProductInRoomId(roomId, auctionId, delayOptions)to actually load the next auction item.
- After a short fixed delay, it publishes a "preparing next" event using
- AuctionEngine.prepareNextProductInRoomId
- Clears any existing price update intervals.
- Retrieves the current auction metadata with
auctionMetadataService.getCurrentAuctionMeta(roomId). - Gets all auction item IDs via
auctionService.getAuctionItemIds(auctionId). - Calls
checkAndHandleIfAuctionEndto see if there are more items queued for auction. If not, it triggerstriggerAuctionEnd. - If more items exist, it:
- Determines the next product index.
- Fetches the next auction item with
auctionService.getAuctionItem. - Retrieves auction configuration data via
auctionService.getAuctionInfoById. - Updates the starting price if needed (including adjustments based on
the previous item’s sale status) by calling
auctionService.updateAuctionItemPrice. - Constructs new auction metadata and updates it using
auctionMetadataService.updateAuctionMeta. - Waits for a delay (based on either product or section intervals) and then starts the price update cycle.
- It initializes the price update cycle by:
- Calling the function returned by
priceUpdateHandleronce (to immediately show the starting price). - Setting an interval that repeatedly calls the update handler at intervals defined by the auction configuration.
- Calling the function returned by
- AuctionEngine.prepareNextProductInRoomIdWithDelay
-
Bidding Process
- Handling a Bid (AuctionEngine.handlePlaceBid)
- When a bid event is received (via a subscription from
redisEventHandler), the callbackhandlePlaceBidis invoked. - This method:
- Acquires a distributed lock (using Redlock) to avoid race conditions(Preventing multiple users to place bid once the bid has been placed by the first user.)
- Fetches auction listings by calling
auctionService.getAuctionItems(auctionId). - Validates the bid against the current auction metadata (retrieved via
auctionMetadataService.getCurrentAuctionMeta(roomId)). - If the bid is valid (i.e. the bid is on the currently queued listing):
- It updates the bid price in the metadata.
- It calls
markCurrentListingSoldAndNotifyUsersto mark the listing as sold.
- When a bid event is received (via a subscription from
- Marking the Listing as Sold
(AuctionEngine.markCurrentListingSoldAndNotifyUsers)
- This function:
- Clears the current price decrement interval.
- Calls
auctionService.markAuctionItemSoldAndPlaceOrderto update the database (mark the item as sold, create an order, etc.). - Calls
auctionService.updateAuctionItemStatusto update the item’s status in the cache. - Publishes a "product sold" event via
redisEventHandler.publishProductSoldto notify all connected clients. - If the sale is successful, it calls
prepareNextProductInRoomIdWithDelayto load the next auction item.
- This function:
- Handling a Bid (AuctionEngine.handlePlaceBid)
-
Price Updates and Unsold Items
- Price Update Handler (AuctionEngine.priceUpdateHandler)
- Runs at regular intervals (set by an interval timer).
- Retrieves the current auction metadata.
- Calculates a new price by decrementing from the original price.
- If the price falls below the minimum without a bid:
- Updates the metadata to indicate no sale.
- Clears the update interval.
- Calls
markCurrentListingUnsoldto mark the listing as unsold. - Then calls
prepareNextProductInRoomIdWithDelayto move to the next product.
- If the new price is still above the minimum, it updates the round number
and publishes the new price via
redisEventHandler.publishPriceUpdate, then updates the metadata.
- Price Update Handler (AuctionEngine.priceUpdateHandler)
-
Ending the Auction
- AuctionEngine.checkAndHandleIfAuctionEnd is called in the product preparation process to verify if there are any remaining items.
- If no items remain, it calls AuctionEngine.triggerAuctionEnd, which:
- Clears all intervals.
- Publishes an auction end (or cancel) event.
- Cleans up auction items from the cache and database by calling functions
in the
AuctionService(e.g.,deleteAuctionItemsFromCacheAndDB,removeOngoingAuction). - Updates the final auction status in the database (COMPLETED or CANCELLED).
- Updates sold and unsold counts via
auctionService.updateAuctionProductIdsandauctionService.updateAuctionProductCounts.
-
Endpoint Trigger (
/start/:auctionId)- Router calls:
schedulerService.unscheduleAuction(auctionId)auctionEngine.startAuctionInRoom(auctionId, roomId)
- Router calls:
-
AuctionEngine.startAuctionInRoom
- Checks ongoing auctions via
auctionService.getOngoingAuctions - If not ongoing:
- Calls
auctionService.insertOngoingAuction(auctionId) - Calls
auctionService.queueInAuctionItemsInDBAndCache(auctionId) - Calls
auctionMetadataService.insertAuctionMetaForDay(auctionId)
- Calls
- Retrieves auction configuration with
auctionService.getAuctionInfoById(auctionId) - Publishes auction start using
redisEventHandler.publishAuctionStart(roomId) - Schedules first product with
prepareNextProductInRoomIdWithDelay(roomId, auctionId, delayOptions)
- Checks ongoing auctions via
-
prepareNextProductInRoomIdWithDelay
- Waits a short delay then:
- Publishes "preparing next" event via
redisEventHandler.publishPreparingNext(roomId) - Calls
prepareNextProductInRoomId(roomId, auctionId, delayOptions)
- Publishes "preparing next" event via
- Waits a short delay then:
-
prepareNextProductInRoomId
- Clears previous intervals.
- Retrieves current metadata
(
auctionMetadataService.getCurrentAuctionMeta(roomId)) - Gets auction item IDs via
auctionService.getAuctionItemIds(auctionId) - Checks if auction is over with
checkAndHandleIfAuctionEnd - Fetches next auction item with
auctionService.getAuctionItem - Updates auction item price using
auctionService.updateAuctionItemPrice - Creates and updates new auction metadata with
auctionMetadataService.updateAuctionMeta - Waits for a configured delay and initializes the price update cycle via
priceUpdateHandler
-
Price Update Cycle (priceUpdateHandler)
- Decrements the current price at regular intervals.
- Publishes price updates via
redisEventHandler.publishPriceUpdate - If price falls below minimum, calls
markCurrentListingUnsoldand then prepares the next product.
-
Bid Handling
- When a bid event is received,
handlePlaceBid(roomId, bidderMeta)is called by the Redis event listener. - This function:
- Acquires a lock.
- Validates the bid against current auction metadata.
- Calls
markCurrentListingSoldAndNotifyUsersto process the bid.
- markCurrentListingSoldAndNotifyUsers
- Clears the current update interval.
- Calls
auctionService.markAuctionItemSoldAndPlaceOrderto update DB and create an order. - Calls
auctionService.updateAuctionItemStatusto update the cache. - Publishes a "product sold" event via
redisEventHandler.publishProductSold - Calls
prepareNextProductInRoomIdWithDelayif successful.
- When a bid event is received,
-
Auction End
- When no more products remain,
checkAndHandleIfAuctionEndtriggerstriggerAuctionEnd. - triggerAuctionEnd
- Clears intervals.
- Publishes an auction end (or cancel) event.
- Cleans up auction data from cache and database.
- Updates auction status and product counts.
- When no more products remain,

