system-design51 min read

How to Design a Live Auction & Bidding System — A Senior+ Guide | Ayodhyya

How to Design a Live Auction & Bidding System

Building a Production-Grade Real-Time Auction Platform — Bid Processing, Anti-Sniping, WebSocket Streaming & Fraud Prevention

Senior+ System Design Guide 10,000+ Words 26 Deep-Dive Sections C# · Mermaid · Real-World Case Studies

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.
Key Insight: An auction system is fundamentally different from a shopping cart system. In shopping, the price is fixed and the action is additive. In an auction, the price is dynamic and every bid potentially invalidates every other bid. This creates a unique set of distributed systems challenges that require specialized architectural patterns.

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
Design Consideration: Supporting multiple auction types requires a flexible architecture. We will use the Strategy pattern to encapsulate auction-specific rules, allowing the core bid processing pipeline to remain type-agnostic while delegating type-specific logic to specialized handlers.

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.

graph TB Client[Client - Browser/Mobile] -->|HTTPS| LB[Load Balancer] Client -->|WSS| WSG[WebSocket Gateway] LB --> APIGW[API Gateway] APIGW --> AUCTION[Auction Service] APIGW --> BID[Bid Processing Service] APIGW --> USER[User Service] APIGW --> SEARCH[Search Service] BID -->|Publish BidPlaced| KAFKA[Event Bus - Kafka] KAFKA -->|Consume BidPlaced| WSG KAFKA -->|Consume BidPlaced| NOTIFY[Notification Service] KAFKA -->|Consume AuctionEnded| PAY[Payment Service] KAFKA -->|Consume BidPlaced| FRAUD[Fraud Detection Service] BID --> DB[(Bid DB - PostgreSQL)] AUCTION --> ADB[(Auction DB - PostgreSQL)] AUCTION --> CACHE[(Redis Cache)] USER --> UDB[(User DB - PostgreSQL)] PAY --> ESCROW[Escrow Service] FRAUD -->|Flag Suspicious| MOD[Moderation Queue] WSG -->|Broadcast| Client

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.
Architecture Principle: The Bid Processing Service is the single point of truth for all bid state. It persists bids durably before publishing events, ensuring that no bid is ever lost even if downstream consumers fail. This is the durable message pattern.

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
Event Sourcing: Every bid is stored as an immutable event in the event store. The current state of an auction (current price, winning bidder) is derived by replaying events. This provides a complete audit trail and enables time-travel debugging, which is essential for resolving disputes.

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/auctionsCreate a new auction (seller only)
GET/api/v1/auctionsList 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}/bidsGet bid history for an auction

6.2 Bid Endpoints

Method Endpoint Description
POST/api/v1/auctions/{id}/bidsPlace a bid on an auction
POST/api/v1/auctions/{id}/proxy-bidsSet up proxy (auto) bidding
DELETE/api/v1/auctions/{id}/proxy-bidsCancel proxy bidding
GET/api/v1/users/me/bidsGet current user's bid history

6.3 Payment Endpoints

Method Endpoint Description
POST/api/v1/auctions/{id}/paymentInitiate payment for won auction
GET/api/v1/users/me/purchasesList user's won auctions and payment status
POST/api/v1/auctions/{id}/confirm-receiptBuyer 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.

stateDiagram-v2 [*] --> Draft Draft --> Preview : Schedule Preview --> Live : Start Time Reached Live --> Extended : Bid in Final 2min Extended --> Extended : Bid in Final 2min Extended --> Ended : Timer Expires Live --> Ended : Timer Expires Draft --> Cancelled : Seller Cancels Preview --> Cancelled : Seller Cancels Cancelled --> [*] Ended --> Paid : Payment Received Paid --> Completed : Receipt Confirmed Ended --> Disputed : Dispute Filed Disputed --> Completed : Resolved

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));
        }
    }
}
Critical Edge Case: The transition from Live to Ended must be idempotent. Due to network delays or service restarts, the ended handler might be invoked multiple times. The implementation must handle this gracefully without creating duplicate payment requests or sending duplicate notifications.

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

flowchart LR A[Receive Bid] --> B[Rate Limit Check] B --> C[Authentication] C --> D[Load Auction State] D --> E{Validate Bid} E -->|Invalid| F[Reject with Reason] E -->|Valid| G[Acquire Optimistic Lock] G --> H[Process Bid Atomically] H --> I[Persist Bid to DB] I --> J[Publish BidPlaced Event] J --> K[Broadcast via WebSocket] K --> L[Return Success]

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;
    }
}
Anti-Pattern Alert: Never place the bid processing logic inside a single database transaction that also handles WebSocket broadcasting. The transaction should only handle the persistent state changes. Events should be published after the transaction commits, and broadcasting should be a fire-and-forget operation. Blocking the transaction on I/O creates bottlenecks and increases the risk of long-running locks.

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.

