Price Tracking Service
Design price history and threshold alerts for products collected from merchant pages or feeds.
Interview scope and guarantees
Track selected products across merchants, retain observations, let users set alert thresholds, and notify when a qualifying change occurs. A scraped price is an observation, not a guarantee that checkout will honor it. Currency, shipping and availability need explicit treatment.
Capacity worksheet
Assume 10 million product offers refreshed every 6 hours: 40 million fetches/day, about 463/s. At 2 KB per observation, full retention adds 80 GB/day. If 1% of offers change and each has 100 watchers, one round can generate 10 million evaluations; fanout requires batching and deduplication.
Concrete API contract
POST /watches {offerId,targetPrice,currency}
GET /offers/{id}/history?from=...
DELETE /watches/{id}
Internal observation {offerId,fetchId,price,currency,observedAt}Data model and access paths
offers(id PK,merchant_id,variant_key,url,currency,shipping_minor,current_observation)
observations(offer_id,observed_at,fetch_id,amount_minor,currency,shipping_minor,availability,parser_version); UNIQUE(offer_id,fetch_id)
watches(id PK,user_id,offer_id,threshold_minor,rule_version,armed,cooldown_until,enabled)
alert_keys(watch_id,rule_version,transition_id); UNIQUE(watch_id,rule_version,transition_id)Evolve a solution and explain each change
Figure — Three architecture decisions for Price Tracking Service, including the pressure each introduces.
Step 1: Save watches
Store a canonical product/variant and an explicit threshold rule. A page title alone can identify the wrong size or seller.
Step 2: Collect observations
Schedule bounded source fetches and normalize currency, availability, and price components. Cached pages and parser changes can look like genuine price drops.
Step 3: Emit threshold transitions
Version observations and create unique alert intent on eligible crossings. Repeated below-threshold polls can spam the user.
Responsibility overview
Figure — Connected responsibilities for Price Tracking Service. Trace the authoritative and derived paths separately.
Worked end-to-end scenario
A user watches a blue size-M item below 50 dollars. A fetched page advertises a 40-dollar price for size S, while size M remains 70. The observation must identify the variant before evaluating the rule. A valid later observation is 45 with shipping excluded under the user’s stated contract. Transition the watch from above to below threshold and record one alert intent for that observation/rule version. Retries reuse the alert identity. If the price stays below 50 for an hour, do not send a new alert at every poll. Define when recovery above the threshold re-arms the watch.
Why these access paths matter
Use canonical product, merchant, variant, currency, and observation time as explicit dimensions. Index watches by product for batch evaluation and schedules by next eligible fetch. Keep raw parser evidence and parser version for debugging false drops. Alerts are keyed by watch, rule version, and qualifying transition; a notification provider needs its own retry identity.
Build the baseline first
- Merchant fetch → parser validation → price observation.
- Price transition → watch evaluation → notification intent.
- History query → time-ordered observations.
Evolve the design under load
- Merchant queues → adaptive refresh priority.
- Offer-key events → batched watcher evaluation.
- Cold history → compressed time partitions.
Defend the hardest decision
Compare normalized offer identity, currency and total-price definition before declaring a drop. A parser accidentally reading installment price instead of total price can trigger a mass false alert. Validate plausible bounds, markup changes and missing fields. Trigger on a defined state transition such as above-to-below threshold, with hysteresis or cooldown to avoid repeated alerts around one boundary.
Failure and recovery analysis
A fetch completes twice after retry and emits two low-price events. Deduplicate by fetch ID and use a unique watch/transition alert key. Out-of-order observations should remain in history without replacing a newer current price. A merchant outage must not become a zero price; represent unknown separately from available and unavailable.
Security and privacy boundary
Respect merchant access policies, validate URLs against SSRF, and avoid storing user contact information in scraping jobs.
Interview follow-ups with reasoning
Question: How do you choose refresh frequency?
Show answer and explanation
Answer: Prioritize watched volatile offers within merchant rate budgets.
Question: How do you handle currency conversion?
Show answer and explanation
Answer: Preserve source currency and conversion timestamp, and distinguish converted estimates.
Question: How do you repair bad observations?
Show answer and explanation
Answer: Version corrections and suppress or explain affected alerts.
Operate and verify the design
Fetch freshness, parser anomaly rate, alert dedup hits and unknown-price fraction.
Replay a low-price observation and then an older high-price observation; verify current state and alert count.
A second scenario to test transfer
A product is observed at $49 at 10:00 and $39 at 10:10. A rule at $40 triggers once for the 10:10 observation. If the event is replayed, the unique rule/observation pair suppresses another alert. A delayed 10:05 observation arriving at 10:12 may add history but cannot replace the 10:10 latest projection.
A late $45 observation arrives after a $39 observation. What updates, and should the $40 alert fire again?
Show answer and explanation
Answer: Persist the late observation with its actual timestamp if valid, but keep $39 as latest because its observation time is newer. Do not trigger the same threshold alert again for the older record. Key alert delivery on rule and observation identity and define whether a later price rise and fall creates a new alert cycle.
Compare alternatives
| Decision | Reason | Failure to handle |
|---|---|---|
| Offer identity | Comparable variants and currency | False price comparisons |
| Append observations | Auditable history | Duplicate/reordered samples |
| Adaptive scheduling | Spend fetch budget usefully | Stale long-tail products |
| Alert idempotency | Avoid repeat messages | Rule edit during delivery |
A design-changing exercise
A merchant parser suddenly reports zero. Should every watcher be alerted?
Show answer and explanation
Answer: No. Validate extraction and availability, quarantine implausible observations, and preserve evidence. Missing or failed parsing is not a zero-price sale.
Design workshop: threshold transitions, not every cheap poll
Core scope is exact-variant offers, price history and user threshold alerts. Purchasing is excluded. Choose high-priority refresh within 15 minutes under a bounded fetch budget, retain observation provenance and send one alert per eligible crossing. A title match does not establish the same size, seller, currency or shipping policy.
Offer identity includes merchant, canonical product/variant key, seller and currency. An observation stores item amount, shipping, tax inclusion policy, availability, observedAt, fetchedAt, fetch ID and parser version. Compare a user's chosen comparable total; do not silently compare item-only dollars with shipping-inclusive euros. Missing/unavailable price is not zero.
A watch has rule_version, threshold, armed and cooldown_until. Choose this policy: alert when an armed valid latest price crosses below the threshold; disarm on that alert; rearm only after a subsequent valid latest price rises above the threshold and cooldown expires. At exactly threshold, explicitly choose <= for firing and > for rearming. Rule edits increment version and initialize armed from the chosen edit policy.
BEGIN;
SELECT * FROM watches WHERE id=:watch FOR UPDATE;
-- Verify enabled, current rule version, comparable latest observation,
-- armed=true, price<=threshold, and cooldown elapsed.
INSERT INTO alert_keys(watch_id,rule_version,transition_id)
VALUES (:watch,:version,:crossing) ON CONFLICT DO NOTHING;
-- Only if newly inserted: disarm watch, set cooldown, insert outbox.
COMMIT;Observation insertion deduplicates unique(offer,fetch_id). Update latest only when event time/tie policy is newer; delayed history must not roll the latest pointer back. The threshold transaction uses a locked current latest identity. A delayed old $39 cannot trigger after a newer $45 merely because its fetch finished later.
Figure — A $40 watch with one-hour cooldown.
Figure — Rule edit races with a queued alert.
Corrections preserve the original observation and add a referenced correction version. If a sent alert was based on a parse error, mark the evidence corrected and send a correction only under an explicit notification policy; it cannot be unsent. Do not rearm solely because a stale corrected record changes history.
Schedule by volatility, watchers and desired freshness under merchant-specific request budgets. Persist parser failures separately from genuine price changes; repeated selector failures back off and mark the offer stale. A merchant health monitor samples known variants and flags abnormal price/availability shifts before spamming users. History stores raw observations for a defined horizon and daily min/max/last summaries afterward; a daily average alone loses threshold evidence.
At one million offers polled hourly, average fetch demand is 278/s. Fifteen-minute refresh for all fourfold increases it to 1,111/s; allocate this expensive budget to watched/high-change offers. Alert delivery has its own bounded quota and lag metric.
Exercise: $39 at 10:10 triggers, then a late $45 from 10:05 arrives. Should the watch rearm?
Show answer and explanation
Answer: No. Append valid history but keep the newer latest $39 and disarmed state. Rearming requires a newer qualifying latest observation under the cooldown rule.