Skip to main content
Version: MyBestDealNow

Live Auction Bid Feed System

Authors

  • Rishika Kumari

Last Updated Date

2025-12-13


SRS References


Version History

VersionDateChangesAuthor
1.22025-12-13Added validation rules, error examples, edge cases, security, live polling example & enhanced testingRishika Kumari
1.12025-12-13Updated InventoryId nullability & Polling SQL logicRishika Kumari
1.02025-12-13Initial draft - Cursor-based feed with Live LogicRishika Kumari

Feature Overview

Objective:
Implement a real-time, paginated feed of bid activities (Place, Update, Withdraw) for a specific auction. The system enables dealers and admins to monitor bid actions as they happen. It utilizes cursor-based pagination to support both infinite scrolling (fetching historical data) and live polling (fetching real-time updates) without missing or duplicating records.

Scope:

  • Phase 1 (Current): Polling-based feed using cursor pagination.
  • Real-time Diff Calculation: Backend calculates LastBidAmount so the frontend can display increments/decrements.
  • Dynamic Context: Supports bids placed on specific Inventory items OR generic Vehicle Specifications.
  • Auction-level Filtering: Provides a global activity feed for a specific auction context.

Dependencies:

  • AuctionEngineService - For core bid logic and persistence.
  • InventoryService - For vehicle specification and inventory details.
  • PostgreSQL Database - For data persistence with UUID support.

Requirements

Functional Requirements

  1. Feed Display Requirements

    • Users must see a paginated list of all bid activities for a specific auction.
    • Feed items must display:
      • Dealer Name (Company Name).
      • Action (Place/Update/Withdraw).
      • Bid Amount (Current).
      • Car Details (Make/Model from Inventory OR Vehicle Spec).
    • Feed must include LastBidAmount (the amount before the current action) for diff calculations.
    • Newest activities must appear at the top.
  2. Pagination & Real-Time Requirements

    • Scrolling Down (NextCursor): Users must be able to load older feed seamlessly.
    • Live Updates (PreviousCursor): Frontend must be able to poll for only new records that occurred after the latest displayed item.
    • Tie-Breaking: Pagination must be stable using (UpdatedTime, BidHistoryId) to handle identical timestamps.
  3. Data Integrity

    • If a bid is Placed: LastBidAmount must be 0.
    • If a bid is Updated: LastBidAmount must reflect the previous value.
    • If a bid is Withdrawn: The feed must capture the final state.
    • If InventoryId is missing (Slot Bid), Make/Model must fallback to VehicleSpecification.

Non-Functional Requirements

  1. Performance

    • API response time must be under 300ms.
    • Efficient Subquery logic for calculating LastBidAmount.
    • Database indexing on (auctionid, updatedat DESC) and (bidid, updatedat DESC).
  2. Usability

    • Polling Interval: 3-5 seconds for live updates.
    • Limit Parameter: Defaults to 20, adjustable for Mobile vs Desktop views.

Design Specifications

UI/UX Design

Feed Interface Logic:

  • Placement: "Best Motors placed a bid of $12,000 on Honda Civic"
  • Update: "Best Motors updated bid on Honda Civic (Increased by $500)"
    • Logic: BidAmount - LastBidAmount
  • Withdraw: "Best Motors withdrew bid on Honda Civic"

Data Models

Database Schema & Query Logic

SQL Strategy: The query handles the "Either/Or" scenario for Vehicle details (Inventory vs Spec) using LEFT JOIN and COALESCE. It supports dynamic direction switching: Strictly Less Than (<) for scrolling down (Older) and Strictly Greater Than (>) for polling (Newer).

SELECT
bh.historyid AS BidHistoryId,
bh.action AS Action,
bh.bidamount AS BidAmount,
bh.updatedat AS UpdatedTime,

-- 1. SUBQUERY FOR LAST BID AMOUNT
-- Fetches the single record immediately preceding the current row
COALESCE(
(SELECT sub.bidamount
FROM public.bidhistory sub
WHERE sub.bidid = bh.bidid
AND (sub.updatedat < bh.updatedat OR (sub.updatedat = bh.updatedat AND sub.historyid < bh.historyid))
ORDER BY sub.updatedat DESC, sub.historyid DESC
LIMIT 1),
0) AS LastBidAmount,

