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)
| Question | Why it matters |
|---|---|
| Single retailer (Amazon-only) or multi-retailer comparison? |
|
| 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? |
|
| What triggers a price alert, any price drop or only drops below a user threshold? |
|
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.
| Metric | Calculation | Value |
|---|---|---|
| Products tracked | Given | 100M |
| Retailers | Given | 50+ |
| Price data points / day | 100M products x ~5 price updates | 500M (100M products x ~5 price updates avg) |
| Search queries / sec | Derived from daily volume ÷ 86400 (+ peak factor) | 10K |
| Price alert checks / day | Given (assumption documented in value) | 50M |
| Price history storage | Given | ~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.
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 minPrice 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 queuesPrice 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 tripsAPI 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.
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.
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.
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
| Concern | Solution |
|---|---|
| Stale prices |
|
| Retailer API down |
|
| Wrong product match |
|
| Price scraping blocked | Proxy rotation, rate limiting, fallback to affiliate API |
| Alert notification lost |
|
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
How helpful was this walkthrough?
Click a star to rate. We actively use this feedback to refine and update our system design content.
Discussion
Share your thoughts, ask questions, or help others.