How to Design a Live Auction & Bidding System
Building a Production-Grade Real-Time Auction Platform — Bid Processing, Anti-Sniping, WebSocket Streaming & Fraud Prevention
1. Introduction and Why Real-Time Auctions are Hard
Live auctions represent one of the most demanding real-time systems in modern software engineering. Unlike traditional e-commerce where a user adds an item to a cart and checks out over minutes or hours, an auction platform must handle sub-second interactions where hundreds or thousands of concurrent bidders compete for items in real time. The margin for error is razor-thin: a dropped bid, a delayed notification, or a race condition can cost users thousands of dollars and destroy trust in the platform.
Consider the scale of major auction platforms. eBay processes over $100 billion in gross merchandise volume annually, with millions of active auctions at any given time. Heritage Auctions, the world's largest collectibles auctioneer, regularly sees bidding wars where a single item receives dozens of bids in the final seconds. Christie's and Sotheby's have moved their prestigious art and wine auctions online, demanding enterprise-grade reliability for high-value transactions. These platforms must simultaneously handle the complexities of real-time communication, financial transactions, fraud prevention, and regulatory compliance.
The fundamental challenges of building a live auction system can be distilled into five key areas:
- Real-time bid processing: Bids must be received, validated, and broadcast to all participants within 100 milliseconds. Any latency advantage for one bidder over another undermines the fairness of the auction.
- Concurrency and consistency: When two bidders place bids simultaneously, the system must deterministically resolve which bid is accepted without losing data or creating inconsistent state.
- Fraud prevention: Shill bidding (where sellers artificially inflate prices using fake accounts), bid sniping (placing bids in the last milliseconds), and payment fraud must all be detected and prevented.
- Auction lifecycle management: An auction transitions through multiple states (preview, live, extended, ended), each with different rules about who can see what and what actions are allowed.
- Scale and reliability: The system must handle flash crowds when high-profile auctions go live, while maintaining consistent performance for all participants.
This guide will walk you through every aspect of designing a production-grade live auction and bidding system, from auction theory fundamentals to the technical implementation of WebSocket streams, optimistic locking, anti-sniping timers, and fraud detection algorithms. We will use C# code examples throughout, with Mermaid diagrams to illustrate the architecture, and conclude with interview preparation questions that test deep understanding of the material.
Whether you are preparing for a system design interview at a major tech company, building an auction platform as a product, or simply expanding your understanding of real-time distributed systems, this guide provides the comprehensive knowledge you need.
2. Auction Types Deep Dive
Before diving into the system architecture, it is essential to understand the different auction formats the platform must support. Each auction type has distinct bidding rules, price discovery mechanisms, and user experience requirements that directly impact system design.
2.1 English Ascending Auction
The English auction is the most common and widely recognized format. Bidders openly place increasingly higher bids, and the auction continues until no participant is willing to bid higher. The highest bidder wins the item at their bid price. This format is used by major platforms like eBay and by traditional auction houses like Christie's and Sotheby's.
In an English auction, the current price is visible to all participants, creating a transparent and competitive environment. The system must broadcast each new high bid to all connected clients in real time. The auctioneer may set a reserve price (a minimum price below which the seller is unwilling to sell). If the final bid does not meet the reserve, the item is not sold.
2.2 Dutch Descending Auction
In a Dutch auction, the price starts high and is gradually reduced at predetermined intervals. The first bidder to accept the current price wins the item. This format is commonly used for perishable goods like flowers and fish at wholesale markets, and has been adapted for online platforms selling electronics and travel deals (e.g., Priceline's original model).
The system design challenge for Dutch auctions is different from English auctions. Instead of broadcasting bid updates, the system must broadcast price decrements at regular intervals. The first bid received at each price level wins. This requires precise timing and efficient first-come-first-served processing.
2.3 Sealed-Bid Auction
In a sealed-bid auction, all bidders submit their bids simultaneously without knowledge of other bids. After the submission deadline, all bids are revealed and the winner is determined. This format is commonly used in government procurement, real estate sales, and spectrum licensing. The key challenge is ensuring bid confidentiality until the reveal phase.
2.4 Vickrey Auction (Second-Price Sealed-Bid)
The Vickrey auction is a variant of the sealed-bid auction where the highest bidder wins but pays the second-highest bid price. Named after Nobel laureate William Vickrey, this auction type is strategically interesting because the dominant strategy for each bidder is to bid their true valuation. Google's original AdWords auction was based on a Vickrey-Clarke-Groves mechanism.
2.5 Penny Auction
Penny auctions charge bidders a fee for each bid placed (typically $0.50 to $1.00 per bid), and each bid incrementally increases the price by a small amount (usually one penny). The last bidder when the timer expires wins the item, often at a fraction of retail value. However, bidders who do not win lose their bid fees. This model has been controversial due to its gambling-like mechanics.
| Auction Type | Price Direction | Bid Visibility | Winner Determination | Payment |
|---|---|---|---|---|
| English Ascending | Upward | Public | Highest bid above reserve | Winning bid price |
| Dutch Descending | Downward | Current price only | First to accept | Acceptance price |
| Sealed-Bid | N/A | Hidden until reveal | Highest bid | Winning bid price |
| Vickrey | N/A | Hidden until reveal | Highest bid | Second-highest bid |
| Penny | Upward (fixed increment) | Public | Last bidder when timer expires | Final price + bid fees |
3. Functional and Non-Functional Requirements
3.1 Functional Requirements
- CRUD Auctions: Sellers can create, list, edit, and cancel auctions. Each auction supports item details (title, description, images, category), starting price, reserve price, bid increment, start time, and end time.
- Place Bids: Registered bidders can place bids on live auctions. The system validates the bid amount, bidder eligibility, and auction state before accepting.
- Proxy Bidding: Bidders can set a maximum bid amount, and the system automatically places incremental bids on their behalf, up to the maximum.
- Real-Time Updates: All connected bidders receive real-time updates of current bid, bid count, number of active bidders, and time remaining.
- Auction Lifecycle: The system manages auction transitions from preview through live, extended (anti-snipe), to ended states.
- Bid History: Complete bid history is visible to authorized participants, showing timestamp, bidder (anonymized or pseudonymous), and amount.
- Notifications: Bidders receive outbid notifications, auction ending warnings, and win confirmations via WebSocket, push, and email.
- Payment and Escrow: Winning bidders are directed to payment processing. Funds are held in escrow until the buyer confirms receipt.
- Search and Browse: Users can search, filter, and browse auctions by category, price range, time remaining, and popularity.
- User Management: Registration, authentication, seller verification, bidder reputation, and profile management.
3.2 Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Bid processing latency | Less than 100ms end-to-end | Fairness requires all bidders to have equal opportunity |
| WebSocket message delivery | Less than 50ms from bid acceptance to client receipt | Real-time experience for all participants |
| Concurrent users per auction | 10,000+ | High-profile auctions attract large crowds |
| Total concurrent auctions | 500,000+ | Scale for a global platform |
| Bids per second (global) | 50,000+ | Peak load during major auction events |
| Availability | 99.99% | Downtime during a live auction directly causes financial loss |
| Data durability | Zero data loss | Every bid is a financial commitment that cannot be lost |
| Audit trail | Complete, immutable | Legal and regulatory requirements for dispute resolution |
4. High-Level Architecture Overview
The auction platform follows a microservices architecture with clear separation of concerns. The core services include the Auction Service, Bid Processing Service, WebSocket Gateway, Notification Service, Payment Service, User Service, and Fraud Detection Service. An event bus (Kafka or RabbitMQ) serves as the backbone for asynchronous communication between services.
4.1 Service Responsibilities
- API Gateway: Routes requests, handles authentication, rate limiting, and request validation. Provides a unified entry point for all REST operations.
- WebSocket Gateway: Manages persistent connections for real-time bid streaming. Handles connection lifecycle, heartbeat monitoring, and message broadcasting to auction rooms.
- Auction Service: Manages auction CRUD operations, lifecycle state transitions, scheduling, and item metadata. Integrates with CDN for image delivery.
- Bid Processing Service: The heart of the system. Receives bids, validates them, processes them atomically, and publishes events. Must handle concurrency, optimistic locking, and proxy bidding.
- User Service: Manages registration, authentication, authorization, seller verification, bidder reputation, and KYC compliance.
- Payment Service: Processes payments, manages buyer premiums, integrates with payment gateways (Stripe, PayPal), and coordinates with the escrow service.
- Fraud Detection Service: Analyzes bid patterns in real time to detect shill bidding, collusion, and anomalous behavior. Feeds into the moderation queue.
- Search Service: Provides full-text search, filtering, and faceted browsing using Elasticsearch or a similar search engine.
- Notification Service: Delivers multi-channel notifications (WebSocket, push, email, SMS) for outbid alerts, auction ending warnings, and win confirmations.
5. Data Model and Storage Schema
The data model must support the full lifecycle of auctions and bids while maintaining referential integrity and enabling efficient queries. We use PostgreSQL as the primary relational database for its strong ACID guarantees, which are essential for financial transactions.
5.1 Core Entities
SQL
CREATE TABLE auctions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
seller_id UUID NOT NULL REFERENCES users(id),
title VARCHAR(255) NOT NULL,
description TEXT,
category_id UUID NOT NULL REFERENCES categories(id),
starting_price DECIMAL(12,2) NOT NULL,
reserve_price DECIMAL(12,2),
bid_increment DECIMAL(10,2) NOT NULL DEFAULT 1.00,
current_price DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
status VARCHAR(20) NOT NULL DEFAULT 'draft',
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
original_end TIMESTAMPTZ NOT NULL,
extension_secs INT NOT NULL DEFAULT 120,
max_extensions INT NOT NULL DEFAULT 5,
ext_count INT NOT NULL DEFAULT 0,
buyer_premium DECIMAL(5,2) NOT NULL DEFAULT 0,
bid_count INT NOT NULL DEFAULT 0,
winner_id UUID REFERENCES users(id),
winning_price DECIMAL(12,2),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
version INT NOT NULL DEFAULT 1
);
CREATE TABLE bids (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
auction_id UUID NOT NULL REFERENCES auctions(id),
bidder_id UUID NOT NULL REFERENCES users(id),
amount DECIMAL(12,2) NOT NULL,
is_proxy BOOLEAN NOT NULL DEFAULT FALSE,
is_winning BOOLEAN NOT NULL DEFAULT FALSE,
is_outbid BOOLEAN NOT NULL DEFAULT FALSE,
parent_bid_id UUID REFERENCES bids(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
version INT NOT NULL DEFAULT 1
);
CREATE TABLE auction_events (
id BIGSERIAL PRIMARY KEY,
auction_id UUID NOT NULL,
event_type VARCHAR(50) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE proxy_bids (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
auction_id UUID NOT NULL REFERENCES auctions(id),
bidder_id UUID NOT NULL REFERENCES users(id),
max_amount DECIMAL(12,2) NOT NULL,
current_bid_id UUID REFERENCES bids(id),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(auction_id, bidder_id)
);
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'bidder',
verified BOOLEAN NOT NULL DEFAULT FALSE,
reputation DECIMAL(3,2) NOT NULL DEFAULT 0.00,
kyc_status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT NOW()
);
5.2 Indexing Strategy
SQL
CREATE INDEX idx_bids_auction_winning
ON bids(auction_id, is_winning)
WHERE is_winning = TRUE;
CREATE INDEX idx_bids_auction_created
ON bids(auction_id, created_at DESC);
CREATE INDEX idx_bids_bidder
ON bids(bidder_id, created_at DESC);
CREATE INDEX idx_auctions_status_endtime
ON auctions(status, end_time)
WHERE status IN ('live', 'extended');
CREATE INDEX idx_auctions_category
ON auctions(category_id, status, end_time);
CREATE INDEX idx_proxy_bids_active
ON proxy_bids(auction_id, is_active)
WHERE is_active = TRUE;
5.3 Storage Strategy
| Data | Store | Reasoning |
|---|---|---|
| Auction metadata | PostgreSQL | ACID transactions for state transitions |
| Bids | PostgreSQL + Event Store | Durable writes with event sourcing for replay |
| Current bid state (hot) | Redis | Sub-millisecond reads for real-time bidding |
| Auction events | Kafka then S3/Parquet | Immutable log for audit trail and analytics |
| Item images | S3 + CloudFront CDN | Global low-latency delivery |
| Search index | Elasticsearch | Full-text search with faceted filtering |
| User sessions | Redis | Fast auth token validation |
6. API Design
The API follows RESTful conventions with clear resource-oriented endpoints. All authenticated endpoints require a JWT bearer token. Rate limits are enforced per-user and per-auction to prevent abuse.
6.1 Auction Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auctions | Create a new auction (seller only) |
| GET | /api/v1/auctions | List auctions with filters and pagination |
| GET | /api/v1/auctions/{id} | Get auction details |
| PUT | /api/v1/auctions/{id} | Edit auction (before start only) |
| DELETE | /api/v1/auctions/{id} | Cancel auction (before first bid) |
| GET | /api/v1/auctions/{id}/bids | Get bid history for an auction |
6.2 Bid Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auctions/{id}/bids | Place a bid on an auction |
| POST | /api/v1/auctions/{id}/proxy-bids | Set up proxy (auto) bidding |
| DELETE | /api/v1/auctions/{id}/proxy-bids | Cancel proxy bidding |
| GET | /api/v1/users/me/bids | Get current user's bid history |
6.3 Payment Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auctions/{id}/payment | Initiate payment for won auction |
| GET | /api/v1/users/me/purchases | List user's won auctions and payment status |
| POST | /api/v1/auctions/{id}/confirm-receipt | Buyer confirms receipt, releases escrow |
6.4 WebSocket Protocol
C#
public enum WsMessageType
{
// Client to Server
SubscribeAuction,
UnsubscribeAuction,
PlaceBid,
Ping,
// Server to Client
BidAccepted,
BidRejected,
Outbid,
AuctionExtended,
AuctionEnding,
AuctionEnded,
AuctionState,
Pong,
Error
}
public record WsMessage(
WsMessageType Type,
string AuctionId,
object Payload,
long Timestamp
);
The WebSocket protocol is binary-efficient, using JSON for simplicity but convertible to MessagePack or Protobuf for higher throughput. Each message includes a type discriminator, the relevant auction ID, a payload, and a server timestamp. The server timestamp is critical because it allows clients to synchronize their local timers with the server's clock, preventing timing-based cheating.
7. Auction Lifecycle Management
Every auction progresses through a well-defined set of states. The lifecycle engine manages transitions, enforces rules for each state, and triggers side effects (notifications, indexing, payment initiation) at the appropriate moments.
7.1 State Definitions
- Draft: Auction is being configured by the seller. Not visible to bidders. Bids cannot be placed.
- Preview: Auction is visible to all users, but bidding is not yet open. Allows potential bidders to research the item and set up proxy bids.
- Live: Bidding is active. Bids are accepted and broadcast in real time. The countdown timer is ticking.
- Extended: A bid was placed in the final moments, triggering an automatic time extension. The auction is still live but with extended time.
- Ended: The timer has expired without any further bids. The winner is determined. Payment process begins.
- Cancelled: The auction was cancelled by the seller (only allowed before any bids are placed) or by an administrator.
7.2 Lifecycle Engine Implementation
C#
public class AuctionLifecycleEngine
{
private readonly IAuctionRepository _auctionRepo;
private readonly IEventPublisher _eventPublisher;
private readonly IAuctionScheduler _scheduler;
public async Task<Auction> TransitionAsync(
Guid auctionId,
AuctionStatus targetStatus)
{
var auction = await _auctionRepo.GetByIdAsync(auctionId);
if (!IsValidTransition(auction.Status, targetStatus))
{
throw new InvalidTransitionException(
auction.Status, targetStatus);
}
auction.Status = targetStatus;
auction.UpdatedAt = DateTime.UtcNow;
switch (targetStatus)
{
case AuctionStatus.Live:
await HandleAuctionGoingLive(auction);
break;
case AuctionStatus.Extended:
await HandleAuctionExtended(auction);
break;
case AuctionStatus.Ended:
await HandleAuctionEnded(auction);
break;
}
await _auctionRepo.UpdateAsync(auction);
await _eventPublisher.PublishAsync(
new AuctionStateChangedEvent(auction));
return auction;
}
private bool IsValidTransition(
AuctionStatus current, AuctionStatus target)
{
return (current, target) switch
{
(Draft, Preview) => true,
(Draft, Cancelled) => true,
(Preview, Live) => true,
(Preview, Cancelled) => true,
(Live, Extended) => true,
(Live, Ended) => true,
(Extended, Extended) => true,
(Extended, Ended) => true,
_ => false
};
}
private async Task HandleAuctionEnded(Auction auction)
{
var winningBid = await GetWinningBidAsync(auction.Id);
if (winningBid != null &&
auction.ReservePrice.HasValue &&
winningBid.Amount < auction.ReservePrice.Value)
{
await _eventPublisher.PublishAsync(
new ReserveNotMetEvent(auction.Id));
}
else if (winningBid != null)
{
auction.WinnerId = winningBid.BidderId;
auction.WinningPrice = winningBid.Amount;
await _eventPublisher.PublishAsync(
new AuctionWonEvent(auction.Id, winningBid));
}
}
}
8. Real-Time Bid Processing Engine
The bid processing engine is the most critical component of the entire system. It must accept bids from multiple sources (HTTP API, WebSocket, proxy bidding system), validate them, process them atomically, and broadcast the results, all within 100 milliseconds end-to-end.
8.1 Bid Processing Pipeline
8.2 Core Bid Processing Logic
C#
public class BidProcessingService
{
private readonly IAuctionRepository _auctionRepo;
private readonly IBidRepository _bidRepo;
private readonly IEventPublisher _eventPublisher;
private readonly IProxyBidEngine _proxyBidEngine;
private readonly IFraudDetector _fraudDetector;
private readonly ILogger<BidProcessingService> _logger;
private const int MaxRetries = 3;
public async Task<BidResult> ProcessBidAsync(
PlaceBidCommand command)
{
for (int attempt = 0; attempt < MaxRetries; attempt++)
{
try
{
return await ProcessBidWithRetryAsync(command);
}
catch (OptimisticConcurrencyException)
{
_logger.LogWarning(
"Bid conflict on attempt {Attempt}", attempt + 1);
if (attempt == MaxRetries - 1) throw;
}
}
throw new BidProcessingException(
"Failed to process bid after maximum retries");
}
private async Task<BidResult> ProcessBidWithRetryAsync(
PlaceBidCommand command)
{
var auction = await _auctionRepo.GetByIdAsync(
command.AuctionId);
if (auction.Status != AuctionStatus.Live &&
auction.Status != AuctionStatus.Extended)
{
return BidResult.Rejected("Auction is not live");
}
var minBid = CalculateMinimumBid(auction);
if (command.Amount < minBid)
{
return BidResult.Rejected(
$"Minimum bid is {minBid:C}");
}
var fraudCheck = await _fraudDetector.EvaluateAsync(
command.BidderId, command.AuctionId, command.Amount);
await using var transaction =
await _bidRepo.BeginTransactionAsync();
var currentHighBid = await _bidRepo
.GetWinningBidAsync(command.AuctionId);
var newBid = new Bid
{
Id = Guid.NewGuid(),
AuctionId = command.AuctionId,
BidderId = command.BidderId,
Amount = command.Amount,
IsProxy = command.IsProxy,
IsWinning = true,
CreatedAt = DateTime.UtcNow,
Version = 1
};
if (currentHighBid != null)
{
currentHighBid.IsWinning = false;
currentHighBid.IsOutbid = true;
await _bidRepo.UpdateAsync(currentHighBid);
}
await _bidRepo.InsertAsync(newBid);
var previousPrice = auction.CurrentPrice;
auction.CurrentPrice = command.Amount;
auction.BidCount += 1;
auction.Version += 1;
await _auctionRepo.UpdateAsync(auction);
await transaction.CommitAsync();
var bidPlacedEvent = new BidPlacedEvent
{
AuctionId = auction.Id,
BidId = newBid.Id,
BidderId = command.BidderId,
Amount = command.Amount,
PreviousPrice = previousPrice,
BidCount = auction.BidCount,
Timestamp = newBid.CreatedAt
};
await _eventPublisher.PublishAsync(bidPlacedEvent);
var timeRemaining = auction.EndTime - DateTime.UtcNow;
if (timeRemaining.TotalSeconds <= auction.ExtensionSeconds)
{
await ExtendAuctionAsync(auction);
}
_ = Task.Run(async () =>
{
await _proxyBidEngine.ProcessOutbidAsync(
auction, newBid);
});
return BidResult.Accepted(newBid.Id, command.Amount);
}
private decimal CalculateMinimumBid(Auction auction)
{
if (auction.BidCount == 0)
return auction.StartingPrice;
return auction.CurrentPrice + auction.BidIncrement;
}
}
9. WebSocket Bid Streaming
WebSocket connections provide the low-latency, bidirectional communication channel essential for real-time auction experiences. Each connected client subscribes to one or more auction rooms, receiving instant updates as bids are placed.
9.1 Connection Management
C#
public class AuctionWebSocketHandler
{
private static readonly ConcurrentDictionary<
string, ConcurrentBag<WebSocket>> _auctionRooms = new();
private static readonly ConcurrentDictionary<
WebSocket, string> _connectionAuctions = new();
public async Task HandleConnectionAsync(
WebSocket socket, HttpContext context)
{
var userId = GetUserIdFromContext(context);
try
{
await SendAsync(socket, new WsMessage(
WsMessageType.Connected,
null,
new { ConnectionId = Guid.NewGuid() },
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
var buffer = new byte[1024 * 4];
var segment = new ArraySegment<byte>(buffer);
while (socket.State == WebSocketState.Open)
{
var result = await socket.ReceiveAsync(
segment, CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"", CancellationToken.None);
break;
}
var message = JsonSerializer.Deserialize<
WsClientMessage>(
Encoding.UTF8.GetString(
buffer, 0, result.Count));
await ProcessClientMessageAsync(
socket, message, userId);
}
}
finally
{
CleanupConnection(socket);
}
}
private async Task SubscribeToAuction(
WebSocket socket, string auctionId, string userId)
{
var room = _auctionRooms.GetOrAdd(
auctionId, _ => new ConcurrentBag<WebSocket>());
room.Add(socket);
_connectionAuctions[socket] = auctionId;
var state = await GetAuctionStateAsync(auctionId);
await SendAsync(socket, new WsMessage(
WsMessageType.AuctionState,
auctionId, state,
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
}
public async Task BroadcastBidAsync(
string auctionId, BidPlacedEvent bidEvent)
{
if (!_auctionRooms.TryGetValue(
auctionId, out var sockets))
return;
var message = new WsMessage(
WsMessageType.BidAccepted,
auctionId,
new
{
BidId = bidEvent.BidId,
Amount = bidEvent.Amount,
BidCount = bidEvent.BidCount,
BidderAnonymized = AnonymizeBidder(
bidEvent.BidderId)
},
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
var json = JsonSerializer.Serialize(message);
var bytes = Encoding.UTF8.GetBytes(json);
var segment = new ArraySegment<byte>(bytes);
var tasks = sockets
.Where(s => s.State == WebSocketState.Open)
.Select(async socket =>
{
try
{
await socket.SendAsync(
segment,
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to send to WebSocket");
CleanupConnection(socket);
}
});
await Task.WhenAll(tasks);
}
private string AnonymizeBidder(string bidderId)
{
return bidderId[..3] + "***";
}
}
9.2 Scalability with Redis Pub/Sub
When the WebSocket gateway is deployed across multiple instances, a bid placed on instance A must be broadcast to connections on instance B. Redis Pub/Sub serves as the cross-instance message bus.
C#
public class DistributedBidBroadcaster : IBidBroadcaster
{
private readonly IConnectionMultiplexer _redis;
private readonly ISubscriber _subscriber;
private readonly AuctionWebSocketHandler _wsHandler;
public DistributedBidBroadcaster(IConnectionMultiplexer redis)
{
_redis = redis;
_subscriber = redis.GetSubscriber();
}
public async Task InitializeAsync()
{
await _subscriber.SubscribeAsync(
"auction:bids", async (channel, message) =>
{
var bidEvent = JsonSerializer.Deserialize<
BidPlacedEvent>(message);
await _wsHandler.BroadcastBidAsync(
bidEvent.AuctionId, bidEvent);
});
}
public async Task BroadcastAsync(BidPlacedEvent bidEvent)
{
await _subscriber.PublishAsync(
"auction:bids",
JsonSerializer.Serialize(bidEvent));
}
}
9.3 Connection Limits and Heartbeats
To prevent resource exhaustion, each user is limited to a maximum number of concurrent WebSocket connections (typically 5). A heartbeat mechanism (ping/pong every 30 seconds) detects and cleans up stale connections. Connections that miss three consecutive heartbeats are forcibly closed.
10. Bid Validation and Fraud Prevention
Bid validation is a multi-layered process that ensures every accepted bid is legitimate, properly formatted, and complies with the auction rules. Fraud prevention goes further, analyzing behavioral patterns to detect malicious actors.
10.1 Validation Layers
| Layer | Validations | Failure Action |
|---|---|---|
| Input | Bid amount is positive, within currency precision, and meets minimum increment | Reject with 400 |
| Authentication | User is authenticated, email verified, account not suspended | Reject with 401 or 403 |
| Auction State | Auction is in Live or Extended status, current time is within auction window | Reject with 409 |
| Bidder Eligibility | Bidder is not the seller, not blocked, KYC verified if required | Reject with 403 |
| Financial | Bidder has sufficient account balance or credit limit | Reject with 402 |
| Race Condition | Optimistic lock version matches, bid amount exceeds current high bid | Retry or reject with 409 |
| Fraud Detection | Pattern analysis (shill bidding, velocity, network analysis) | Accept but flag for review |
10.2 Fraud Detection Rules
C#
public class FraudDetectionEngine
{
private readonly IDatabase _redis;
private readonly IFraudRule[] _rules;
public FraudDetectionEngine(
IDatabase redis, IFraudRule[] rules)
{
_redis = redis;
_rules = rules;
}
public async Task<FraudCheckResult> EvaluateAsync(
string bidderId, string auctionId, decimal amount)
{
var context = new FraudContext
{
BidderId = bidderId,
AuctionId = auctionId,
Amount = amount,
Timestamp = DateTime.UtcNow
};
var results = await Task.WhenAll(
_rules.Select(rule => rule.EvaluateAsync(context)));
var suspicious = results
.Where(r => r.IsSuspicious)
.ToList();
if (suspicious.Any())
{
return new FraudCheckResult
{
IsSuspicious = true,
Reason = string.Join("; ",
suspicious.Select(r => r.Reason)),
Confidence = suspicious.Max(r => r.Confidence)
};
}
return FraudCheckResult.Clean;
}
}
public class ShillBiddingRule : IFraudRule
{
private readonly IAuctionRepository _auctionRepo;
public async Task<RuleResult> EvaluateAsync(
FraudContext context)
{
var auction = await _auctionRepo.GetByIdAsync(
context.AuctionId);
if (auction.SellerId == context.BidderId)
{
return RuleResult.Suspicious(
"Bidder is the seller",
confidence: 1.0,
ruleName: "SHILL_BIDDING_SELF");
}
return RuleResult.Clean;
}
}
public class RapidEscalationRule : IFraudRule
{
private readonly IDatabase _redis;
public async Task<RuleResult> EvaluateAsync(
FraudContext context)
{
var key = $"fraud:bid_count:{context.AuctionId}";
var count = await _redis.StringIncrementAsync(key);
if (count == 1)
await _redis.KeyExpireAsync(key,
TimeSpan.FromMinutes(10));
if (count > 50)
{
return RuleResult.Suspicious(
$"Rapid escalation: {count} bids in window",
confidence: 0.7,
ruleName: "RAPID_ESCALATION");
}
return RuleResult.Clean;
}
}
public class SelfBiddingRule : IFraudRule
{
private readonly IDatabase _redis;
public async Task<RuleResult> EvaluateAsync(
FraudContext context)
{
var currentWinner = await _redis.HashGetAsync(
$"auction:{context.AuctionId}:state",
"winner_id");
if (currentWinner == context.BidderId)
{
return RuleResult.Suspicious(
"Bidder is outbidding themselves",
confidence: 0.8,
ruleName: "SELF_BIDDING");
}
return RuleResult.Clean;
}
}
10.3 Shill Bidding Detection
Shill bidding (where a seller or their accomplices artificially inflate the price) is one of the most damaging forms of auction fraud. Detection involves analyzing multiple signals:
- Relationship analysis: Check if bidders share IP addresses, device fingerprints, shipping addresses, or payment methods with the seller.
- Bidding pattern analysis: Shill bids typically follow a pattern: they appear just after a legitimate bid, increase the price by small increments, and stop once the auction reaches a target price.
- Account age analysis: Newly created accounts that only bid on a single seller's items are suspicious.
- Winning rate analysis: Bidders who rarely win but frequently participate in specific sellers' auctions may be shills.
- Geographic analysis: Bids from the same geographic region as the seller, especially from IP addresses associated with VPNs or proxies.
11. Concurrent Bid Handling and Optimistic Locking
When two bidders place bids at nearly the same time, the system must deterministically process them. The first bid (by server timestamp) is accepted; the second must either be rejected or retried with the updated state. Optimistic locking ensures consistency without the performance penalty of pessimistic locks.
11.1 Optimistic Locking Pattern
C#
public class OptimisticBidProcessor
{
private readonly AuctionDbContext _db;
public async Task<BidResult> ProcessBidOptimisticAsync(
PlaceBidCommand command)
{
const int MaxRetries = 5;
for (int attempt = 0; attempt < MaxRetries; attempt++)
{
using var transaction = await _db.Database
.BeginTransactionAsync(IsolationLevel.ReadCommitted);
try
{
var auction = await _db.Auctions
.Where(a => a.Id == command.AuctionId)
.FirstOrDefaultAsync();
if (auction == null)
return BidResult.NotFound();
if (auction.Status != AuctionStatus.Live &&
auction.Status != AuctionStatus.Extended)
return BidResult.Rejected("Auction not active");
var minBid = auction.BidCount == 0
? auction.StartingPrice
: auction.CurrentPrice + auction.BidIncrement;
if (command.Amount < minBid)
return BidResult.Rejected(
$"Minimum bid: {minBid}");
var expectedVersion = auction.Version;
auction.CurrentPrice = command.Amount;
auction.BidCount += 1;
auction.Version += 1;
var bid = new Bid
{
Id = Guid.NewGuid(),
AuctionId = command.AuctionId,
BidderId = command.BidderId,
Amount = command.Amount,
CreatedAt = DateTime.UtcNow
};
_db.Bids.Add(bid);
var rowsAffected = await _db.Database
.ExecuteSqlRawAsync(
@"UPDATE auctions
SET current_price = {0},
bid_count = {1},
version = version + 1,
updated_at = NOW()
WHERE id = {2} AND version = {3}",
command.Amount,
auction.BidCount,
command.AuctionId,
expectedVersion);
if (rowsAffected == 0)
{
await transaction.RollbackAsync();
await Task.Delay(
Random.Shared.Next(5, 25));
continue;
}
await transaction.CommitAsync();
return BidResult.Accepted(
bid.Id, command.Amount);
}
catch (Exception)
{
await transaction.RollbackAsync();
throw;
}
}
return BidResult.Rejected(
"Too many concurrent bids. Please try again.");
}
}
11.2 Distributed Locking with Redis
For scenarios where optimistic locking alone is insufficient (e.g., proxy bidding must process sequentially), a distributed lock using Redis provides mutual exclusion with automatic expiration.
C#
public class DistributedBidLock
{
private readonly IDatabase _redis;
private static readonly TimeSpan HoldTimeout =
TimeSpan.FromSeconds(2);
public async Task<IDisposable?> AcquireBidLockAsync(
string auctionId)
{
var lockKey = $"lock:auction:{auctionId}:bid";
var lockValue = Guid.NewGuid().ToString();
var acquired = await _redis.StringSetAsync(
lockKey, lockValue, HoldTimeout,
When.NotExists);
if (!acquired)
return null;
return new RedisLockReleaser(_redis, lockKey, lockValue);
}
}
internal class RedisLockReleaser : IDisposable
{
private readonly IDatabase _redis;
private readonly string _key;
private readonly string _value;
private bool _disposed;
public RedisLockReleaser(
IDatabase redis, string key, string value)
{
_redis = redis;
_key = key;
_value = value;
}
public void Dispose()
{
if (_disposed) return;
var script = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
_redis.ScriptEvaluate(script,
new RedisKey[] { _key },
new RedisValue[] { _value });
_disposed = true;
}
}
11.3 Race Condition Analysis
Consider this scenario: Bidder A places a $100 bid, and simultaneously Bidder B places a $105 bid. Both bids arrive within the same millisecond window. The system must ensure that both bids are processed correctly: A's bid becomes the current high bid first, then B's bid supersedes it. Optimistic locking guarantees this ordering even under extreme concurrency.
| Scenario | Without Locking | With Optimistic Lock |
|---|---|---|
| Bid A ($100) and Bid B ($105) arrive simultaneously | Either could win (non-deterministic, potential data loss) | Bid A wins first, Bid B retries and supersedes |
| Proxy bid responds to outbid while user places manual bid | Race condition: two bids at same amount | Proxy bid processes first, manual bid rejected as too low |
| Auction ends while bid is in transit | Bid might be accepted on expired auction | Auction status check fails, bid rejected |
12. Anti-Sniping Mechanisms
Bid sniping is the practice of placing a bid in the final seconds of an auction, giving other bidders no time to respond. While legal, sniping reduces auction revenue and creates an unfair advantage for bidders with faster internet connections or automated sniping tools. Anti-sniping mechanisms automatically extend the auction when bids are placed near the end.
12.1 Time Extension Logic
C#
public class AntiSnipingService
{
private readonly IAuctionRepository _auctionRepo;
private readonly IAuctionScheduler _scheduler;
private readonly IEventPublisher _eventPublisher;
public async Task<bool> CheckAndExtendAsync(
string auctionId, decimal bidAmount)
{
var auction = await _auctionRepo.GetByIdAsync(auctionId);
var timeRemaining = auction.EndTime - DateTime.UtcNow;
if (timeRemaining.TotalSeconds <=
auction.ExtensionSeconds &&
auction.ExtensionCount < auction.MaxExtensions)
{
var newEndTime = DateTime.UtcNow.AddSeconds(
auction.ExtensionSeconds);
auction.EndTime = newEndTime;
auction.ExtensionCount += 1;
auction.Status = AuctionStatus.Extended;
await _auctionRepo.UpdateAsync(auction);
await _scheduler.RescheduleEndAsync(
auctionId, newEndTime);
await _eventPublisher.PublishAsync(
new AuctionExtendedEvent
{
AuctionId = auctionId,
NewEndTime = newEndTime,
ExtensionCount = auction.ExtensionCount,
Reason = "Anti-snipe extension"
});
return true;
}
return false;
}
}
12.2 Extension Configuration
| Parameter | Default | Description |
|---|---|---|
| extension_seconds | 120 | Seconds added to end time when anti-snipe triggers |
| max_extensions | 5 | Maximum number of times an auction can be extended |
| sniping_window | extension_seconds | Time window in which a bid triggers extension |
| extension_notification | true | Whether to notify all bidders of the extension |
12.3 Countdown Timer Synchronization
Clients must synchronize their countdown timers with the server to prevent discrepancies. The server sends its current timestamp with every bid update, and the client adjusts its local timer accordingly. A periodic sync (every 10 seconds) compensates for clock drift.
C#
public class CountdownSynchronizer
{
private TimeSpan _serverClientOffset;
public void CalibrateWithServer(DateTime serverTime)
{
var clientTime = DateTime.UtcNow;
_serverClientOffset = serverTime - clientTime;
}
public DateTime GetServerTime()
{
return DateTime.UtcNow + _serverClientOffset;
}
public TimeSpan GetTimeRemaining(DateTime auctionEndTime)
{
var remaining = auctionEndTime - GetServerTime();
return remaining > TimeSpan.Zero
? remaining
: TimeSpan.Zero;
}
}
13. Event Sourcing and Bid History
Event sourcing stores every state change as an immutable event, providing a complete audit trail. For auction systems, this is not just a best practice but a requirement for dispute resolution, regulatory compliance, and debugging.
13.1 Event Store Schema
C#
public abstract record AuctionEvent
{
public Guid EventId { get; init; } = Guid.NewGuid();
public Guid AuctionId { get; init; }
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
public int SequenceNumber { get; init; }
}
public record BidPlacedEvent : AuctionEvent
{
public Guid BidId { get; init; }
public Guid BidderId { get; init; }
public decimal Amount { get; init; }
public decimal PreviousPrice { get; init; }
public bool IsProxy { get; init; }
}
public record AuctionCreatedEvent : AuctionEvent
{
public Guid SellerId { get; init; }
public string Title { get; init; } = "";
public decimal StartingPrice { get; init; }
public decimal? ReservePrice { get; init; }
public DateTime StartTime { get; init; }
public DateTime EndTime { get; init; }
}
public record AuctionExtendedEvent : AuctionEvent
{
public DateTime NewEndTime { get; init; }
public int ExtensionCount { get; init; }
public string Reason { get; init; } = "";
}
public record AuctionEndedEvent : AuctionEvent
{
public Guid? WinnerId { get; init; }
public decimal? WinningPrice { get; init; }
public int TotalBids { get; init; }
public string EndReason { get; init; } = "";
}
public record ProxyBidCreatedEvent : AuctionEvent
{
public Guid BidderId { get; init; }
public decimal MaxAmount { get; init; }
}
public record ProxyBidTriggeredEvent : AuctionEvent
{
public Guid BidderId { get; init; }
public Guid TriggeredByBidId { get; init; }
public decimal ProxyBidAmount { get; init; }
}
13.2 Rebuilding State from Events
C#
public class AuctionAggregate
{
private readonly List<AuctionEvent> _events = new();
public Guid Id { get; private set; }
public decimal CurrentPrice { get; private set; }
public Guid? CurrentWinnerId { get; private set; }
public int BidCount { get; private set; }
public AuctionStatus Status { get; private set; }
public DateTime EndTime { get; private set; }
public int Version { get; private set; }
public static AuctionAggregate FromEvents(
IEnumerable<AuctionEvent> events)
{
var aggregate = new AuctionAggregate();
foreach (var @event in events)
{
aggregate.Apply(@event);
}
return aggregate;
}
public void Apply(AuctionEvent @event)
{
switch (@event)
{
case AuctionCreatedEvent e:
Id = e.AuctionId;
CurrentPrice = e.StartingPrice;
Status = AuctionStatus.Preview;
EndTime = e.EndTime;
break;
case BidPlacedEvent e:
CurrentPrice = e.Amount;
CurrentWinnerId = e.BidderId;
BidCount += 1;
Status = AuctionStatus.Live;
break;
case AuctionExtendedEvent e:
EndTime = e.NewEndTime;
Status = AuctionStatus.Extended;
break;
case AuctionEndedEvent e:
Status = AuctionStatus.Ended;
CurrentWinnerId = e.WinnerId;
CurrentPrice = e.WinningPrice ?? CurrentPrice;
break;
}
Version = @event.SequenceNumber;
_events.Add(@event);
}
}
14. Proxy Bidding System
Proxy bidding (also called auto-bidding or maximum bidding) allows bidders to specify the maximum amount they are willing to pay. The system then automatically places incremental bids on their behalf, only bidding enough to maintain their position as the high bidder. This is the same mechanism used by eBay's "Place Bid" feature.
14.1 Proxy Bidding Algorithm
C#
public class ProxyBidEngine
{
private readonly IAuctionRepository _auctionRepo;
private readonly IBidRepository _bidRepo;
private readonly IBidProcessingService _bidService;
private readonly IEventPublisher _eventPublisher;
public async Task<ProxyBidResult> CreateProxyBidAsync(
Guid auctionId, Guid bidderId, decimal maxAmount)
{
var auction = await _auctionRepo.GetByIdAsync(auctionId);
if (auction.Status != AuctionStatus.Live &&
auction.Status != AuctionStatus.Extended)
{
return ProxyBidResult.Rejected(
"Auction is not active");
}
var minBid = auction.BidCount == 0
? auction.StartingPrice
: auction.CurrentPrice + auction.BidIncrement;
if (maxAmount < minBid)
{
return ProxyBidResult.Rejected(
$"Maximum must be at least {minBid}");
}
var existingProxy = await _bidRepo
.GetActiveProxyBidAsync(auctionId, bidderId);
if (existingProxy != null)
{
existingProxy.MaxAmount = maxAmount;
await _bidRepo.UpdateProxyBidAsync(existingProxy);
}
else
{
var proxyBid = new ProxyBid
{
Id = Guid.NewGuid(),
AuctionId = auctionId,
BidderId = bidderId,
MaxAmount = maxAmount,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
await _bidRepo.InsertProxyBidAsync(proxyBid);
}
if (auction.CurrentPrice < maxAmount)
{
var immediateBidAmount = Math.Min(
auction.CurrentPrice + auction.BidIncrement,
maxAmount);
await _bidService.ProcessBidAsync(
new PlaceBidCommand
{
AuctionId = auctionId,
BidderId = bidderId,
Amount = immediateBidAmount,
IsProxy = true
});
}
await _eventPublisher.PublishAsync(
new ProxyBidCreatedEvent
{
AuctionId = auctionId,
BidderId = bidderId,
MaxAmount = maxAmount
});
return ProxyBidResult.Created(maxAmount);
}
public async Task ProcessOutbidAsync(
Auction auction, Bid outbiddingBid)
{
var proxyBids = await _bidRepo
.GetActiveProxyBidsForAuctionAsync(
auction.Id, excludeBidderId: outbiddingBid.BidderId);
foreach (var proxy in proxyBids
.OrderByDescending(p => p.MaxAmount))
{
if (proxy.MaxAmount <= outbiddingBid.Amount)
{
proxy.IsActive = false;
await _bidRepo.UpdateProxyBidAsync(proxy);
continue;
}
var proxyBidAmount = Math.Min(
outbiddingBid.Amount + auction.BidIncrement,
proxy.MaxAmount);
var result = await _bidService.ProcessBidAsync(
new PlaceBidCommand
{
AuctionId = auction.Id,
BidderId = proxy.BidderId,
Amount = proxyBidAmount,
IsProxy = true
});
if (result.IsSuccess)
{
proxy.CurrentBidId = result.BidId;
await _bidRepo.UpdateProxyBidAsync(proxy);
await _eventPublisher.PublishAsync(
new ProxyBidTriggeredEvent
{
AuctionId = auction.Id,
BidderId = proxy.BidderId,
TriggeredByBidId = outbiddingBid.Id,
ProxyBidAmount = proxyBidAmount
});
break;
}
}
}
}
14.2 Proxy Bid Priority Rules
When multiple proxy bids compete, priority is determined by two factors in order:
- Maximum amount: The bidder with the highest maximum bid gets priority.
- Time of submission: If two proxy bids have the same maximum, the one submitted first wins (first-come-first-served).
| Scenario | Proxy A (max $200, first) | Proxy B (max $180, second) | Result |
|---|---|---|---|
| Both active | Wins at $181 | Loses | A wins, pays $181 |
| Manual bid of $150 | Auto-bids to $155 | Exceeded | A wins at $155 |
| Manual bid of $205 | Exceeded | Exceeded | Manual bidder wins at $205 |
15. Auction Scheduling and Multiple Rooms
A platform may host hundreds of thousands of auctions simultaneously. The scheduling system must efficiently manage start times, end times, and timer-based state transitions for all active auctions.
15.1 Auction Timer Architecture
15.2 Timer Implementation
C#
public class AuctionTimerService : BackgroundService
{
private readonly IAuctionRepository _auctionRepo;
private readonly IEventPublisher _eventPublisher;
private readonly ILogger<AuctionTimerService> _logger;
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessTimersAsync();
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error processing auction timers");
}
await Task.Delay(
TimeSpan.FromSeconds(1), stoppingToken);
}
}
private async Task ProcessTimersAsync()
{
var now = DateTime.UtcNow;
var toStart = await _auctionRepo
.GetAuctionsDueForTransitionAsync(
AuctionStatus.Preview, now);
foreach (var auction in toStart)
{
auction.Status = AuctionStatus.Live;
await _auctionRepo.UpdateAsync(auction);
await _eventPublisher.PublishAsync(
new AuctionStartedEvent(auction.Id));
}
var toEnd = await _auctionRepo
.GetAuctionsDueForTransitionAsync(
AuctionStatus.Live, now);
var extendedToEnd = await _auctionRepo
.GetAuctionsDueForTransitionAsync(
AuctionStatus.Extended, now);
foreach (var auction in toEnd.Concat(extendedToEnd))
{
auction.Status = AuctionStatus.Ended;
await _auctionRepo.UpdateAsync(auction);
await _eventPublisher.PublishAsync(
new AuctionEndedEvent(auction.Id));
}
}
}
15.3 Auction Room Management
An auction room is a logical grouping of connections subscribed to a specific auction. For very large auctions (e.g., a high-profile art sale with 50,000+ viewers), the room may be sharded across multiple WebSocket gateway instances.
C#
public class AuctionRoomManager
{
private readonly IDistributedCache _cache;
private readonly ISubscriber _redisPubSub;
private const int RoomShardCount = 8;
public async Task<string> JoinRoomAsync(
string auctionId, string connectionId)
{
var shard = ComputeShard(auctionId, connectionId);
var roomKey = $"room:{auctionId}:shard:{shard}";
await _cache.SetStringAsync(
$"{roomKey}:{connectionId}",
connectionId,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromHours(2)
});
return $"auction:{auctionId}:shard:{shard}";
}
public async Task<int> GetRoomSizeAsync(string auctionId)
{
int totalSize = 0;
for (int i = 0; i < RoomShardCount; i++)
{
var keys = await _cache.SearchKeysAsync(
$"room:{auctionId}:shard:{i}:*");
totalSize += keys.Count;
}
return totalSize;
}
private int ComputeShard(
string auctionId, string connectionId)
{
var hash = (auctionId + connectionId).GetHashCode();
return Math.Abs(hash) % RoomShardCount;
}
}
16. Reserve Price Management
A reserve price is the minimum price at which the seller is willing to sell. If the final bid does not meet the reserve, the auction ends without a sale. The reserve price is hidden from bidders, though they can see whether the reserve has been met.
16.1 Reserve Price Logic
C#
public class ReservePriceService
{
public ReserveStatus CheckReserve(
Auction auction, decimal bidAmount)
{
if (!auction.ReservePrice.HasValue)
return ReserveStatus.NoReserve;
if (bidAmount >= auction.ReservePrice.Value)
return ReserveStatus.Met;
if (bidAmount >= auction.ReservePrice.Value * 0.9m)
return ReserveStatus.NearlyMet;
return ReserveStatus.NotMet;
}
public string GetReserveDisplayText(
Auction auction, decimal currentPrice)
{
var status = CheckReserve(auction, currentPrice);
return status switch
{
ReserveStatus.NoReserve => "No reserve price",
ReserveStatus.Met => "Reserve price met!",
ReserveStatus.NearlyMet =>
"You are close to the reserve price",
ReserveStatus.NotMet =>
"Reserve price not yet met",
_ => ""
};
}
}
public enum ReserveStatus
{
NoReserve,
Met,
NearlyMet,
NotMet
}
18. Escrow Service and Seller Verification
For high-value items (art, antiques, jewelry, collectibles), an escrow service holds the buyer's payment until the item is delivered and verified. This protects both parties: the buyer is assured they will receive the item, and the seller is assured they will be paid.
18.1 Escrow Workflow
C#
public class EscrowService
{
public async Task<EscrowTransaction> CreateEscrowAsync(
Guid auctionId, Guid buyerId, Guid sellerId,
decimal amount)
{
var escrow = new EscrowTransaction
{
Id = Guid.NewGuid(),
AuctionId = auctionId,
BuyerId = buyerId,
SellerId = sellerId,
Amount = amount,
Status = EscrowStatus.FundsHeld,
CreatedAt = DateTime.UtcNow,
ConfirmationDeadline = DateTime.UtcNow
.AddDays(7),
ReleaseDeadline = DateTime.UtcNow.AddDays(14)
};
await _escrowRepo.InsertAsync(escrow);
return escrow;
}
public async Task ConfirmReceiptAsync(Guid escrowId)
{
var escrow = await _escrowRepo.GetByIdAsync(escrowId);
if (escrow.Status != EscrowStatus.FundsHeld)
throw new InvalidOperationException(
"Cannot confirm receipt in current state");
escrow.Status = EscrowStatus.FundsReleased;
escrow.ConfirmedAt = DateTime.UtcNow;
await _escrowRepo.UpdateAsync(escrow);
await _paymentGateway.TransferAsync(
escrow.SellerId, escrow.Amount);
}
public async Task DisputeAsync(
Guid escrowId, string reason)
{
var escrow = await _escrowRepo.GetByIdAsync(escrowId);
escrow.Status = EscrowStatus.UnderDispute;
escrow.DisputeReason = reason;
escrow.DisputedAt = DateTime.UtcNow;
await _escrowRepo.UpdateAsync(escrow);
await _notificationService.NotifyDisputeAsync(escrow);
await _moderationQueue.EnqueueAsync(
new DisputeReviewTask(escrow));
}
}
18.2 Seller Verification
Seller verification ensures that only legitimate sellers can list items. The verification process includes:
- Identity verification (KYC): Government-issued ID, proof of address, and in some cases, video verification.
- Tax verification: Tax ID or SSN for sellers above certain transaction thresholds (IRS 1099-K reporting).
- Item authentication: For high-value categories (art, watches, wine), third-party authentication is required before listing.
- Deposit requirement: New sellers may be required to place a refundable deposit to cover potential dispute costs.
19. Notifications System
A robust notification system is critical for auction engagement. Bidders must be immediately aware when they are outbid, when an auction they are watching is ending soon, and when they have won an item.
19.1 Notification Types
| Event | Channels | Timing |
|---|---|---|
| Outbid notification | WebSocket, Push, Email | Immediate |
| Auction ending in 5 minutes | WebSocket, Push | 5 min before end |
| Auction extended | WebSocket, Push | Immediate |
| Auction won | WebSocket, Push, Email, SMS | Immediate |
| Payment confirmation | Immediate | |
| Item shipped | Email, Push | When shipped |
| Escrow released | Email, Push | Immediate |
19.2 Notification Service Implementation
C#
public class NotificationService
{
private readonly IWebSocketBroadcaster _wsBroadcaster;
private readonly IPushNotificationService _pushService;
private readonly IEmailService _emailService;
private readonly ISmsService _smsService;
public async Task NotifyOutbidAsync(
Guid auctionId, Guid previousBidderId,
decimal newAmount)
{
var tasks = new List<Task>();
tasks.Add(_wsBroadcaster.SendToUserAsync(
previousBidderId, new OutbidNotification
{
AuctionId = auctionId,
NewAmount = newAmount,
Message = $"You have been outbid! " +
$"Current price: ${newAmount:N2}"
}));
tasks.Add(Task.Delay(TimeSpan.FromSeconds(2))
.ContinueWith(async _ =>
{
await _pushService.SendAsync(
previousBidderId,
"Outbid!",
$"A new bid of ${newAmount:N2} " +
$"has been placed. Bid now to stay ahead!");
}));
tasks.Add(_emailService.SendOutbidEmailAsync(
previousBidderId, auctionId, newAmount));
await Task.WhenAll(tasks);
}
public async Task NotifyAuctionWonAsync(
Guid auctionId, Guid winnerId,
PaymentBreakdown breakdown)
{
await Task.WhenAll(
_wsBroadcaster.SendToUserAsync(winnerId,
new WonNotification
{
AuctionId = auctionId,
Amount = breakdown.TotalDue
}),
_pushService.SendAsync(winnerId,
"Congratulations! You Won!",
$"You won the auction for ${breakdown.HammerPrice:N2}. " +
$"Total due: ${breakdown.TotalDue:N2}"),
_emailService.SendWonEmailAsync(
winnerId, auctionId, breakdown),
_smsService.SendAsync(winnerId,
$"You won! Pay ${breakdown.TotalDue:N2} " +
$"within 48 hours.")
);
}
}
20. Mobile Optimization and CDN
Over 60% of auction traffic now comes from mobile devices. The platform must deliver a fast, responsive experience on mobile while efficiently serving item images globally.
20.1 Mobile-First Design Considerations
- Touch-friendly bid controls: Large bid buttons with haptic feedback, swipe gestures for bid increments, and a persistent bid bar at the bottom of the screen.
- Offline resilience: Service workers cache auction data and bid history for offline viewing. Pending bids are queued and submitted when connectivity resumes.
- Push notifications: Native push notifications for outbid alerts and auction endings, even when the app is in the background.
- Adaptive layouts: The bid interface adapts to screen size, with a compact mode for small screens that shows only essential information (current price, time remaining, bid button).
- WebSocket reconnection: Automatic reconnection with exponential backoff when WebSocket connections drop on mobile networks.
20.2 CDN Architecture for Item Images
20.3 Image Optimization Pipeline
C#
public class ImageProcessingService
{
private readonly IAmazonS3 _s3Client;
private readonly ICloudFrontClient _cfClient;
public async Task<ProcessedImageResult> ProcessUploadAsync(
Stream imageStream, string auctionId)
{
var sizes = new Dictionary<string, Size>
{
["thumbnail"] = new(150, 150),
["small"] = new(400, 400),
["medium"] = new(800, 800),
["large"] = new(1600, 1600),
["full"] = new(3200, 3200)
};
var result = new ProcessedImageResult();
foreach (var (sizeName, dimensions) in sizes)
{
var processed = await ResizeImageAsync(
imageStream, dimensions);
var webpBytes = await ConvertToWebPAsync(processed);
var key = $"auctions/{auctionId}/images/" +
$"{sizeName}_{Guid.NewGuid()}.webp";
await _s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = "auction-images",
Key = key,
InputStream = new MemoryStream(webpBytes),
ContentType = "image/webp",
CacheControl = "public, max-age=31536000"
});
result.ImageUrls[sizeName] =
$"https://cdn.auctionplatform.com/{key}";
}
return result;
}
}
21. Moderation and Shill Bidding Detection
Moderation ensures the integrity of the marketplace by detecting and addressing fraudulent or policy-violating behavior. This section covers the comprehensive moderation framework including automated detection, human review workflows, and policy enforcement.
21.1 Moderation Queue Architecture
C#
public class ModerationEngine
{
private readonly IFraudDetectionEngine _fraudEngine;
private readonly IAuctionRepository _auctionRepo;
private readonly IModerationQueue _queue;
private readonly INotificationService _notifications;
public async Task<ModerationResult> EvaluateBidAsync(
BidPlacedEvent bidEvent)
{
var flags = new List<ModerationFlag>();
var shillCheck = await _fraudEngine
.DetectShillBiddingAsync(
bidEvent.AuctionId, bidEvent.BidderId);
if (shillCheck.IsDetected)
{
flags.Add(new ModerationFlag
{
Type = FlagType.ShillBidding,
Severity = shillCheck.Confidence > 0.8m
? Severity.High
: Severity.Medium,
Details = shillCheck.Details
});
}
var velocityCheck = await _fraudEngine
.CheckBidVelocityAsync(
bidEvent.AuctionId, bidEvent.BidderId);
if (velocityCheck.IsAnomalous)
{
flags.Add(new ModerationFlag
{
Type = FlagType.UnusualVelocity,
Severity = Severity.Low,
Details = velocityCheck.Details
});
}
if (flags.Any())
{
var highSeverityFlags = flags
.Where(f => f.Severity == Severity.High)
.ToList();
if (highSeverityFlags.Any())
{
await _queue.EnqueueAsync(
new ModerationTask
{
AuctionId = bidEvent.AuctionId,
BidderId = bidEvent.BidderId,
Flags = highSeverityFlags,
RequiresImmediateAction = true
});
return ModerationResult
.FlaggedForReview(flags);
}
await _queue.EnqueueAsync(
new ModerationTask
{
AuctionId = bidEvent.AuctionId,
BidderId = bidEvent.BidderId,
Flags = flags,
RequiresImmediateAction = false
});
}
return ModerationResult.Clean;
}
}
21.2 Shill Bidding Detection Algorithms
Detecting shill bidding requires analyzing patterns across multiple dimensions:
- Bid frequency analysis: A bidder who places an unusually high number of bids on items from a single seller, especially if they never win, is a strong signal.
- Price impact analysis: If bids from a particular user consistently drive up prices but the user never wins, the bids may be artificial.
- Network graph analysis: Shared IP addresses, device fingerprints, browser fingerprints, or payment methods between bidders and sellers indicate collusion.
- Temporal pattern analysis: Shill bids often follow a predictable pattern: they appear at regular intervals and typically increase the price by exactly the minimum increment.
- New account analysis: Newly created accounts that immediately start bidding on specific sellers' items and never bid elsewhere.
| Detection Signal | Weight | False Positive Risk |
|---|---|---|
| Seller bids on own item | 1.0 (definitive) | None |
| Shared IP or payment method | 0.9 | Low |
| Never wins, high frequency on same seller | 0.7 | Medium |
| New account with single seller focus | 0.6 | Medium |
| Minimum increment bidding pattern | 0.5 | Medium-High |
22. Monitoring and Security
22.1 Key Metrics and Dashboards
Comprehensive monitoring is essential for maintaining system health and quickly identifying issues during live auctions. The monitoring stack includes Prometheus for metrics, Grafana for dashboards, and PagerDuty for alerting.
| Metric | Target | Alert Threshold |
|---|---|---|
| Bid processing latency (p99) | Less than 100ms | Greater than 200ms |
| WebSocket broadcast latency (p99) | Less than 50ms | Greater than 100ms |
| Bid success rate | Greater than 99.9% | Less than 99.5% |
| Active WebSocket connections | Monitoring | Sudden drop over 20% |
| Auction timer drift | Less than 1 second | Greater than 5 seconds |
| Fraud detection false positive rate | Less than 5% | Greater than 10% |
| Payment processing success rate | Greater than 99% | Less than 97% |
| Database connection pool utilization | Less than 70% | Greater than 85% |
22.2 Security Measures
- Rate limiting: Per-user, per-IP, and per-auction rate limits to prevent abuse. Bidding endpoints are rate-limited more aggressively than read endpoints.
- Input validation: All bid amounts are validated on both client and server. Server-side validation is authoritative; client-side validation is for UX only.
- Anti-automation: CAPTCHA challenges for users who exhibit bot-like behavior (rapid bidding, consistent timing intervals).
- API key management: Third-party integrations use scoped API keys with rate limits and IP whitelisting.
- Audit logging: Every bid, state change, and administrative action is logged with user identity, timestamp, and IP address.
- Encryption: All data in transit is encrypted with TLS 1.3. Sensitive data at rest (payment info, personal details) is encrypted with AES-256.
- DDoS protection: Cloudflare or AWS Shield for DDoS mitigation, with automatic scaling to absorb traffic spikes during high-profile auctions.
22.3 Security Audit Logging
C#
public class SecurityAuditLogger
{
private readonly IAuditLogRepository _auditRepo;
public async Task LogBidAsync(
BidPlacedEvent bidEvent, string ipAddress,
string userAgent, bool wasSuccessful)
{
var auditEntry = new AuditLogEntry
{
Id = Guid.NewGuid(),
EventType = "BID_PLACED",
UserId = bidEvent.BidderId,
AuctionId = bidEvent.AuctionId,
Amount = bidEvent.Amount,
IpAddress = ipAddress,
UserAgent = userAgent,
WasSuccessful = wasSuccessful,
Timestamp = DateTime.UtcNow,
Metadata = JsonSerializer.Serialize(new
{
BidId = bidEvent.BidId,
ServerTimestamp = DateTime.UtcNow
.ToString("O")
})
};
await _auditRepo.InsertAsync(auditEntry);
await _kafkaProducer.PublishAsync(
"security.audit", auditEntry);
}
public async Task LogSuspiciousActivityAsync(
string userId, string activityType,
string details, string ipAddress)
{
var entry = new AuditLogEntry
{
Id = Guid.NewGuid(),
EventType = $"SUSPICIOUS_{activityType}",
UserId = userId,
IpAddress = ipAddress,
Timestamp = DateTime.UtcNow,
Metadata = details
};
await _auditRepo.InsertAsync(entry);
if (activityType.Contains("FRAUD") ||
activityType.Contains("BREACH"))
{
await _alertService.SendCriticalAlertAsync(
$"Suspicious activity for user {userId}: {details}");
}
}
}
23. Compliance and Legal Considerations
Operating an auction platform involves navigating a complex web of legal and regulatory requirements. Non-compliance can result in fines, lawsuits, and loss of operating licenses.
23.1 Key Regulatory Requirements
- Consumer protection laws: The FTC (in the US) and equivalent bodies in other jurisdictions regulate deceptive practices, including fake bids and misleading reserve prices. Auction platforms must disclose buyer premiums, taxes, and shipping costs before the bid is placed.
- Money transmission: Holding funds in escrow may require money transmitter licenses in certain states or countries. Partner with a licensed escrow provider if direct licensing is not feasible.
- KYC/AML compliance: Know Your Customer and Anti-Money Laundering regulations require identity verification for sellers above certain transaction thresholds. Suspicious transactions must be reported to FinCEN.
- Data privacy: GDPR (EU), CCPA (California), and other privacy regulations govern how user data is collected, stored, and processed. Bidding history, payment data, and personal information must be handled in compliance.
- Art and cultural property: Auctions of art, antiquities, and cultural artifacts may be subject to export/import restrictions, provenance requirements, and sanctions compliance (OFAC).
- Warranty and returns: Auction sales are generally "as-is" in many jurisdictions, but platform policies may offer additional protections. Clear disclosure of item condition and return policies is essential.
23.2 Compliance Implementation Checklist
| Requirement | Implementation | Owner |
|---|---|---|
| Fee disclosure | Buyer premium and taxes shown on bid confirmation | Product and Legal |
| KYC verification | Seller identity verification before listing | User Service |
| AML reporting | Suspicious transaction reports to FinCEN | Compliance Team |
| Data retention | Bid history retained for 7 years per regulation | Data Team |
| Right to deletion | GDPR data export and deletion endpoints | User Service |
| Anti-fraud disclosure | Shill bidding prohibited, clearly stated in ToS | Legal and Product |
24. Cost Estimation
Understanding the infrastructure costs is critical for business viability. The following estimation is for a mid-scale platform handling 100,000 active auctions and 5 million bids per month.
| Component | Configuration | Monthly Cost (USD) |
|---|---|---|
| Application Servers (Bid Processing) | 8x c6i.xlarge (4 vCPU, 8GB) | $1,120 |
| WebSocket Gateway | 6x c6i.large (2 vCPU, 4GB) | $504 |
| PostgreSQL (Primary + Replica) | 2x db.r6g.xlarge (4 vCPU, 32GB) | $1,200 |
| Redis Cluster | 3x cache.r6g.large (2 vCPU, 13GB) | $600 |
| Kafka (3 brokers) | 3x kafka.m5.large | $750 |
| Elasticsearch | 3x m6i.large.search | $540 |
| S3 + CloudFront CDN | 5TB storage, 50TB transfer | $500 |
| Load Balancers | 2x ALB | $50 |
| Monitoring (Prometheus/Grafana) | Managed service | $200 |
| Payment Processing (Stripe fees) | 2.9% + $0.30 per transaction | Variable |
| Email/SMS Notifications | SendGrid + Twilio | $300 |
| Total Infrastructure | ~$5,764/mo |
24.1 Cost Scaling Projections
| Scale | Active Auctions | Bids/Day | Est. Monthly Cost |
|---|---|---|---|
| Startup | 1,000 | 10,000 | $2,000 |
| Growth | 10,000 | 100,000 | $5,800 |
| Scale | 100,000 | 1,000,000 | $18,000 |
| Enterprise | 500,000 | 5,000,000 | $55,000 |
25. Testing Strategy
Testing a real-time auction system requires a multi-layered approach that covers unit logic, integration with databases and message brokers, end-to-end WebSocket testing, and load testing under concurrent bid scenarios.
25.1 Unit Testing Bid Logic
C#
public class BidProcessingTests
{
[Fact]
public async Task ProcessBid_ValidBid_Accepted()
{
var auction = CreateAuction(
startingPrice: 100m,
bidIncrement: 5m,
status: AuctionStatus.Live,
version: 1);
var command = new PlaceBidCommand
{
AuctionId = auction.Id,
BidderId = Guid.NewGuid(),
Amount = 105m
};
var mockRepo = new Mock<IAuctionRepository>();
mockRepo.Setup(r => r.GetByIdAsync(auction.Id))
.ReturnsAsync(auction);
var service = new BidProcessingService(
mockRepo.Object,
Mock.Of<IBidRepository>(),
Mock.Of<IEventPublisher>(),
Mock.Of<IProxyBidEngine>(),
Mock.Of<IFraudDetector>(),
Mock.Of<ILogger<BidProcessingService>>());
var result = await service.ProcessBidAsync(command);
Assert.True(result.IsSuccess);
Assert.Equal(105m, result.AcceptedAmount);
}
[Fact]
public async Task ProcessBid_BelowMinimum_Rejected()
{
var auction = CreateAuction(
startingPrice: 100m,
bidIncrement: 5m,
status: AuctionStatus.Live,
currentPrice: 100m,
bidCount: 0);
var command = new PlaceBidCommand
{
AuctionId = auction.Id,
BidderId = Guid.NewGuid(),
Amount = 102m
};
var service = CreateService(auction);
var result = await service.ProcessBidAsync(command);
Assert.False(result.IsSuccess);
Assert.Contains("Minimum bid", result.ErrorMessage);
}
[Fact]
public async Task ProcessBid_OptimisticConflict_Retried()
{
var auction = CreateAuction(
currentPrice: 100m,
bidIncrement: 5m,
status: AuctionStatus.Live,
version: 1);
var service = CreateServiceWithConflict(auction);
var command = new PlaceBidCommand
{
AuctionId = auction.Id,
BidderId = Guid.NewGuid(),
Amount = 105m
};
var result = await service.ProcessBidAsync(command);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task ProcessBid_AuctionEnded_Rejected()
{
var auction = CreateAuction(
status: AuctionStatus.Ended);
var service = CreateService(auction);
var result = await service.ProcessBidAsync(
new PlaceBidCommand
{
AuctionId = auction.Id,
BidderId = Guid.NewGuid(),
Amount = 200m
});
Assert.False(result.IsSuccess);
Assert.Contains("not active", result.ErrorMessage);
}
}
25.2 Integration Testing WebSocket Broadcasting
C#
public class WebSocketIntegrationTests : IAsyncLifetime
{
private TestServer _server;
private HttpClient _httpClient;
public async Task InitializeAsync()
{
_server = new TestServer(
WebApplication.CreateBuilder()
.ConfigureServices(services =>
{
services.AddAuctionServices();
})
.Configure(app =>
{
app.UseWebSockets();
app.UseMiddleware<
AuctionWebSocketMiddleware>();
}).Build());
_httpClient = _server.CreateClient();
}
[Fact]
public async Task BidBroadcast_AllSubscribersReceive()
{
var client1 = await ConnectWebSocketAsync(
"/ws/auction/test-auction-1");
var client2 = await ConnectWebSocketAsync(
"/ws/auction/test-auction-1");
var client3 = await ConnectWebSocketAsync(
"/ws/auction/test-auction-1");
await _httpClient.PostAsJsonAsync(
"/api/v1/auctions/test-auction-1/bids",
new { Amount = 150m });
var msg1 = await ReceiveMessageAsync(client1);
var msg2 = await ReceiveMessageAsync(client2);
var msg3 = await ReceiveMessageAsync(client3);
Assert.Equal(WsMessageType.BidAccepted, msg1.Type);
Assert.Equal(WsMessageType.BidAccepted, msg2.Type);
Assert.Equal(WsMessageType.BidAccepted, msg3.Type);
Assert.Equal(150m,
((dynamic)msg1.Payload).Amount);
}
public async Task DisposeAsync()
{
_httpClient?.Dispose();
_server?.Dispose();
}
}
25.3 Load Testing with Concurrent Bids
C#
public class AuctionLoadTests
{
[Test]
public async Task ConcurrentBids_PeakLoad()
{
var httpPool = HttpPool.Create("auction-api",
config: HttpPoolConfig.WithUrl(
"http://localhost:5000"));
var placeBidStep = Step.Create("place_bid")
.WithFeeders(Feeder.FromRandom(
() => new
{
AuctionId = "load-test-auction",
Amount = Random.Shared.Next(100, 10000)
}))
.WithCall(async context =>
{
var response = await httpPool.Client
.PostAsJsonAsync(
$"/api/v1/auctions/" +
$"{context.FeedItem.AuctionId}/bids",
new
{
Amount = context.FeedItem.Amount
});
return response.IsSuccessStatusCode
? Response.Ok(statusCode:
(int)response.StatusCode)
: Response.Fail(statusCode:
(int)response.StatusCode);
});
var scenario = ScenarioBuilder
.CreateScenario("concurrent_bids", placeBidStep)
.WithLoadSimulations(
Simulation.Inject(rate: 500,
interval: TimeSpan.FromSeconds(1),
during: TimeSpan.FromMinutes(5))
);
var report = await NBomberRunner
.RegisterScenarios(scenario)
.RunAsync();
var okCount = report.OkCount;
var total = report.AllCount;
var successRate = (double)okCount / total;
Assert.GreaterOrEqual(successRate, 0.99,
$"Success rate {successRate:P2} is below 99%");
}
}
25.4 Test Coverage Summary
| Test Layer | Coverage Target | Focus Areas |
|---|---|---|
| Unit Tests | 90%+ | Bid validation, price calculation, state transitions |
| Integration Tests | 80%+ | Database operations, Redis caching, Kafka events |
| E2E Tests | 60%+ | Full bid flow, WebSocket broadcasting, payment |
| Load Tests | Every release | Concurrent bids, WebSocket scale, database contention |
| Chaos Tests | Monthly | Node failure, network partition, database failover |
26. Interview Q&A Deep Dive
The following questions test deep understanding of the system design concepts covered in this guide. Each answer demonstrates the level of depth expected in a senior+ system design interview.
Q1: How would you handle two bidders placing bids at the exact same millisecond?
Answer: We use optimistic locking with version checking on the auction record. Both bids arrive and attempt to update the auction's current price. The first bid to acquire the version stamp wins. The second bid encounters a version conflict (the UPDATE WHERE version = N returns 0 affected rows), and the system retries with the updated state. Since the bid increment ensures the second bid amount exceeds the first, the retry succeeds deterministically. The key insight is that "same millisecond" is not truly simultaneous at the server level: there is always an ordering within the CPU's instruction pipeline. Optimistic locking guarantees consistency without the throughput penalty of pessimistic locks.
Q2: What happens if the WebSocket gateway crashes while a bid is being processed?
Answer: The bid is persisted to the database before any WebSocket broadcast occurs. This is the durable message pattern. When the WebSocket gateway comes back up, it reconnects to Redis Pub/Sub and receives any messages published during the downtime. Additionally, clients maintain a local sequence number of the last received bid update. On reconnection, the client requests any missed updates via a REST endpoint. The auction state is always reconstructible from the event store, so no data is ever lost.
Q3: How do you prevent a seller from shill bidding on their own auction?
Answer: Multiple layers of protection. First, the bid validation layer checks if the bidder ID matches the seller ID, which is a hard block. Second, the fraud detection engine analyzes network patterns: shared IP addresses, device fingerprints, and payment methods between the bidder and seller. Third, behavioral analysis flags accounts that consistently bid on the same seller's items without winning. Fourth, we monitor for bidding patterns characteristic of shill bidding: minimum increment bids at regular intervals, especially in the final minutes. High-confidence flags auto-suspend the auction and queue for human review.
Q4: How would you design the anti-sniping mechanism to handle 10,000+ concurrent auctions?
Answer: Each auction has its own timer managed by the Auction Timer Service, which polls for auctions due for state transitions every second. When a bid arrives in the anti-sniping window, the bid processing service atomically updates the end time and reschedules the timer. For scale, active auction timers are partitioned across multiple timer service instances using consistent hashing on auction ID. The Redis sorted set (ZSET) is the efficient data structure: auctions are scored by their end time, and a polling loop finds all auctions with end time less than or equal to the current time. Timer precision of 1 second is sufficient because anti-sniping extensions are typically 120 seconds.
Q5: How do you handle the payment flow for auction wins, especially with escrow?
Answer: When an auction ends, the system initiates a payment flow with a 48-hour deadline. The winning bidder is directed to a payment page that calculates the total due (hammer price + buyer premium + taxes + shipping). Payment is processed via Stripe or PayPal, and the funds are held in escrow rather than sent directly to the seller. The buyer has 7 days to inspect the item and confirm receipt. If confirmed, escrow releases funds to the seller. If a dispute is filed, the funds are held pending resolution by the moderation team. For high-value items over $10,000, we require wire transfer with a longer processing window.
Q6: How would you scale the WebSocket connections to handle 100,000 concurrent bidders on a single high-profile auction?
Answer: A single WebSocket server process can handle approximately 10,000 to 50,000 concurrent connections depending on message frequency. For 100,000+ connections, we shard the auction room across multiple gateway instances. The pub/sub channel for that auction is partitioned by connection hash. When a bid arrives, it is published to the global auction channel, and each gateway instance broadcasts to its local connections. Additionally, we implement a tiered update model: active bidders (those who placed a bid in the last 5 minutes) receive every update, while passive viewers receive updates every 5 seconds or on significant price changes only. This reduces message volume by 80%.
Q7: How do you ensure auction fairness when bidders have different network latencies?
Answer: Complete fairness across different network conditions is impossible, but we minimize the advantage. The server timestamp is authoritative for bid ordering, not the client's local clock. All timer synchronization uses the server's timestamp, not the client's. We also implement a bid acceptance window: bids received within 100ms of the auction end time are considered as part of the same time window and ordered by server receipt order. This prevents sub-millisecond advantages from network timing. For penny auctions where timing is critical, we display a ping indicator showing each user's connection latency to the server.
Q8: Design the data model for handling 500,000 concurrent auctions efficiently.
Answer: Hot data (current bid state, timer, participant count) lives in Redis for sub-millisecond access. Warm data (recent bid history, auction details) lives in PostgreSQL with read replicas for query distribution. Cold data (completed auction history, old bid logs) is archived to S3/Parquet for analytics. The PostgreSQL database uses table partitioning on the bids table by auction ID hash, distributing data across multiple physical partitions. Active auctions have their state cached in Redis with a write-through strategy: every bid update writes to both Redis and PostgreSQL, with Redis as the primary read path.
Q9: How would you handle a scenario where the payment gateway is temporarily unavailable?
Answer: The payment service implements a retry queue backed by a dead letter queue. If the primary gateway (Stripe) is down, we fall back to a secondary gateway (PayPal). If both are down, we queue the payment attempt with exponential backoff and notify the buyer that payment processing is temporarily delayed. The 48-hour payment deadline is extended by the duration of the outage. The system logs the outage for SLA reporting and automatically credits the affected users if the outage exceeds a threshold. Circuit breaker patterns prevent cascading failures from payment gateway downtime to other services.
Q10: What is the trade-off between optimistic and pessimistic locking for bid processing?
Answer: Optimistic locking is preferred for bid processing because the vast majority of bids succeed on the first attempt (contention is rare even under high load). It allows higher throughput because no locks are held during the validation and business logic phases. The retry mechanism handles the rare conflicts gracefully. Pessimistic locking would serialize all bids for an auction, creating a bottleneck and reducing throughput to one bid per transaction duration. However, for proxy bidding, where a single bidder's proxy bid may trigger multiple sequential bids, pessimistic locking (or at minimum, a distributed lock) ensures that the proxy bid engine processes one auction at a time, preventing duplicate bids from racing conditions within the proxy logic itself.