-- 2. DEALER DETAILS
dm.dealerid AS DealerId,
dm.dealercode AS DealerCode,
dm.companyname AS DealerName,

-- 3. SMART CAR DETAILS (Inventory Fallback Logic)
b.inventoryid AS InventoryId,
COALESCE(inv.make, vs.make) AS Make,
COALESCE(inv.model, vs.model) AS Model

FROM
public.bidhistory bh
JOIN
public.bid b ON bh.bidid = b.bidid
JOIN
public.dealermaster dm ON b.dealerid = dm.dealerid

-- 4. LEFT JOINS (To handle missing Inventory links)
LEFT JOIN
public.inventorymaster inv ON b.inventoryid = inv.inventoryid
LEFT JOIN
public.vehiclespecification vs ON b.vehiclespecificationid = vs.vehiclespecificationid

WHERE
b.auctionid = @AuctionId

-- 5. CURSOR PAGINATION LOGIC (Bi-Directional)
AND (
-- CASE A: Scrolling Down (Fetching Older History) - Default
(@IsPolling = false AND (
(bh.updatedat < @CursorTime) OR
(bh.updatedat = @CursorTime AND bh.historyid < @CursorId)
))
OR
-- CASE B: Live Polling (Fetching Newer History)
(@IsPolling = true AND (
(bh.updatedat > @CursorTime) OR
(bh.updatedat = @CursorTime AND bh.historyid > @CursorId)
))
OR
-- CASE C: Initial Load
(@CursorTime IS NULL)
)

ORDER BY
bh.updatedat DESC, bh.historyid DESC
LIMIT @Limit


// Request DTO
public class BidFeedFilter
{
/// <summary>
/// Mandatory. The unique identifier of the Auction context.
/// </summary>
public Guid AuctionId { get; set; }

/// <summary>
/// Optional. Default 20. Max records to return per request.
/// </summary>
public int Limit { get; set; } = 20;

/// <summary>
/// Optional. Used for Scrolling Down (Fetching OLDER feed).
/// Value is obtained from the 'nextCursor' of the previous response.
/// </summary>
public string? NextCursor { get; set; }

/// <summary>
/// Optional. Used for Polling/Live Updates (Fetching NEWER feed).
/// Value is obtained from the 'previousCursor' of the first response.
/// </summary>
public string? PreviousCursor { get; set; }
}

// Response DTO (Item)
public record BidFeedItem
{
public Guid BidHistoryId { get; init; } // Tie-breaker ID
public Guid BidId { get; init; }
public string Action { get; init; } // "PLACE", "UPDATE", "WITHDRAW"

// Amount Details
public decimal BidAmount { get; init; }
public decimal LastBidAmount { get; init; } // Derived via Subquery

// Timing
public DateTime UpdatedTime { get; init; }

// Dealer Info
public Guid DealerId { get; init; }
public string DealerName { get; init; } // Mapped from CompanyName
public string DealerCode { get; init; }

// Vehicle Info
public Guid? InventoryId { get; init; }
public string Make { get; init; } // Mapped via COALESCE
public string Model { get; init; } // Mapped via COALESCE
}

## **C# Contract Models**

// Response Wrapper
public class CursorPaginatedData<T>
{
public List<T> Data { get; set; } = [];
public string? NextCursor { get; set; }
public string? PreviousCursor { get; set; }
public bool HasNextPage { get; set; }
public bool HasPreviousPage { get; set; }
public int? TotalCount { get; set; }
public int Limit { get; set; }
}

public record CommonResponse
{
public int Status { get; init; }
public string? Message { get; init; }
}

API Interfaces

EndpointMethodParametersResponseResponse Status Codes
/api/auction/bid-feedGETBidFeedFilterCursorPaginatedData<BidFeedItem>200, 400, 401, 500

Request Validation Rules

  1. AuctionId: Must be a valid GUID and reference an existing auction
  2. Limit: Must be between 1 and 100 (default: 20)
  3. NextCursor & PreviousCursor: Mutually exclusive - only one can be provided
  4. Cursor Format: Must be valid Base64-encoded string in format {timestamp}|{guid}

Authorization

  • Dealers can view bid feed for auctions they are participating in
  • Admins can view bid feed for any auction
  • Unauthorized access returns 401
  • Access to non-participating auction returns 403

API Request & Response Examples