Performance Tip: Batch broadcast messages when multiple bids arrive within a short window (e.g., 50ms). Instead of sending one WebSocket message per bid, aggregate them into a single batched message. This reduces the number of frames sent over the wire and improves throughput during high-traffic auctions.

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.
False Positive Management: Fraud detection must balance sensitivity with specificity. Flagging legitimate bidders damages trust and drives users away. Use a confidence scoring system: high-confidence flags auto-block, medium-confidence flags go to human review, and low-confidence flags are logged for future analysis.

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
User Experience: When an anti-snipe extension occurs, the UI should prominently display a message like "This auction has been extended by 2 minutes due to a last-minute bid." This builds trust by demonstrating that the platform actively protects all bidders from sniping.

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);
    }
}
Immutability Guarantee: Once an event is written to the event store, it is never modified or deleted. This is enforced at the database level with append-only table constraints and at the infrastructure level with WORM (Write Once Read Many) storage policies on the S3 archive.

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:

  1. Maximum amount: The bidder with the highest maximum bid gets priority.
  2. 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
Privacy: Proxy bid maximum amounts must never be revealed to other bidders, including the seller. The system should show only that "a proxy bid is active" without disclosing the maximum. This is critical for maintaining auction integrity.

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

flowchart TB subgraph Scheduler DB[(Active Auctions)] --> Poll[Poll Due Events] Poll --> StartAuctions[Start Live Auctions] Poll --> EndAuctions[End Expired Auctions] Poll --> CheckExtensions[Check Extensions] end StartAuctions -->|Publish| KAFKA[Kafka - auction.lifecycle] EndAuctions -->|Publish| KAFKA CheckExtensions -->|Publish| KAFKA KAFKA --> Handler1[Lifecycle Handler] KAFKA --> Handler2[Notification Handler] KAFKA --> Handler3[Search Index Handler]

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
}
UI Disclosure: Most auction platforms display a status indicator showing "Reserve price met" or "Reserve not met" next to the current bid. Some platforms show a progress bar indicating how close the current bid is to the reserve. This transparency encourages higher bidding.

17. Buyer Premium and Payment Processing

A buyer premium is an additional percentage charged on top of the winning bid price. It is the primary revenue mechanism for auction houses. For example, if the winning bid is $1,000 and the buyer premium is 15%, the buyer pays $1,150.

17.1 Premium Calculation

C#
public class PaymentCalculator
{
    public PaymentBreakdown Calculate(
        Auction auction, Bid winningBid)
    {
        var hammerPrice = winningBid.Amount;
        var premiumRate = auction.BuyerPremium / 100m;
        var premium = hammerPrice * premiumRate;

        var tax = CalculateTax(
            hammerPrice + premium, auction.TaxJurisdiction);

        var shipping = CalculateShipping(auction);

        return new PaymentBreakdown
        {
            HammerPrice = hammerPrice,
            BuyerPremium = premium,
            SalesTax = tax,
            ShippingCost = shipping,
            TotalDue = hammerPrice + premium + tax + shipping,
            Currency = auction.Currency
        };
    }
}

public record PaymentBreakdown
{
    public decimal HammerPrice { get; init; }
    public decimal BuyerPremium { get; init; }
    public decimal SalesTax { get; init; }
    public decimal ShippingCost { get; init; }
    public decimal TotalDue { get; init; }
    public string Currency { get; init; } = "";
}

17.2 Payment Processing Integration

sequenceDiagram participant Winner as Winning Bidder participant API as Payment API participant Stripe as Stripe/PayPal participant Escrow as Escrow Service participant Seller as Seller Winner->>API: POST /auctions/{id}/payment API->>API: Calculate total (hammer + premium + tax) API->>Stripe: Create PaymentIntent Stripe-->>API: Client secret API-->>Winner: Payment page with Stripe widget Winner->>Stripe: Enter payment details Stripe->>API: Webhook: payment_intent.succeeded API->>Escrow: Hold funds in escrow API->>Seller: Notification: payment received Note over Escrow: Buyer has 7 days to confirm receipt Winner->>API: POST /auctions/{id}/confirm-receipt API->>Escrow: Release funds to seller Escrow->>Seller: Transfer funds

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.
Trust Score: The platform maintains a seller trust score based on verification level, transaction history, dispute rate, and buyer reviews. Higher trust scores unlock premium features like higher listing limits and reduced commission rates.

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 Email 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

flowchart LR Upload[Seller Uploads Image] --> S3[AWS S3 Origin] S3 --> Process[Image Processing Lambda] Process --> Resize[Generate Thumbnails] Process --> Optimize[WebP/AVIF Conversion] Process --> Metadata[Extract EXIF Data] Resize --> CDN[CloudFront CDN] Optimize --> CDN CDN --> Client[Client - Browser/Mobile]

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;
    }
}
Performance Tip: Use progressive JPEG loading for large item images. This renders a low-quality version immediately and progressively refines it as data loads, providing instant visual feedback on slow connections. Combined with lazy loading below the fold, this dramatically improves perceived performance.

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.
Legal Risk: Operating a penny auction without proper disclosure of the pay-to-bid model has resulted in FTC enforcement actions against several platforms. Ensure that all auction mechanics, fees, and terms are clearly disclosed before any user places a bid.

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
Cost Optimization: For the first 10,000 active auctions, a single-region deployment with 2-3 application servers can handle the load at approximately $2,000/month. Scale horizontally as auction volume grows. Use reserved instances for 30-40% savings on predictable workloads.

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%");
    }
}
Testing Anti-Pattern: Never use real payment gateways in integration or load tests. Use mock payment processors that simulate success and failure scenarios. Real charges during testing are expensive and difficult to reverse.

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.

Live Auction and Bidding System Design — Senior+ Guide | Ayodhyya