System Design Problem

Design a Price Comparison Engine

Commonly Asked By:GoogleAmazonPayPal

Interview Setup

Interview Prompt

Design a price comparison engine that aggregates prices from 50+ retailers for 100M products, shows price history, and notifies users when prices drop below a target.

Clarifying Questions (ask before designing)

QuestionWhy it matters
Single retailer (Amazon-only) or multi-retailer comparison?
  • Multi-retailer needs product matching across different titles and URLs
  • single-retailer simplifies to crawl + alert.
How fresh must prices be, such as hourly, daily, or real-time?Drives the tiered crawl schedule with hot products refreshed hourly for ~1M SKUs, and the long tail refreshed every 24 to 48 hours for ~89M SKUs.
Do users care about sticker price or total landed cost?
  • Price + shipping + estimated tax changes sort order
  • showing sticker price alone misleads shoppers.
What triggers a price alert, any price drop or only drops below a user threshold?
  • 50M alert checks/day against 500M price updates
  • batch matching in Flink reduces DB round trips.

Scope

In scope

  • Multi-source price ingestion (APIs, feeds, scraping)
  • Product entity resolution across retailers
  • Price normalization (currency, shipping, tax)
  • Search and comparison API
  • Price-drop alert notifications

Out of scope (state explicitly)

  • Checkout and payment processing
  • User product reviews and ratings
  • Building retailer partnerships or affiliate billing

Functional Requirements

Start by asking your interviewer how fresh prices must be and whether product matching across retailers is in scope. Clarify if price alerts and historical charts are required for this round.

  • Aggregate prices: Collect prices for the same product from multiple retailers/sellers
  • Product matching: Identify the same product across different sites (different names/URLs)
  • Price tracking: Track price history over time; show price trends and charts
  • Price alerts: Notify users when price drops below their target
  • Search & browse: Search products; filter by category, brand, price range
  • Best deal identification: Show cheapest option with total cost (price + shipping + tax)
  • Coupon integration: Show applicable coupons/deals alongside prices
  • Retailer ratings: Show retailer reliability and shipping speed

Non-Functional Requirements

Interviewers focus heavily on price freshness and search latency. Call out the ingestion vs serving split early, as crawls are inherently slow and bursty, and the user-facing comparison API must never block on web scrape latency.

  • Freshness: Prices updated at least every 6 hours; popular products every 1 hour
  • Scale: 100M+ products, 50+ retailers, 1B+ price data points
  • Search Latency: < 200 ms for product search with price comparison
  • Accuracy: Prices must reflect actual retailer prices (stale prices erode trust)
  • Availability: 99.9%

Capacity Estimations

Run this math before you size the crawl fleet. Products tracked, crawl frequency, and retailer count tell you proxy bandwidth; ClickHouse history drives storage for price charts.

MetricCalculationValue
Products trackedGiven100M
RetailersGiven50+
Price data points / day100M products x ~5 price updates500M (100M products x ~5 price updates avg)
Search queries / secDerived from daily volume ÷ 86400 (+ peak factor)10K
Price alert checks / dayGiven (assumption documented in value)50M
Price history storageGiven~2 TB/year

Architecture Diagram

In the interview: product entity resolution across retailers is the central challenge because the same SKU carries different titles across sites, requiring robust matching before price comparisons.

The system separates the ingestion path (crawling, validation, and entity resolution) from the low-latency serving path (search, comparison, and price alerts). Price updates stream through Kafka so that consumer writes never block public API queries. PostgreSQL stores canonical product metadata and current retailer offers, while ClickHouse tracks historical price points for trend charts and manipulation detection. For related commerce patterns, see the Review and Rating System and the Coupon and Discount Engine.

Loading...

Component Deep Dives

Product Matching

Product Entity Resolution

Product matching represents the core algorithmic challenge in multi-retailer aggregation because identical SKUs feature divergent titles, formats, and identifiers across platforms. Matching pipelines pair UPC barcodes with fuzzy title matching and human verification queues.

Event Bus Design (Kafka)

Topic: raw-price-events
  Partitions: 256
  Partition key: retailer_id (preserves per-retailer crawl ordering)
  Retention: 7 days (replay after Flink outage)
  Replication factor: 3, min.insync.replicas: 2

Producer: Crawler connectors + affiliate feed processors (idempotent producer)
  Event: { retailer_id, product_url, price, currency, scraped_at, raw_html_s3_key }

Consumer groups:
  1. price-pipeline: Flink handles deduplication under 5 minutes, validates changes under 50%, and performs entity resolution to map product_id
  2. alert-checker: batches affected product IDs over 60 seconds and queries alerts where target_price >= new_price
  3. anomaly-detector: flags spikes or drops exceeding 50% from the rolling median to a manual review queue
  4. history-writer: inserts time-series records into ClickHouse price_history for trend charts