1. Get Live Auction Feed (Initial Load)

Endpoint: GET /api/auction/bid-feed

Request Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Query Parameters (BidFeedFilter): ?auctionId=8b2e9c7d-4f5a-4e3b-9c8d-7e6f5a4b3c2d&limit=20

Success Response (200 OK - CursorPaginatedData<BidFeedItem>):

{
"data": [
{
"bidHistoryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"bidId": "1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6",
"action": "UPDATE",
"bidAmount": 15500.0,
"lastBidAmount": 15000.0,
"updatedTime": "2025-12-12T10:30:05.123Z",
"dealerId": "9f8c65-...",
"dealerName": "Auto World Ltd",
"dealerCode": "D-105",
"inventoryId": "77a85f...",
"make": "Toyota",
"model": "Camry"
},
{
"bidHistoryId": "5af8f61b-7391-4984-bd21-b354efa78...",
"bidId": "b15c1993-90fe-48f9-a313-0c10ab652...",
"action": "PLACE",
"bidAmount": 12000.0,
"lastBidAmount": 0.0,
"updatedTime": "2025-12-12T10:29:55.000Z",
"dealerId": "2e4f6a...",
"dealerName": "Best Motors",
"dealerCode": "D-106",
"inventoryId": "88b96c...",
"make": "Honda",
"model": "Civic"
}
],
"nextCursor": "MjAyNS0xMi0xMlQxMDoyOTo1NS4wMDBafDVhZjhmNjFiLTczOTEtNDk4NC1iZDIxLWIzNTRlZmE3OA==",
"previousCursor": "MjAyNS0xMi0xMlQxMDozMDowNS4xMjNafDNmYTg1ZjY0LTU3MTctNDU2Mi1iM2ZjLTJjOTYzZjY2YWZhNg==",
"hasNextPage": true,
"hasPreviousPage": false,
"totalCount": 450,
"limit": 20
}

2. Get Older History (Scrolling Down)

Endpoint: GET /api/auction/bid-feed

Query Parameters: Uses nextCursor from the previous response.

?auctionId=8b2e9c7d-4f5a-4e3b-9c8d-7e6f5a4b3c2d&limit=20&nextCursor=MjAyNS0xMi0xMlQxMDoyOTo1NS4wMDBafDVhZjhmNjFiLTczOTEtNDk4NC1iZDIxLWIzNTRlZmE3OA==

Success Response (200 OK): Returns items OLDER than the cursor time.

{
"data": [
{
"bidHistoryId": "11223344-5566-7788-99aa-bbccddeeff00",
"bidId": "aabbccdd-1122-3344-5566-778899aabbcc",
"action": "UPDATE",
"bidAmount": 11500.0,
"lastBidAmount": 11000.0,
"updatedTime": "2025-12-12T10:28:00.000Z",
"dealerId": "2e4f6a8c-...",
"dealerName": "Best Motors",
"dealerCode": "D-106",
"inventoryId": null,
"make": "Honda",
"model": "Civic"
}
],
"nextCursor": "MjAyNS0xMi0xMlQxMDoyNzowMC4wMDBafDk5ODg3Nzc2LTY2NTUtNDQzMy0yMjExLTAwMDAwMDAwMDAwMA==",
"previousCursor": "MjAyNS0xMi0xMlQxMDoyODowMC4wMDBafDExMjIzMzQ0LTU1NjYtNzc4OC05OWFhLWJiY2NkZGVlZmYwMA==",
"hasNextPage": true,
"hasPreviousPage": true,
"totalCount": 450,
"limit": 20
}

3. Live Polling (Fetching New Updates)

Endpoint: GET /api/auction/bid-feed

Query Parameters: Uses previousCursor from the initial/latest response.

?auctionId=8b2e9c7d-4f5a-4e3b-9c8d-7e6f5a4b3c2d&limit=20&previousCursor=MjAyNS0xMi0xMlQxMDozMDowNS4xMjNafDNmYTg1ZjY0LTU3MTctNDU2Mi1iM2ZjLTJjOTYzZjY2YWZhNg==

Success Response (200 OK): Returns NEW items that occurred AFTER the cursor time.