Sync path: GET /compare reads Redis price cache + PostgreSQL current prices < 50ms
Async path: 500M daily price updates never block synchronous API
DLQ: raw-price-events-dlq; alert when Flink lag > 5 min

Price Ingestion Pipeline

Multi-Source Ingestion Architecture

Ingestion prioritizes high-confidence affiliate APIs first, structured bulk feeds second, and headless browser scraping as a targeted gap filler with rate limiting and proxy rotation.

Three data sources:

1. Affiliate APIs (highest data quality):
   Amazon Product Advertising API, eBay Finding API, and retailer partner APIs
   Structured data: price, availability, shipping, and product imagery
   Rate limited: approximately 1 request per second per API key
   Coverage: major national retailers

2. Data Feeds (bulk ingestion):
   Retailers provide daily CSV or XML dumps containing all active catalog prices
   Process: download feed -> parse rows -> match canonical products -> update prices
   Coverage: merchants enrolled in affiliate networks

3. Web Scraping (filling catalog gaps):
   Crawl merchant product pages when no programmatic API or feed exists
   Challenges: anti-bot fingerprinting, dynamic JavaScript rendering, and rate limits
   Compliance: strictly honor robots.txt and published terms of service
   Execution: headless Chromium (Playwright) backed by rotating residential proxy pools
    
Pipeline execution:
  Source -> Kafka (raw price events) -> Flink (deduplication, validation, matching)
    -> PostgreSQL (current prices) + ClickHouse (historical time-series)
  
Validation rules:
  - Price is positive and realistic (flagging obvious anomalies such as a $0.01 laptop)
  - Currency codes match country specifications
  - Product target URL returns HTTP 200 and remains active
  - Price changes exceeding 50% relative to prior observations trigger review queues

Price Alert System

Asynchronous Price Alert Pipeline

Price alerts invert the standard read path by evaluating price drops against stored user thresholds and batching notifications to avoid sudden alert storms.

User sets alert: "Notify me when AirPods Pro drops below $180"

Implementation:
  1. Alerts stored in PostgreSQL:
     alerts: { alert_id, user_id, product_id, target_price, active }
     Index on (product_id, active, target_price)

  2. Price update arrives for product X at $175:
     SELECT user_id FROM alerts 
     WHERE product_id = 'X' AND active = true AND target_price >= 175

  3. For each matching user: dispatch push or email notification
     Mark alert as triggered: UPDATE alerts SET active = false

  Optimization for batch processing:
    Flink consumer accumulates price updates over 1-minute tumbling windows
    Batch query: SELECT * FROM alerts WHERE product_id IN (updated_products)
                 AND target_price >= current_price AND active = true
    Dispatch notifications in batched worker pools, minimizing database round trips

API Design

Price Comparison and Alert APIs

The platform exposes RESTful endpoints to query current offers across retailers, search products by price range, and register user price-drop notifications.

HTTP
GET /api/v1/products/{product_id}/prices
--> { "product": { "name": "AirPods Pro 2", "image": "..." },
      "prices": [
        { "retailer": "Amazon", "price": 199.99, "shipping": "Free",
          "url": "https://...", "in_stock": true, "updated_at": "..." },
        { "retailer": "BestBuy", "price": 189.99, ... }
      ],
      "price_history": { "30d_low": 179.99, "30d_high": 249.99, "current_vs_avg": "-12%" } }

GET /api/v1/search?q=airpods+pro&sort=price_asc&min_price=100&max_price=300

POST /api/v1/alerts
{ "product_id": "prod-123", "target_price": 180.00 }

Common Error Responses

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff

Data Model

PostgreSQL Schema: Products and Current Prices

PostgreSQL stores canonical product definitions, current active retailer prices, and indexed user price alerts.

SQL
CREATE TABLE products (
    product_id UUID PRIMARY KEY, name TEXT, brand VARCHAR(100),
    category VARCHAR(100), upc VARCHAR(20), image_url TEXT,
    attributes JSONB, created_at TIMESTAMPTZ
);
CREATE TABLE current_prices (
    product_id UUID, retailer_id VARCHAR(50),
    price DECIMAL(10,2), currency CHAR(3), shipping_cost DECIMAL(10,2),
    in_stock BOOLEAN, url TEXT, updated_at TIMESTAMPTZ,
    PRIMARY KEY (product_id, retailer_id)
);
CREATE TABLE price_alerts (
    alert_id UUID PRIMARY KEY, user_id UUID, product_id UUID,
    target_price DECIMAL(10,2), active BOOLEAN DEFAULT TRUE,
    INDEX idx_product_active (product_id, active, target_price)
);