{
"data": [
{
"bidHistoryId": "7f8e9d0c-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"bidId": "6a5b4c3d-2e1f-0a9b-8c7d-6e5f4a3b2c1d",
"action": "PLACE",
"bidAmount": 16000.0,
"lastBidAmount": 0.0,
"updatedTime": "2025-12-12T10:30:45.678Z",
"dealerId": "3d2e1f0a-...",
"dealerName": "Premium Auto",
"dealerCode": "D-107",
"inventoryId": "99a88b77-...",
"make": "BMW",
"model": "X5"
},
{
"bidHistoryId": "8a9b0c1d-2e3f-4a5b-6c7d-8e9f0a1b2c3d",
"bidId": "1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6",
"action": "UPDATE",
"bidAmount": 16000.0,
"lastBidAmount": 15500.0,
"updatedTime": "2025-12-12T10:30:30.456Z",
"dealerId": "9f8c65-...",
"dealerName": "Auto World Ltd",
"dealerCode": "D-105",
"inventoryId": "77a85f...",
"make": "Toyota",
"model": "Camry"
}
],
"nextCursor": null,
"previousCursor": "MjAyNS0xMi0xMlQxMDozMDo0NS42NzhabDdmOGU5ZDBjLTFhMmItM2M0ZC01ZTZmLTdhOGI5YzBkMWUyZg==",
"hasNextPage": false,
"hasPreviousPage": true,
"totalCount": 452,
"limit": 20
}

Empty Polling Response (No New Data):

{
"data": [],
"nextCursor": null,
"previousCursor": "MjAyNS0xMi0xMlQxMDozMDowNS4xMjNafDNmYTg1ZjY0LTU3MTctNDU2Mi1iM2ZjLTJjOTYzZjY2YWZhNg==",
"hasNextPage": false,
"hasPreviousPage": false,
"totalCount": 450,
"limit": 20
}

4. Error Response Examples

400 Bad Request - Invalid Parameters:

{
"status": 400,
"message": "Both NextCursor and PreviousCursor cannot be provided simultaneously."
}

400 Bad Request - Invalid Cursor:

{
"status": 400,
"message": "Invalid cursor format. Cursor may be expired or malformed."
}

400 Bad Request - Invalid Limit:

{
"status": 400,
"message": "Limit must be between 1 and 100."
}

401 Unauthorized:

{
"status": 401,
"message": "Authentication required. Please provide a valid bearer token."
}

403 Forbidden:

{
"status": 403,
"message": "Access denied. You do not have permission to view this auction."
}

404 Not Found:

{
"status": 404,
"message": "Auction not found with ID: 8b2e9c7d-4f5a-4e3b-9c8d-7e6f5a4b3c2d"
}

500 Internal Server Error:

{
"status": 500,
"message": "An unexpected error occurred while processing your request."
}

Edge Cases & Error Handling

1. Empty Auction (No Bids)

Scenario: User requests feed for an auction with no bid feed.

Response:

{
"data": [],
"nextCursor": null,
"previousCursor": null,
"hasNextPage": false,
"hasPreviousPage": false,
"totalCount": 0,
"limit": 20
}

2. Invalid/Expired Cursor

Scenario: Cursor becomes invalid due to data purge or format change.

Handling:

  • Return 400 Bad Request with clear error message
  • Client should restart pagination from the beginning (without cursor)
  • Consider cursor versioning for format changes

3. Non-Existent Auction ID

Scenario: Request made with GUID that doesn't exist in database.

Handling:

  • Return 404 Not Found
  • Distinguish between "never existed" and "deleted" auctions

4. Concurrent Updates During Pagination

Scenario: New bids arrive while user is scrolling through feed.

Handling:

  • Cursor-based pagination ensures consistency
  • Historical pages remain stable
  • New items only appear when polling with previousCursor

5. Both Cursors Provided

Scenario: Client mistakenly sends both nextCursor and previousCursor.

Handling:

  • Return 400 Bad Request
  • Message: "Both NextCursor and PreviousCursor cannot be provided simultaneously."

6. Vehicle Data Missing (Deleted Inventory)

Scenario: Inventory or Vehicle Specification record deleted after bid placement.

Handling:

  • Make and Model return as empty strings or "Unknown"
  • Consider soft-delete strategy for reference data
  • Log warning for data integrity investigation

7. Dealer Deleted/Deactivated

Scenario: Dealer account removed but bid feed retained.

Handling:

  • Display generic name like "Deleted Dealer" or last known company name
  • Include dealerCode for traceability

Workflow & Technical Implementation Details

1. Cursor Generation Logic (Backend)

The nextCursor and previousCursor fields are opaque Base64 strings to the frontend. Internally, they store the Time and ID of the item to ensure precision.

Format:

Raw String: "{UpdatedTime}|{BidHistoryId}"

Example: 2025-12-13T14:30:00.1234567Z|a1b2c3d4-e5f6-7g8h-9i0j-k1l2m3n4o5p6

Encoding: Base64(UTF8_Bytes(RawString))

Usage:

NextCursor: Points to the Last Item (Oldest) in the current list. Used to fetch items older than this.

PreviousCursor: Points to the First Item (Newest) in the current list. Used to poll for items newer than this.

  1. Polling Strategy (Frontend)

  2. Initial Load: Call API with auctionId only.

    • Store the previousCursor from the response (Points to Top Item).
    • Render the initial list.
  3. Live Polling (Every 3-5 seconds):

    • Call API with auctionId AND previousCursor.
    • Backend returns NEW items (if any).
    • If data is not empty:
      • Prepend new items to the top of the UI list.
      • Update previousCursor to the new top item.
    • If data is empty:
      • Do nothing (keep using the old previousCursor).

Development Tasks & Estimates

No.Task NameEstimate (Hours)Dependencies
1Database Indexing: Create indices on BidHistory and Bid tables2 hoursNone
2DAL Implementation: Write SQL query with COALESCE & Subquery logic6 hoursTask 1
3Service Layer: Implement Cursor Encoding/Decoding logic4 hoursTask 2
4Controller: Create API endpoint and integrate filters3 hoursTask 3
5Unit Testing: Test Pagination, Tie-breaking, and Polling scenarios5 hoursTask 4
6Integration Testing: Verify Live Feed behavior with real data4 hoursTask 5
TotalGrand Total24 hours

Testing & Quality Assurance

Unit Test Scenarios:

  • Cursor Encoding/Decoding: Verify Base64 encoding and parsing logic
  • Validation Logic: Test limit ranges, cursor exclusivity, GUID validation
  • Authorization Rules: Verify dealer and admin access permissions

Integration Test Scenarios:

  • Standard Pagination: Verify Page 2 loads items older than Page 1 without duplicates.
  • Tie-Breaking: Create 2 bids at the exact same millisecond. Verify that scrolling does not skip the second bid.
  • Live Polling: Verify that passing PreviousCursor returns only newly inserted bids.
  • Data Integrity: Verify LastBidAmount is correct for the first bid (0) vs updated bids (Previous Amount).
  • Vehicle Context: Verify a bid placed with only VehicleSpecificationId correctly displays Make/Model (using the COALESCE fallback).
  • Empty Auction: Request feed for auction with no bids, verify graceful empty response.
  • Invalid Cursor: Pass malformed cursor, verify 400 error with appropriate message.
  • Non-Existent Auction: Request feed for invalid AuctionId, verify 404 response.
  • Mutual Exclusivity: Pass both NextCursor and PreviousCursor, verify 400 error.
  • Limit Boundaries: Test with limit=0, limit=1, limit=100, limit=101.
  • HasNextPage Logic: Fetch with exact page boundary, verify correct hasNextPage value.
  • Deleted Vehicle Data: Delete Inventory record, verify Make/Model fallback or graceful handling.
  • Authorization: Test dealer accessing non-participating auction, verify 403.
  • Concurrent Updates: Insert new bids during pagination, verify historical consistency.

Performance Test Scenarios:

  • Response Time: Verify <300ms response with 10,000+ bid feed records.
  • Index Effectiveness: Run EXPLAIN ANALYZE on query, verify index usage.
  • Polling Load: Simulate 100 concurrent clients polling every 3 seconds.
  • Cursor Overhead: Measure Base64 encoding/decoding performance impact.

Deployment Considerations

Configuration Changes

Database Migration:

Execute the following Index Creation scripts to ensure O(log N) performance for the feed:

CREATE INDEX idx_bid_auctionid ON public.bid(auctionid); CREATE INDEX idx_bidhistory_pagination ON public.bidhistory(updatedat DESC, historyid DESC); CREATE INDEX idx_bidhistory_lookup ON public.bidhistory(bidid, updatedat DESC, historyid DESC);