ClickHouse Schema: Price History

ClickHouse maintains columnar time-series price snapshots partitioned by month to generate interactive price trend charts and identify promotional manipulation.

SQL
CREATE TABLE price_history (
    product_id UUID, retailer_id String, price Float32,
    in_stock UInt8, recorded_at DateTime, recorded_date Date
) ENGINE = MergeTree() PARTITION BY toYYYYMM(recorded_at)
  ORDER BY (product_id, retailer_id, recorded_at);

Fault Tolerance

ConcernSolution
Stale prices
  • TTL-based freshness
  • show 'last updated X hours ago'
  • flag stale
Retailer API down
  • Serve last known price with staleness indicator
  • retry with backoff
Wrong product match
  • Human review queue for low-confidence matches
  • user 'report wrong match'
Price scraping blockedProxy rotation, rate limiting, fallback to affiliate API
Alert notification lost
  • Kafka at-least-once
  • dedup by (alert_id, trigger_time)

Additional Considerations

Interview Walkthrough

  • 25-minute cut

    Skip arch50/arch75 depth unless staff.

    • Crawl, normalize, match, and serve pipeline (8 min)
    • Tiered refresh rates for hot vs long-tail SKUs (9 min)
    • Total cost calculation including price, shipping, and tax (8 min)
  • Frame as a crawl-and-serve system: 100M+ products with tiered refresh rates, updating hot products hourly and long-tail items every 24 to 48 hours.
  • Explain product matching across retailers via UPC and GTIN normalization, combined with fuzzy title matching for products without barcodes.
  • Cover total-cost comparison including price, shipping, and estimated tax, emphasizing that sorting by sticker price alone misleads shoppers.
  • Walk through scraper fleet architecture: headless Chromium workers, residential proxies, and strict 1 request per second per domain rate limits.
  • Mention ClickHouse for price history: rolling 90-day medians detect fake markdown sales and price manipulation.
  • Discuss the price alert service: users set targets, background workers evaluate incoming price updates, and push notifications fire when drops occur.
  • Highlight the common pitfall of crawling every product at the same frequency, which exhausts proxy budgets on inactive listings while hot SKUs go stale.

Engineering Trade-offs

Total Cost Comparison vs Sticker Price

Price engines balance crawl freshness against retailer load limits, trading real-time accuracy for polite crawl intervals while ensuring shoppers evaluate true landed costs.

Price alone is misleading:
  Amazon: $189.99 with free shipping and no regional tax = $189.99
  Specialty Retailer: $179.99 + $9.99 shipping + $14.40 tax = $204.38

Landed cost computation:
  total_cost = price + shipping + estimated_tax

Sorting by total_cost rather than sticker price reflects actual checkout expense.
Tax estimation uses user postal code, product tax category, and state jurisdiction rules.

Web Scraper Architecture and Tiered Crawling

Partitioning products into distinct refresh tiers ensures high crawl frequency on in-demand items without exceeding proxy bandwidth budgets.

Tiered crawl distribution across 100M catalog products:

Tier 1 (Hot products, top 1%): crawl every 1 hour
  Products with > 1000 daily views or active user price alerts
  ~1M products x 24 crawls/day = 24M crawls/day

Tier 2 (Popular, top 10%): crawl every 6 hours
  ~10M products x 4 crawls/day = 40M crawls/day

Tier 3 (Long tail, 89%): crawl every 24 to 48 hours
  ~89M products x 0.5 crawls/day = 44.5M crawls/day

Total volume: ~108M crawls/day ≈ 1,250 crawls/sec

Scraper infrastructure:
  200 containerized scraper workers (Kubernetes pods)
  Worker stack: headless Chromium (Playwright) + rotating residential proxies
  Rate limits: maximum 1 request per second per worker per retailer domain

Price Manipulation Detection

Tracking historical price distributions prevents retailers from exaggerating discounts through temporary pre-sale markups.

Retailers may inflate prices immediately before promotional events to advertise deceptive discounts:
  Baseline price: $100. Retailer marks up to $150, then claims "50% off" at $75.
  Actual savings: true discount from baseline is 25%, not 50%.

Algorithmic detection:
  1. Track rolling 90-day median price per product per retailer in ClickHouse
  2. If current "original price" exceeds 120% of 90-day median, flag as promotional inflation
  3. Display transparent badges: "Lowest price in 90 days: $75" and "90-day average: $100"
  4. Emulates historical transparency standards established by CamelCamelCamel

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...