Designing a Real-Time Multi-Seller Price Comparison System

Designing a Real-Time Multi-Seller Price Comparison System

Designing a Real-Time Multi-Seller Price Comparison System

How do sites like PriceGrabber, Google Shopping, Amazon’s “Other Sellers” panel, or Skyscanner show you the cheapest price across dozens of sellers — updated within seconds of a seller changing it? This is the complete architecture, from seller feed ingestion to the moment a price update lands on a user’s screen.

01

Introduction & History

Imagine you want to buy a specific model of wireless headphones. Ten different sellers on a marketplace — or even ten different websites entirely — might be selling that exact same product, each at a different price, with different shipping costs and delivery dates. A price comparison system is the piece of engineering that watches all of those sellers at once, figures out who is cheapest right now, and shows that answer to a shopper before they even finish typing the product name.

Think of it like a group of friends who each go check a different store for the same toy, then run back and shout the price to you the moment they find out. The faster your friends run, and the more often they check, the more accurate and “real-time” your information is. A price comparison system is that same idea, except the “friends” are software services, the “stores” are seller systems or external websites, and the “running back” happens over networks in milliseconds instead of minutes.

1.1 A brief history

Late 1990s

The First Comparison Engines

Sites like PriceGrabber, DealTime, and mySimon appeared, using slow, nightly web crawlers to scrape prices from online stores. Updates were stale for hours or days — nobody expected “real time.”

Early 2000s

Structured Feeds Arrive

Sellers began publishing structured product feeds (XML/CSV) instead of raw HTML. Comparison engines could parse prices reliably instead of scraping messy web pages.

2007–2012

Marketplace-Native Comparison

Amazon’s “Buy Box” and multi-seller listings made price comparison a first-party marketplace feature — not just a third-party aggregator — because many sellers now competed on the same product page.

2013–2018

Event-Driven & Streaming

The rise of message queues (Kafka, Kinesis) and WebSockets let systems move from “poll every hour” to “push the instant a price changes,” enabling genuinely real-time comparison.

2019–Present

AI-Assisted Matching & Dynamic Pricing

Machine learning now matches products across sellers even when titles/images differ slightly, and sellers themselves use dynamic repricing bots — meaning the comparison system must react in near real time to price wars that can happen dozens of times a minute.

📌
Why this topic matters for system design interviews

This problem combines nearly every major distributed systems theme — event-driven ingestion, caching, search, consistency trade-offs, real-time push, and massive read scale — into one coherent story, which is exactly why it appears so often in senior and staff-level system design interviews.

1.2 Why “real-time” matters more today than ever before

Two decades ago, prices on physical shelves rarely changed more than once a day, so a comparison engine that refreshed once every few hours was perfectly adequate. Today, many large sellers run automated repricing software that can adjust a price dozens of times an hour in response to a competitor’s move, inventory levels, or even the time of day. If a comparison system still worked on an hourly refresh cycle, shoppers would routinely see prices that had already changed several times since the last update — and worse, a shopper might click “buy” on a price that is no longer honored, creating a poor experience and eroding trust in the platform. This shift from “prices change occasionally” to “prices change continuously and automatically” is the single biggest reason the real-time architecture in this article exists at all.

It also matters because comparison shopping has become a default consumer habit rather than a niche behavior. Shoppers routinely open multiple tabs, use browser extensions, or rely on a marketplace’s own “other sellers” panel before completing a purchase. A platform whose comparison feature feels sluggish or unreliable pushes shoppers toward a competitor’s app instead, which turns “make prices update fast” from a nice-to-have engineering goal into a direct, measurable revenue concern for the business.

02

Problem & Motivation

Let’s state the problem precisely, the way an interviewer would expect you to restate it before designing anything:

📌
Problem statement, restated

Design a system that lets a shopper see, in near real time, the lowest available price for a product across many different sellers — while each seller can change their price at any moment, independently, and without warning.

2.1 Why is this hard?

At first glance it sounds simple: “just look up the price and show it.” The difficulty comes from scale, freshness, and disagreement between sources happening simultaneously:

Challenge

Scale of Sellers

A large marketplace might have hundreds of thousands of sellers, each listing millions of products, each seller changing prices dozens of times per day through automated repricing tools.

Challenge

Freshness vs. Cost

Polling every seller every second for every product would be prohibitively expensive and would overwhelm seller systems. But polling too rarely shows shoppers stale, wrong prices.

Challenge

Product Matching

The “same” product might be listed with different titles, images, or SKUs by different sellers. The system must know these all refer to one underlying product before it can compare prices.

Challenge

Consistency Under Load

Millions of shoppers might read a price at the exact moment a seller updates it. The system must decide: show a possibly-stale price fast, or a guaranteed-fresh price slowly?

2.2 What makes this different from a typical CRUD application

A standard create-read-update-delete application usually assumes one trusted writer per record and a comfortably small number of concurrent readers. This system inverts both assumptions at once: there are thousands of independent, only partially trusted writers (each seller) contending to update the same small set of records (offers for a given product), while there are simultaneously millions of readers expecting an answer in well under a second. That combination — many untrusted concurrent writers, massive concurrent readers, and a tight latency budget — is precisely what pushes the design away from a simple synchronous request/response model and toward the event-driven, cache-heavy architecture explored throughout this article.

Real-life analogy

Picture an airport departure board. Dozens of airlines report gate and delay changes, sometimes seconds before the change takes effect. The board can’t ask each airline “has anything changed?” once per second for every flight — instead, airlines push updates the moment something changes, and the board’s job is only to display the latest pushed value quickly and correctly. A price comparison system works the same way: sellers (like airlines) push changes, and the platform’s job is to absorb, normalize, and redisplay those changes as fast as possible.

2.3 Goals of the system

  • Freshness: A shopper should see a price that is at most a few hundred milliseconds to a few seconds old.
  • Correctness: The displayed “lowest price” must actually be the lowest currently valid, in-stock offer — not a stale or unavailable one.
  • Scale: Support millions of concurrent shoppers browsing, searching, and comparing, and thousands of sellers pushing updates concurrently.
  • Extensibility: New sellers, new product categories, and new currencies/regions should be easy to onboard.
  • Resilience: A single slow or broken seller feed must never degrade the experience for products from other sellers.
💬
What an interviewer may ask

“What does ‘real-time’ actually mean here — hard real-time, or near real-time? What staleness is acceptable, and why?” Be ready to say that this is near real-time (soft, best-effort freshness measured in hundreds of milliseconds to a few seconds), not hard real-time (which implies guaranteed deadlines, as in embedded control systems).

2.4 Putting numbers on the problem

Before designing anything, it helps to reason about rough scale, because the right architecture for ten sellers looks nothing like the right architecture for a hundred thousand sellers. Consider a mid-to-large marketplace with the following back-of-envelope numbers:

200KActive sellers
50MDistinct products tracked
~5K/secPeak price-change events
2M/secPeak shopper read requests

Notice the ratio: read traffic outweighs write traffic by roughly 400 to 1. This single observation drives most of the architecture — it tells us to optimize the read path aggressively (caching, replicas, CDN) while making sure the write path is durable and correctly ordered rather than raw-throughput-optimized, since it simply doesn’t need to be as fast in absolute terms as the read path does.

2.5 Explicit non-goals

Just as important as stating goals is stating what this system deliberately does not try to solve, since a design that tries to do everything usually does nothing well:

  • This system does not guarantee the shopper will always pay the exact displayed price — checkout systems perform a final authoritative price check, and the comparison system’s job is to be a very good, very fast estimate, not the final source of truth at the moment of payment.
  • This system does not attempt hard real-time guarantees (bounded worst-case latency) — it targets a strong best-effort freshness SLA instead.
  • This system does not itself set prices or run repricing algorithms for sellers — that logic lives in each seller’s own systems; this platform only ingests, compares, and displays.
03

Core Concepts

Before diving into architecture, let’s define the building blocks in plain language. Each of these terms will come up repeatedly, so understanding them now makes the rest of the design click into place.

3.1 Product Catalog

What: A master, canonical record of every distinct product the platform knows about (e.g., “Sony WH-1000XM5 Headphones, Black”). Why: Without one canonical product, the system cannot know that Seller A’s “Sony XM5 Wireless Headset Black” and Seller B’s “Sony WH1000XM5 (Black)” are the same item. Analogy: Like a library’s master catalog card that all copies of a book — hardcover, paperback, different publishers — are linked back to. Example: Amazon’s ASIN, or Google Shopping’s GTIN/product ID, are canonical product identifiers.

3.2 Seller Offer

What: A specific seller’s listing for a canonical product — including their price, stock, shipping cost, and delivery estimate. Why: Many offers map to one product; the comparison engine’s job is to rank offers, not products. Analogy: If the product is “a taxi ride from A to B,” an offer is one specific driver’s quoted fare for that ride right now. Example: Ten sellers each have an “offer” row for the same headphones, each with its own price field.

3.3 Price Normalization

What: Converting all incoming prices into one comparable unit — same currency, same tax treatment, same shipping inclusion — before comparing them. Why: $50 + $10 shipping from Seller A is not directly comparable to €45 with free shipping from Seller B until both are converted to the same basis. Analogy: Converting recipe measurements from cups and grams to one unit before comparing which recipe uses more sugar. Example: A normalization service converts every offer to “total landed cost in USD” before ranking.

3.4 Event-Driven Ingestion

What: Sellers (or scraping/polling adapters) emit “price changed” events into a message queue rather than the platform constantly asking “did anything change?” Why: Push-based ingestion scales far better than polling millions of products on a fixed schedule. Analogy: A notification on your phone versus manually refreshing an app every 5 seconds to check for news. Example: A Kafka topic named price-events that seller adapters publish to.

3.5 Real-Time Push (WebSockets / Server-Sent Events)

What: A persistent connection between the client and server that lets the server push new prices to the browser without the browser asking again. Why: Enables shoppers watching a product page to see a price update appear live, without refreshing. Analogy: A live sports scoreboard app that updates itself, versus one you must manually refresh. Example: A WebSocket channel product:12345:prices that pushes new offer data to any subscribed browser tab.

3.6 Dead-Letter Queue

What: A separate holding queue for messages that repeatedly fail processing, rather than being retried forever or silently dropped. Why: Without one, a malformed event could either block an entire partition’s processing indefinitely, or simply vanish with no trace when discarded — both are bad outcomes for a system that must account for every price change. Analogy: A postal service’s “undeliverable mail” bin — items that couldn’t be delivered aren’t destroyed, they’re set aside for a human to investigate. Example: If a price event references a product ID that doesn’t exist in the catalog, after a few retries it lands in a dead-letter topic where an automated job or engineer can inspect and resolve it.

3.7 Read-Your-Own-Writes Consistency

What: A guarantee that after a specific actor makes a change, that same actor immediately sees their own change reflected, even if the wider system is still eventually consistent for everyone else. Why: Without it, a seller who just updated their price in a dashboard might refresh the page and see their old price, reasonably concluding the update failed. Analogy: Editing your own social media post and immediately seeing your edit, even though other viewers’ cached copies of your feed might take a moment longer to update. Example: The seller dashboard routes a seller’s own read requests to the primary database or a guaranteed-caught-up replica for a short window after they submit a change, rather than to an arbitrary potentially-lagging replica.

3.8 Eventual Consistency

What: A guarantee that all parts of the system will eventually agree on the latest price, but might briefly disagree right after a change. Why: Strong (immediate, all-or-nothing) consistency across a globally distributed, high-throughput system is prohibitively slow; eventual consistency trades a tiny, bounded staleness window for massive scalability. Analogy: Several people editing copies of a shared shopping list that sync up a few seconds later — briefly out of sync, but they converge. Example: A cache might serve a price that is 400ms old while the database already has the newer value.

3.9 Idempotency

What: A property where applying the same operation multiple times has the same effect as applying it once. Why: Message queues like Kafka typically guarantee at-least-once delivery, meaning the same price event can be delivered and processed more than once after a retry — without idempotency, a duplicate delivery could double-apply a discount or corrupt a counter. Analogy: Pressing an elevator call button five times doesn’t call five elevators — the operation “request this floor” is idempotent. Example: Each price event carries a unique event ID; the Normalization Service checks a short-lived deduplication cache before applying it, so a re-delivered event is silently ignored the second time.

3.10 Backpressure

What: A signal or mechanism that tells an upstream producer to slow down because a downstream consumer cannot keep up. Why: Without it, a burst of seller updates (say, during a major sale event) could overwhelm the Normalization Service faster than it can process, causing unbounded memory growth or crashes. Analogy: A dam’s spillway gates that open only as fast as the river downstream can safely carry the water away. Example: Kafka naturally provides backpressure because consumers pull at their own pace; the queue simply grows (bounded by retention) rather than the consumer being force-fed data it can’t process.

3.11 Rate Limiting & the Token Bucket Algorithm

What: A control that caps how many requests a client (here, a seller’s feed or a shopper’s API client) can make in a given time window. Why: Protects shared infrastructure from being overwhelmed — intentionally or accidentally — by any single caller. Analogy: A bucket that refills with tokens at a steady rate; every request consumes one token, and once the bucket is empty, requests must wait for it to refill. Example: Each seller is allotted, say, 100 price-update calls per second; a token bucket at the API Gateway enforces this cleanly, allowing short bursts while capping the sustained average rate.

3.12 Exponential Backoff & Jitter

What: A retry strategy where each failed request waits progressively longer before retrying (e.g., 1s, 2s, 4s, 8s), with a small random “jitter” added to each wait. Why: Without backoff, a failing downstream service gets hit with an immediate flood of retries, making the outage worse (a “retry storm”). Jitter prevents many clients from retrying at exactly the same moment and re-creating the same spike. Analogy: If a shop is closed, you don’t keep knocking every second — you wait a bit longer each time, and different people naturally knock at slightly different times rather than in perfect unison. Example: The Seller Feed Ingestion Service uses exponential backoff with jitter when a seller’s webhook endpoint fails, rather than hammering it every second.

3.13 Circuit Breaker

What: A safety mechanism that stops calling a failing dependency for a cooldown period after repeated failures, instead of retrying forever. Why: Prevents a slow or broken downstream dependency from exhausting threads/connections in the caller, which would otherwise cascade the failure elsewhere. Analogy: An electrical circuit breaker that trips and cuts power rather than letting a short circuit start a fire. Example: If a seller’s price API times out five times in a row, the ingestion adapter “opens” the circuit for that seller for 60 seconds before trying again, rather than queuing indefinitely.

3.14 Consistent Hashing

What: A hashing technique that maps both data (e.g., product IDs) and servers (e.g., cache/database nodes) onto the same conceptual ring, so each piece of data is owned by the nearest node on the ring. Why: When a node is added or removed, only the data between that node and its neighbor needs to move — not the entire dataset, as would happen with naive modulo hashing. Analogy: Assigning house addresses along a circular street, where adding one new house only affects its immediate neighbors’ numbering, not the whole street’s. Example: The cache tier uses consistent hashing on product ID so that resizing the Redis cluster from 8 to 10 nodes only reshuffles roughly 20% of keys, not 100%.

3.15 Connection Pooling

What: Reusing a fixed set of already-open database or network connections instead of opening a brand-new connection for every request. Why: Establishing a new TCP/TLS connection has real overhead (round trips, handshake cost); reusing connections dramatically reduces latency and resource usage under high request volume. Analogy: A taxi rank with a fixed number of cars circulating continuously, versus building a brand-new car for every single passenger. Example: The Price Comparison Service maintains a pool of, say, 50 persistent connections per instance to the database and Redis, rather than opening and closing a connection on every single request.

📌
Mental model to carry forward

Everything in this system exists to answer one question as fast, cheaply, and correctly as possible: “Right now, who has the lowest valid price for this product?” Every component either helps collect that data, helps compute that answer, or helps deliver that answer to a shopper.

04

Architecture & Components

Below is the high-level architecture. Every box is explicitly labeled with the component it represents — including the API Gateway and Load Balancer layers — so you can see exactly where each responsibility lives.

graph TD A[“Client Apps Web Mobile Voice”] –> B[“CDN Edge Cache”] B –> C[“API Gateway AuthN Rate Limit Routing”] C –> D[“Load Balancer L7”] D –> E[“Price Comparison Service Ranking Aggregation”] D –> F[“Product Catalog Service Canonical Data”] D –> G[“Notification Service WebSocket SSE Gateway”] E –> H[“Cache Layer Redis Cluster”] E –> I[“Search Index Elasticsearch”] E –> J[“Primary Database Sharded Replicated”] K[“Seller Feed Ingestion Service API Gateway for Sellers”] –> L[“Message Queue Apache Kafka”] L –> M[“Price Normalization Service Currency Tax Shipping”] M –> H M –> J M –> I M –> G N[“Seller Systems APIs Webhooks Scrapers”] –> K G –> A
Figure 4.1 — High-level architecture with every major box naming its component role explicitly.

4.1 Component breakdown

Edge

API Gateway

The single, secured front door for all client and partner traffic. Handles authentication, authorization, rate limiting, request validation, and routes each request to the correct backend service. Also used on the seller-inbound side as a dedicated “Seller API Gateway” for webhook/feed submissions.

Edge

Load Balancer

Distributes incoming requests across many identical service instances (Layer 7, HTTP-aware) so no single instance is overwhelmed, and automatically routes around unhealthy instances.

Core

Price Comparison Service

The core business-logic service. Given a product ID, it fetches all current valid offers, applies ranking rules (lowest total cost, in-stock only, seller trust score), and returns the ranked comparison.

Core

Product Catalog Service

Owns canonical product records and the mapping from many seller SKUs to one canonical product ID, using matching rules and/or ML-based fuzzy matching.

Ingest

Seller Feed Ingestion Service

The dedicated intake layer for seller-originated data — via push webhooks, scheduled API polling, or (as a fallback) compliant web scraping — normalizing all three into one internal event format.

Ingest

Message Queue (Kafka)

A durable, ordered, replayable buffer between ingestion and processing, decoupling the rate sellers push data from the rate downstream services can consume it.

Ingest

Price Normalization Service

Converts every incoming offer into a comparable, canonical form: same currency, tax treatment, and shipping-inclusive “landed cost.”

Storage

Cache Layer (Redis)

Serves the overwhelming majority of read traffic (shoppers viewing prices) directly from memory, protecting the primary database from read amplification.

Storage

Search Index (Elasticsearch)

Powers product search and filtered/sorted browsing (e.g., “show me all wireless headphones under $100, sorted by price”).

Storage

Primary Database

The durable source of truth for product, offer, and seller data, sharded and replicated for scale and availability.

Delivery

Notification Service

Maintains WebSocket/SSE connections with active shoppers and pushes price-change events to any product page currently being viewed.

External

Seller Systems

External systems outside the platform’s control — seller inventory management tools, repricing bots, or third-party marketplaces — that originate every price change.

💬
What an interviewer may ask

“Why put a Load Balancer behind the API Gateway rather than in front of it, or why have both at all?” Answer: the Gateway is a smart, request-aware layer (auth, quotas, routing rules); the Load Balancer beneath it is a dumb, fast traffic distributor across many identical instances of one service. Some architectures place a load balancer in front of the gateway too, to scale the gateway itself horizontally — it’s layers of load balancing, not a single point.

4.2 Why microservices instead of a monolith?

It’s fair to ask whether all of this really needs to be split into separate services — couldn’t one well-written application handle catalog, ingestion, normalization, and comparison together? For a small catalog with a handful of sellers, a monolith is genuinely simpler to build and operate, and is often the right starting point. But as the numbers in the Problem section show — hundreds of thousands of sellers, tens of millions of products, wildly different load patterns between writes and reads — a monolith becomes a liability for three concrete reasons. First, the ingestion pipeline needs to scale independently and burst aggressively during sales events, while the comparison service needs steady, always-on horizontal scale; bundling them means over-provisioning one to satisfy the other. Second, a bug or slowdown in, say, currency conversion logic should never be able to take down the ability to serve already-cached comparisons — separate services provide a natural fault boundary. Third, different teams typically own different domains (catalog quality, seller integrations, ranking algorithms) and independent services let them deploy on their own schedules without a shared release train blocking everyone.

4.3 Component responsibilities at a glance

ComponentOwnsScales ByFailure Mode If Down
API GatewayAuth, routing, rate limitsAdding gateway instances behind its own LBAll external traffic blocked — highest-severity outage
Load BalancerTraffic distributionManaged service, scales automaticallyTraffic cannot reach healthy instances
Price Comparison ServiceRanking, aggregationStateless horizontal scalingReads fail; cached CDN responses may still serve briefly
Product Catalog ServiceCanonical product recordsRead replicas, horizontal scalingNew product matching stalls; existing comparisons unaffected
Seller Feed IngestionIntake & validationConsumer group scaling per source typeNew price updates delayed; last-known prices still served
Message Queue (Kafka)Durable, ordered event bufferAdding partitions and brokersIngestion backs up; no data loss within retention window
Normalization ServiceCurrency, tax, shipping mathConsumer group scalingPrices stop updating; stale prices continue serving
Cache LayerFast read pathAdding shards/nodesReads fall through to DB; latency rises, may cascade
Search IndexBrowse/search/sortAdding shards/replicasSearch degrades; direct product lookups unaffected
Primary DatabaseSource of truthSharding + read replicasWrites fail; cache can serve reads temporarily
Notification ServiceReal-time pushHorizontal scaling of connection handlersLive push stops; client falls back to polling

This table is worth building explicitly in an interview setting, because it demonstrates you’ve thought not just about what each piece does when everything works, but what specifically breaks — and what still keeps working — when it doesn’t.

4.4 Seller-side ingestion in detail

The “Seller Feed Ingestion Service” box in Figure 4.1 is itself a small system with three distinct intake mechanisms, since not every seller integrates the same way:

graph TD A[“Seller Webhook Push Signed HTTPS Callback”] –> D[“Seller API Gateway AuthN Schema Validation”] B[“Scheduled Polling Adapter Seller REST SOAP API”] –> D C[“Compliant Scraper Adapter No API Sellers”] –> D D –> E[“Format Normalizer Canonical Event Schema”] E –> F[“Message Queue Apache Kafka”]
Figure 4.2 — Three intake mechanisms converge into one canonical event format before reaching the queue.

Webhooks are strongly preferred wherever a seller supports them, since they are push-based and near-instant. Scheduled polling is the fallback for sellers whose systems only expose a pull-based API — polling frequency here is often tiered by seller sales volume, so a top seller might be polled every 10 seconds while a low-volume seller is polled every few minutes. Scraping is used only as a last resort, is rate-limited aggressively to respect the target site, and is treated as inherently less reliable — its output is weighted with lower confidence in the anomaly-detection step described in the Security section.

05

Internal Working

Let’s trace exactly what happens, component by component, when a seller changes a price.

5.1 Step-by-step lifecycle

  1. Seller emits a change. A seller’s repricing bot decides to lower the price of a product from $79.99 to $74.99. It either (a) calls a webhook the platform exposed to them, (b) the platform’s polling adapter notices the change on its next scheduled poll of the seller’s feed, or (c) — only as a last resort for sellers with no API — a compliant scraper adapter detects the change on the seller’s public page.
  2. Ingestion & validation. The Seller Feed Ingestion Service receives the raw payload, validates its shape and authenticity (signed webhook, API key, or scraper checksum), and converts it into one internal canonical event format regardless of source. Invalid or suspicious events (e.g., price of $0, or a 90% price drop that looks like a data error) are flagged for a sanity-check step rather than published directly.
  3. Publish to queue. The validated event is published to a Kafka topic partitioned by product ID, guaranteeing all events for the same product are processed in order by the same consumer.
  4. Normalization. The Price Normalization Service consumes the event, converts currency if needed, applies tax rules for the shopper’s likely region, folds in shipping cost, and produces a “landed cost” figure — the number that will actually be compared against other sellers’ offers.
  5. Write-through to cache and database. The normalized offer is written to the primary database (durable source of truth) and to the Redis cache (fast read path) in the same logical step, along with a version/timestamp so stale writes can be detected and discarded if they arrive out of order.
  6. Search index update. The updated price is also pushed into Elasticsearch so that browse/search/sort-by-price experiences reflect the new price without waiting for a batch reindex.
  7. Push to active viewers. If any shoppers currently have that product page open, the Notification Service pushes the new price over their open WebSocket/SSE connection so the price visibly updates on their screen without a page refresh.

5.2 End-to-end sequence diagram

sequenceDiagram participant Seller as Seller System participant Ingest as Feed Ingestion Service participant Queue as Kafka Queue participant Norm as Normalization Service participant Cache as Redis Cache participant DB as Primary Database participant Search as Search Index participant Push as Notification Service participant Client as Shopper Browser Seller->>Ingest: Push price update via webhook Ingest->>Ingest: Validate signature and sanity check Ingest->>Queue: Publish normalized format event Queue->>Norm: Consume event in product key order Norm->>Norm: Convert currency tax shipping Norm->>DB: Write landed cost offer Norm->>Cache: Update cached offer Norm->>Search: Update indexed price Norm->>Push: Emit price changed notification Push->>Client: Deliver update over WebSocket
Figure 5.1 — End-to-end lifecycle of a single price change event.
💬
A subtle trap: out-of-order events

Because sellers can push updates faster than the pipeline can process them, an older price event could theoretically be processed after a newer one if not handled carefully. Partitioning Kafka by product ID and attaching a monotonically increasing version/timestamp to every event — then discarding any write whose version is older than what’s already stored — prevents an old price from ever “winning” over a newer one.

5.3 The lifecycle of a single offer, as a state machine

It’s useful to think of every individual offer (one seller’s price for one product) as moving through a small set of well-defined states, rather than as a single opaque “price” field. This makes error handling and debugging far more tractable, because at any moment you can ask “which state is this offer stuck in?”

stateDiagram-v2 [*] –> Received Received –> Validated: passes schema and auth checks Received –> Rejected: fails validation Validated –> Normalized: currency tax shipping applied Validated –> Flagged: fails sanity anomaly check Flagged –> Normalized: manual or automated approval Flagged –> Rejected: confirmed bad data Normalized –> Published: written to cache DB index Published –> Superseded: newer event for same offer arrives Rejected –> [*] Superseded –> [*]
Figure 5.2 — State machine for a single price offer moving through the pipeline.

5.4 Error handling & retry strategy

Failures happen at every hop of this pipeline, so each step needs an explicit strategy rather than hoping errors don’t occur. If the Normalization Service cannot reach the currency-rate provider, it retries with exponential backoff and jitter (introduced in Core Concepts) rather than retrying immediately in a tight loop. If retries are exhausted, the event is written to a dead-letter queue — a separate Kafka topic holding events that could not be processed — so it is never silently dropped, and an on-call engineer or automated job can inspect and reprocess it later. If the database write succeeds but the cache write fails, the system relies on the cache’s own short TTL to self-heal — the stale cache entry will simply expire and be refreshed from the now-correct database value within seconds, which is a deliberate and acceptable trade-off given the very short TTLs used for hot products. If, conversely, the database write itself fails after the event has already been marked as consumed from the queue, the consumer must not blindly commit its offset — instead, the commit only happens after a successful write, so a crash or database outage mid-processing simply results in the event being re-delivered and reprocessed on restart, which is safe precisely because the write itself is idempotent.

It’s also worth designing explicitly for the case where an entire downstream dependency — say, the search index — is unavailable for an extended period while the database and cache continue working normally. Rather than blocking the whole pipeline on the slowest dependency, each downstream write (database, cache, search index, push notification) is treated as an independent, separately-retried step. A search index outage delays only search-result freshness, while direct product-page price lookups (served from cache and database) continue to update normally and immediately — a good illustration of why decoupling the fan-out into independent steps, rather than one large all-or-nothing transaction, keeps a partial outage from becoming a total one.

RetryWithBackoff.java — exponential backoff with jitter
public class RetryWithBackoff {

    public static <T> T executeWithRetry(
            Callable<T> task, int maxAttempts) throws Exception {

        int attempt = 0;
        while (true) {
            try {
                return task.call();
            } catch (Exception ex) {
                attempt++;
                if (attempt >= maxAttempts) {
                    throw ex; // exhausted retries, send to dead-letter queue
                }
                long baseDelayMs = (long) Math.pow(2, attempt) * 100;
                long jitterMs = ThreadLocalRandom.current().nextLong(50, 150);
                Thread.sleep(baseDelayMs + jitterMs);
            }
        }
    }
}
06

Data Flow & Lifecycle

It helps to separate the system into two distinct data flows that run continuously and independently: the write path (sellers pushing price changes in) and the read path (shoppers pulling comparisons out). They share storage but have very different performance requirements.

Write Path Characteristics

  • Lower total volume (thousands of updates/sec at peak) but bursty around sales events.
  • Requires strong ordering per product and durability — losing a price update is a correctness bug.
  • Tolerates a few hundred milliseconds of processing latency before the price is visible.

Read Path Characteristics

  • Enormous volume (millions of reads/sec at peak) dominated by the same popular products.
  • Tolerates brief staleness (eventual consistency) in exchange for very low read latency.
  • Must degrade gracefully — a slow read path is far worse than a slightly stale one.

6.1 Read path walkthrough

  1. Shopper opens a product page or searches for a product category.
  2. Request hits the CDN — for a specific hot product page, edge caching may serve a very recent snapshot directly.
  3. Cache miss (or dynamic comparison request) routes through the API Gateway and Load Balancer to the Price Comparison Service.
  4. The service checks Redis first; on a cache hit, it returns immediately.
  5. On a cache miss, it queries the database, ranks the offers, writes the result back into Redis, and returns it.
  6. The client opens a WebSocket subscription for that product so future price changes are pushed live.
~95%Read requests served from cache
<50msTypical cached read latency
<2sEnd-to-end write-to-visible latency (P95)
1:1000Approx. write-to-read ratio

6.2 Why three different data stores for one piece of data?

It can seem redundant that a single price update ends up written to the database, the cache, and the search index — three copies of essentially the same fact. This redundancy is deliberate, because each store is optimized for a different access pattern that the others handle poorly. The database is the durable source of truth, optimized for correctness and point lookups by product/seller key. The cache is optimized purely for raw read speed on the hottest keys, trading some memory cost for sub-millisecond access. The search index is optimized for a completely different query shape — free-text search combined with filtering and sorting across millions of products at once, which neither a key-value cache nor a traditional row-oriented database index handles efficiently. Accepting the operational cost of keeping three stores in sync is the price paid for each individual read — whether it’s “get this one offer,” “check the cache,” or “search and sort 50,000 headphones by price” — being fast.

6.3 Lifecycle beyond the first write

A price doesn’t just get written once and sit static — offers continuously expire and get refreshed even without an explicit seller update. Every cached offer carries a TTL; if a seller’s feed goes quiet for longer than a defined threshold (say, 30 minutes with no update and no successful poll), the offer is proactively marked “unconfirmed” and deprioritized in ranking, rather than being trusted indefinitely just because it was correct the last time anyone heard from that seller. This protects shoppers from acting on a price that might have silently gone stale because a seller’s integration quietly broke rather than because nothing changed.

07

Advantages, Disadvantages & Trade-offs

Advantages of an Event-Driven, Cache-First Design

  • Scales read traffic almost independently of write traffic.
  • New sellers/feeds plug into the same ingestion pipeline without redesigning downstream services.
  • A slow or broken seller feed is isolated by the queue and cannot block other sellers’ updates.
  • Real-time push gives a noticeably better user experience than manual refresh.

Disadvantages / Costs

  • Eventual consistency means shoppers can occasionally see a price that changed a moment ago.
  • Operating Kafka, Redis, Elasticsearch, and WebSocket infrastructure adds real operational complexity.
  • Product matching across sellers is genuinely hard and never 100% accurate.
  • Real-time push at scale (millions of open sockets) is itself a significant engineering challenge.

7.1 Key trade-off: consistency vs. latency

This is fundamentally a CAP theorem trade-off (explored further in Advanced Topics). Choosing to serve reads from cache — accepting a small staleness window — is a deliberate choice of availability and low latency over strict consistency. The alternative (always reading the primary database directly) would guarantee freshness but collapse under read load and increase latency for every shopper, every time.

ApproachFreshnessRead LatencySystem Load
Always read primary DBPerfectHigh (100–300ms)Very high
Cache-first, event-driven invalidationNear-real-time (100s of ms stale)Low (<50ms)Low
Batch nightly recomputeUp to 24h staleVery lowVery low

7.2 Trade-off: push (webhooks) vs. poll (scheduled fetch)

Choosing how sellers deliver price changes is itself a significant trade-off decision, not an implementation detail:

DimensionPush (Webhooks)Poll (Scheduled Fetch)
FreshnessNear-instant — seller notifies the moment a change happensBounded by poll interval, e.g., up to 30–60s stale
Seller integration effortHigher — seller must implement and maintain a reliable webhook senderLower — platform-driven, seller only exposes a read API
Resilience to seller downtimeCan silently miss events if the seller’s sender fails without retry logicSelf-healing — next poll picks up the current true state regardless of history
Load on seller systemsMinimal — one call per actual changeConstant background load regardless of whether anything changed

In practice, production systems use both, favoring webhooks for large, technically sophisticated sellers and falling back to polling for the long tail of smaller sellers — with polling also serving as a periodic reconciliation check even for webhook-integrated sellers, to catch any events a broken webhook sender might have silently dropped.

7.3 Trade-off: normalization accuracy vs. processing speed

The Price Normalization Service could, in principle, apply extremely precise, region-specific tax rules for every possible shopper jurisdiction before publishing a price — but doing so for every single incoming event would add meaningful processing latency to the write path, directly hurting the freshness metric the whole system is built around. Most production designs instead normalize to a reasonably accurate default (e.g., the seller’s home jurisdiction tax treatment, or a common baseline region) immediately for speed, and apply final, fully precise, shopper-specific tax and duty calculations only at the point the shopper actually views a specific offer or proceeds to checkout — trading a small amount of display-time approximation for a much faster, more scalable write path, while guaranteeing the number that actually matters (what the shopper is charged) is always computed precisely at the moment it counts.

7.4 Trade-off: build vs. buy for infrastructure components

Not every piece of this architecture needs to be built in-house. Message queuing, search indexing, and caching are all available as fully managed cloud services (e.g., managed Kafka offerings, managed Elasticsearch/OpenSearch, managed Redis). Using managed services trades some cost and a degree of control for dramatically reduced operational burden — no need to run upgrade cycles, capacity planning, or on-call rotations for the underlying infrastructure. Most teams building a system like this choose managed infrastructure for the “commodity” pieces (queue, cache, search) and reserve custom engineering effort for the parts that actually differentiate the product — the normalization rules, matching logic, and ranking algorithm that make the comparison genuinely useful.

08

Performance & Scalability

Two very different scaling problems exist side by side: scaling the number of sellers/products (write-side) and scaling the number of shoppers (read-side).

8.1 Scaling the write side

  • Partitioning by product ID: Kafka topics are partitioned so that all updates for a given product route to the same partition, preserving per-product ordering while allowing horizontal scale-out across products.
  • Backpressure-aware consumers: The Normalization Service scales out consumer instances independently as ingestion volume grows, without needing to touch the ingestion layer.
  • Rate limiting per seller: The Seller Feed Ingestion Service enforces per-seller quotas so one misbehaving repricing bot cannot flood the pipeline.

8.2 Scaling the read side

  • Multi-tier caching: CDN edge cache for the most popular products, Redis for everything else, database only on cold misses.
  • Read replicas: Database read replicas absorb the residual read traffic that misses the cache, especially for less popular (“long-tail”) products.
  • Horizontal service scaling: The Price Comparison Service is stateless, so it scales horizontally behind the Load Balancer simply by adding instances.
graph LR A[“Shopper Request”] –> B[“API Gateway”] B –> C[“Load Balancer”] C –> D[“Price Comparison Service”] D –> E{“Cache Hit”} E –>|Yes| F[“Return from Redis Cache”] E –>|No| G[“Query Database Read Replica”] G –> H[“Populate Cache”] H –> F F –> I[“Response to Shopper”]
Figure 8.1 — Read-path scaling: cache-first with database fallback.
💬
What an interviewer may ask

“How would you use Little’s Law here?” Little’s Law states $L = lambda times W$ (average number of requests in the system equals arrival rate times average time each spends in the system). If the Price Comparison Service sees 50,000 requests/sec and each takes 40ms, roughly 2,000 requests are “in flight” at any moment — which tells you how many concurrent worker threads/connections you must be able to hold without saturating.

8.3 Handling hot products (the “thundering herd”)

During a flash sale, one product might receive far more traffic than average — a “hot key” problem. Mitigations include: caching at multiple layers (CDN + Redis), using a short client-side/edge TTL with background refresh so the cache never fully expires under load, and request coalescing (if 1,000 requests arrive for the same missing cache key simultaneously, only one actually queries the database while the rest wait for that result).

8.4 Worked example: sizing the comparison service with Little’s Law

Suppose product launches and marketing campaigns push the Price Comparison Service to a sustained peak of 80,000 requests per second, and each request — including the cache lookup and ranking logic — takes an average of 25 milliseconds to complete. Little’s Law ($L = lambda times W$) tells us the average number of requests “in flight” inside the service at any instant is $80{,}000 times 0.025 = 2{,}000$ concurrent requests. If each service instance is configured to safely handle 200 concurrent requests before latency starts degrading, this tells us we need at least $2{,}000 div 200 = 10$ healthy instances at peak — and capacity planning should target perhaps 15–20 instances to leave headroom for an instance failing health checks or a sudden 30% traffic spike. This is exactly the kind of quick, defensible capacity estimate an interviewer wants to see derived on the spot, rather than an arbitrary guessed number.

8.5 Capacity planning for seasonal peaks

Retail-adjacent systems like this one have a distinctive traffic shape: relatively steady baseline demand punctuated by predictable, enormous spikes around major shopping events. Rather than treating every spike as a surprise to be handled purely by reactive auto-scaling — which can lag behind a sudden 20x jump by the time new instances boot and warm up — mature teams model these events explicitly ahead of time. Historical traffic from the previous year’s equivalent event, adjusted for expected year-over-year growth, becomes the baseline capacity target; pre-warming caches, pre-scaling the service tier an hour before a known sale start time, and pre-approving temporary quota increases with the cloud provider are all standard practice. Auto-scaling then handles the residual, harder-to-predict variance on top of that pre-provisioned baseline, rather than being asked to handle the entire spike reactively from a cold start.

8.6 Database read scaling: replicas and sharding together

For the residual traffic that does reach the database (cache misses on long-tail products), a single layer of scaling is rarely enough at this volume. Sharding by product ID spreads data — and therefore write load — across many independent database clusters, while read replicas within each shard absorb read traffic without burdening the shard’s write leader. The combination means a single very popular shard can still be read-scaled independently by adding more replicas to just that shard, without needing to re-shard the entire dataset.

09

High Availability & Reliability

A price comparison feature that is down, or shows wrong prices, directly costs sales and erodes shopper trust — so this system is designed assuming components will fail, and focuses on containing the blast radius.

graph TD A[“Shopper”] –> B[“Global Load Balancer DNS GSLB”] B –> C[“Region 1 API Gateway Load Balancer”] B –> D[“Region 2 API Gateway Load Balancer”] C –> E[“Active Service Cluster”] D –> F[“Standby Service Cluster”] E –> G[“Primary Database”] F –> H[“Replica Database”] G -.->|Async Replication| H
Figure 9.1 — Multi-region failover: Region 2 stands ready if Region 1 degrades.

9.1 Key reliability techniques

Technique

Circuit Breakers

If a seller’s feed adapter starts timing out repeatedly, a circuit breaker trips and stops calling it for a cooldown period, preventing cascading slowness elsewhere in the ingestion pipeline.

Technique

Bulkheads

Ingestion adapters for different sellers run in isolated resource pools so one seller’s misbehaving feed cannot exhaust threads/connections needed by others.

Technique

Graceful Degradation

If the real-time push layer fails, the system falls back to periodic polling from the client rather than showing no price at all.

Technique

Replication & Failover

Both the database and Redis run as replicated clusters with automatic failover, so a single node failure doesn’t cause an outage.

💬
Reliability vs. freshness tension

During an outage of the Normalization Service, should the system show the last-known price (possibly stale) or hide the price entirely (definitely unhelpful)? Most production systems choose to show the last-known price with a subtle “may not reflect the latest price” indicator — availability of some useful information usually beats withholding information outright.

9.2 Disaster recovery: RTO and RPO

Two metrics anchor any disaster recovery plan. Recovery Time Objective (RTO) is how long the system may reasonably be down before the business impact becomes unacceptable — for this system, a reasonable target might be under five minutes for a full regional failover. Recovery Point Objective (RPO) is how much data (in this case, how many seconds of price events) the business can tolerate losing in a worst-case disaster — given asynchronous cross-region replication, an RPO of a few seconds is typical, meaning at most a handful of the very latest price events might need to be replayed from the Kafka log after failover, since Kafka retains events for a configurable retention window and can be re-consumed from an earlier offset.

9.3 Backup strategy

Beyond live replication, the primary database takes regular automated snapshots (e.g., every few hours) plus continuous write-ahead log shipping, allowing point-in-time recovery — restoring the database to its exact state at any recent moment, not just to the last snapshot — which matters if a bad deployment corrupts data rather than a hardware failure destroying it.

9.4 A graceful degradation ladder

Rather than treating availability as binary (fully up or fully down), it helps to design an explicit ladder of degraded-but-still-useful states the system can fall back through as dependencies fail, each rung less ideal than the one above it but still far better than showing an error:

  1. Full experience: Live comparison with real-time push, fully fresh prices.
  2. Degraded push: WebSocket layer is down; client falls back to polling the REST API every few seconds instead of receiving instant pushes.
  3. Degraded freshness: Normalization Service is delayed; the API serves the last successfully processed prices with a visible “may be slightly outdated” indicator rather than blocking.
  4. Cache-only mode: Database is unreachable; the API serves whatever is currently in cache, refusing only genuinely uncached (cold) product lookups.
  5. Read-only maintenance mode: Only in a severe, multi-component outage does the system stop accepting new seller price updates entirely while continuing to serve the best available cached comparisons to shoppers.

Designing this ladder in advance — rather than improvising during an actual incident — means on-call engineers have a clear, pre-agreed set of fallback behaviors to trigger, and shoppers experience a gradually degraded but still functional product instead of a hard outage.

9.5 Chaos engineering

Rather than waiting for a real outage to discover a weakness, mature teams deliberately inject controlled failures into a staging or even production environment — killing a random service instance, adding artificial network latency to a dependency, or simulating a full region outage — to verify that circuit breakers, retries, and failover actually behave as designed under real conditions, rather than only in theory. Running these “game days” regularly catches assumptions that silently broke as the system evolved.

10

Security

This system ingests data from thousands of external, only-partially-trusted seller systems, and serves data to millions of anonymous shoppers — so it has meaningful attack surface on both sides.

10.1 Seller-side (inbound) security

  • Signed webhooks / API keys: Every inbound seller update must be authenticated (HMAC signature or per-seller API key) so an attacker cannot inject fake price data.
  • Anomaly detection: A 95% price drop or a price of $0.01 gets flagged for review rather than published instantly — preventing both honest bugs and deliberate price manipulation from reaching shoppers.
  • Per-seller rate limiting: Prevents both accidental floods and deliberate denial-of-service attempts from a compromised seller account.

10.2 Shopper-side (outbound) security

  • API Gateway authentication & rate limiting: Protects against scraping/abuse of the comparison API itself.
  • TLS everywhere: All client-server and service-to-service traffic is encrypted in transit.
  • Least privilege between services: The Notification Service, for example, should only be able to read prices — never write them — limiting the damage if it were ever compromised.
💬
What an interviewer may ask

“How would you prevent a seller from gaming the ranking to appear as the ‘lowest price’ fraudulently?” A strong answer combines signed, verifiable feeds; automated anomaly detection on price deltas; a seller trust/reputation score factored into ranking; and post-hoc auditing that compares the advertised price to what shoppers were actually charged at checkout.

10.3 Multi-factor authentication for the seller portal

While the machine-to-machine ingestion API relies on signed webhooks and API keys, the human-facing seller dashboard — where a seller’s staff can log in and manually adjust prices, view analytics, or manage their integration settings — requires multi-factor authentication (MFA) for login, given that a compromised seller account could otherwise be used to push fraudulent low prices designed to attract fraudulent orders, or to disrupt a competitor’s listing through unauthorized access. Enforcing MFA specifically for any account with write access to pricing data is a proportionate control given the direct financial impact a compromised account could cause.

10.4 Web application firewall & DDoS protection

Because the read-facing API is public and high-traffic, it sits behind a Web Application Firewall (WAF) that filters known attack patterns (SQL injection attempts, malformed requests, known bad IP ranges) before traffic even reaches the API Gateway, and behind a dedicated DDoS mitigation layer at the network edge that can absorb and scrub volumetric attacks — a real risk for any publicly popular price comparison feature, since it is an attractive target for scraping-bot floods disguised as legitimate shopper traffic.

10.5 Encryption at rest and seller credential management

All persisted data — the primary database, cache snapshots, and backups — is encrypted at rest, not just in transit, so that a stolen disk or backup file is not directly usable. Seller API keys and webhook signing secrets are stored in a dedicated secrets manager rather than in application configuration files, with automated periodic rotation, so a leaked credential has a bounded window of usefulness to an attacker rather than remaining valid indefinitely.

10.6 Secure API design for the seller-facing gateway

The seller-facing ingestion API applies the same rigor as any public API: strict request schema validation (rejecting malformed payloads before they reach business logic), per-seller API key scoping (a key for Seller A cannot be used to submit prices for Seller B’s products), and comprehensive audit logging of every price submission — who submitted it, when, and from what source — which becomes essential evidence if a pricing dispute or suspected manipulation needs to be investigated after the fact.

10.7 Least privilege between internal services

ServiceCan WriteCan Read
Price Normalization ServiceOffers, cache entriesCurrency rates, product catalog
Notification ServiceNothing (read-only)Cached offers, subscription list
Price Comparison ServiceCache entries (on miss)Offers, catalog, cache
Seller Feed Ingestion ServiceRaw event queueSeller credentials (own scope only)

Enforcing this table at the infrastructure level — via service-to-service authentication and scoped database roles, not just application-level convention — means that even if one service were compromised, the attacker’s reach is limited to that service’s narrow, explicitly granted permissions.

11

Monitoring, Logging & Metrics

Because “the wrong price silently shown to a shopper” is a correctness bug that produces no errors or crashes, observability here must go beyond typical uptime/latency metrics — it must actively watch for correctness drift.

Metric

Freshness Lag

Time between a seller’s price change and it becoming visible to shoppers — tracked as a distribution (P50/P95/P99), alerting if P95 exceeds a few seconds.

Metric

Cache Hit Ratio

Percentage of reads served from Redis vs. falling through to the database — a sudden drop signals cache invalidation storms or a hot-key problem.

Metric

Ingestion Lag

Consumer lag on the Kafka topic — how far behind the Normalization Service is from the latest published event, per partition.

Metric

Seller Feed Health

Per-seller success/error rates and response times, so one misbehaving seller integration is caught before it degrades the shared pipeline.

Metric

Price Sanity Alerts

Automated alerts on statistically unusual price deltas, catching bugs or manipulation before they reach shoppers at scale.

Metric

WebSocket Connection Health

Active connection counts, reconnect rates, and push delivery success rates for the real-time notification layer.

11.1 Distributed tracing

A single price update traverses seven or more services (ingestion, queue, normalization, cache, database, search, push). Distributed tracing (e.g., OpenTelemetry with a trace ID attached to every event) lets engineers see exactly where a specific update spent its time — critical for debugging “why did this price take 8 seconds to show up?”

11.2 SLIs, SLOs, and error budgets

Rather than vague goals like “be fast” and “be reliable,” mature teams define concrete Service Level Indicators (SLIs) — measurable signals such as P95 read latency or P95 freshness lag — and set a Service Level Objective (SLO) target for each, such as “P95 freshness lag under 2 seconds, measured over a rolling 30-day window.” The gap between 100% and the SLO becomes the error budget — for a 99.9% availability SLO, roughly 43 minutes of budget exists per month. As long as the team is within budget, they can ship new features and take reasonable risks; once the budget is exhausted, the team shifts focus to stability work until the budget replenishes. This turns “reliability” from a subjective argument into an objective, shared number everyone can look at.

11.3 Alerting philosophy: alert on symptoms, not causes

A common mistake in observability design is creating an alert for every possible internal cause — high CPU on one service, one slow database query, one dropped connection — which quickly produces so much noise that on-call engineers start ignoring alerts altogether. A more durable approach is to alert primarily on user-facing symptoms defined by the SLOs above — freshness lag breaching its threshold, error rate on the public API exceeding its budget, cache hit ratio dropping sharply — and treat internal-cause metrics (CPU, individual query latency, consumer lag on one specific partition) as diagnostic data to consult only once a symptom-level alert has already fired. This keeps the signal-to-noise ratio high and ensures every page an engineer receives at 3 a.m. genuinely reflects something a shopper or seller would notice.

11.4 Structured logging

Every service emits structured (JSON) logs rather than free-text strings, with consistent fields — traceId, productId, sellerId, eventVersion, service, latencyMs — across every service. This uniformity is what makes it possible to query “show me every log line related to product X in the last five minutes, across all seven services it touched” in a centralized log aggregation tool, rather than manually grepping through each service’s individual log files.

12

Deployment & Cloud

Each component is deployed as an independently scalable, containerized service (Docker, orchestrated by Kubernetes), which allows the write-heavy ingestion pipeline and the read-heavy comparison service to scale on entirely different schedules.

Deployment

Blue-Green Deployments

New versions of the Price Comparison Service are deployed to a parallel “green” environment and traffic is switched over only after health checks pass, allowing instant rollback if something is wrong.

Deployment

Canary Releases

Changes to ranking logic or normalization rules are rolled out to a small percentage of traffic first, with automated comparison of output against the previous version before a full rollout.

Deployment

Multi-Region Deployment

Regional deployments keep read latency low for shoppers worldwide and provide the failover capability described in the HA section.

Deployment

Infrastructure as Code

Terraform/CloudFormation define every Kafka topic, Redis cluster, and service deployment, ensuring environments are reproducible and auditable.

📌
Cost optimization tip

The Seller Feed Ingestion Service and Normalization Service can auto-scale aggressively during known peak windows (e.g., flash sales) and scale down afterward, since write volume is far more predictable and bursty than the steadier read volume — saving significant compute cost versus statically over-provisioning both tiers equally.

12.1 CI/CD pipeline

A typical pipeline for the Price Comparison Service runs through several automated gates before any code reaches production: unit tests, integration tests against a test instance of the cache/database, a container image build pushed to a private container registry, automated security scanning of that image for known vulnerabilities, deployment to a staging environment for smoke tests, and only then a canary rollout to production. Each gate can automatically halt the pipeline and roll back if it fails, meaning a broken change is caught well before it can affect real shoppers.

12.2 Container registry & image management

Every service is built into a versioned, immutable container image and pushed to a private container registry. Kubernetes then pulls a specific, pinned image version for each deployment — never “latest” — which guarantees that a rollback always restores an exact, previously-known-good state rather than an ambiguous moving target.

12.3 Environment parity

Staging environments mirror production as closely as is cost-effective — same service topology, same Kafka topic structure, same cache configuration — just at smaller scale. This “environment parity” principle catches an entire category of bugs (“it worked in staging but broke in prod”) that arise purely from configuration or topology differences rather than code differences.

12.4 Cloud region selection & data residency

Beyond pure latency optimization, region selection is also shaped by data residency requirements — many jurisdictions require that personal or transactional data about their residents be stored within regional boundaries. Since this system is largely about product and pricing data rather than personal shopper data, residency constraints are lighter here than in, say, a payments system, but seller account details and any shopper-identifying analytics data still need to respect the relevant regional requirements, which factors directly into which cloud regions host which pieces of the primary database.

13

Databases, Caching & Load Balancing

13.1 Database choice & sharding

The primary database favors a model that handles high write throughput and horizontal scale well — many production systems use a distributed NoSQL or NewSQL store (e.g., a wide-column or document store) sharded by product ID, with each shard replicated for durability and read scaling.

graph TD A[“API Gateway”] –> B[“Load Balancer”] B –> C[“Price Comparison Service”] C –> D[“Shard Router Consistent Hashing on Product ID”] D –> E[“Shard 1 Products A to H”] D –> F[“Shard 2 Products I to P”] D –> G[“Shard 3 Products Q to Z”] E –> H[“Replica Set 1”] F –> I[“Replica Set 2”] G –> J[“Replica Set 3”]
Figure 13.1 — Product-ID sharding with per-shard replica sets.

13.2 Why consistent hashing?

Consistent hashing minimizes the number of products that must be reshuffled when a shard is added or removed — only a fraction of keys move, rather than nearly all of them, which matters enormously when the dataset spans hundreds of millions of offers.

13.3 Caching strategy

PatternHow it works hereTrade-off
Cache-asideService checks Redis first; on miss, reads DB and populates cacheSimple, but first reader after expiry pays full latency
Write-throughNormalization Service writes to DB and cache together on every updateCache always fresh, but every write does double work
TTL + background refreshKeys expire but a background job refreshes hot keys before expiryAvoids thundering herd on hot products, adds complexity

This system uses write-through caching for hot/popular products combined with cache-aside with short TTLs for the long tail — balancing freshness for the products that matter most against memory cost for the millions of rarely-viewed ones.

13.4 Load balancing approaches

  • Round-robin / least-connections at the service tier, spreading requests evenly across stateless Price Comparison Service instances.
  • Consistent hashing at the cache tier, so the same product ID always routes to the same Redis node — maximizing cache hit rate.
  • Geo-based (GSLB) at the global tier, directing shoppers to their nearest healthy region.

13.5 Indexing strategy

The primary database indexes offers by (productId, sellerId) as the primary lookup key, with a secondary index on sellerId alone to support “show me all of this seller’s listings” queries used by seller dashboards. The Elasticsearch index, by contrast, is built for very different access patterns — full-text search on product titles/descriptions, faceted filtering (brand, category, price range), and sorting by computed landed cost — which is precisely why a separate search index exists alongside the primary database rather than trying to serve both access patterns from one store.

13.6 Handling replication lag

Because read replicas apply changes asynchronously, a replica can lag behind its leader by anywhere from single-digit milliseconds under normal load to a few seconds under heavy write pressure. For most comparison reads this is entirely acceptable given the system’s overall eventual-consistency stance. However, immediately after a shopper’s own action — for instance, right after a seller updates their own listing in a seller dashboard — the system routes that specific “read your own write” request to the primary (or to a replica confirmed to be caught up) rather than to an arbitrary replica, so the seller isn’t confused by seeing their own just-made change appear to have not taken effect.

13.7 Connection pooling at the database tier

With potentially thousands of Price Comparison Service instances, naively allowing each instance to open its own large pool of direct database connections would exhaust the database’s maximum connection limit almost immediately. A connection pooler sitting between the service tier and the database (such as a dedicated pooling proxy) multiplexes many logical application connections onto a smaller number of actual database connections, which is what makes horizontal scaling of the stateless service tier practical without redesigning the database tier every time capacity is added.

14

APIs & Microservices

Each service exposes a small, focused API. Below is a simplified Java interface for the core comparison lookup, and the internal event contract used between ingestion and normalization.

PriceComparisonController.java — public REST endpoint
@RestController
@RequestMapping("/api/v1/products")
public class PriceComparisonController {

    private final PriceComparisonService comparisonService;

    public PriceComparisonController(PriceComparisonService comparisonService) {
        this.comparisonService = comparisonService;
    }

    // Returns the ranked list of current valid offers for a product
    @GetMapping("/{productId}/offers")
    public ResponseEntity<OfferComparisonResponse> getOffers(
            @PathVariable String productId) {

        OfferComparisonResponse response =
                comparisonService.getRankedOffers(productId);

        return ResponseEntity.ok(response);
    }
}
PriceChangeEvent.java — canonical internal event contract
// Canonical internal event published to the Kafka "price-events" topic
public class PriceChangeEvent {
    private String productId;
    private String sellerId;
    private BigDecimal rawPrice;
    private String currency;
    private BigDecimal shippingCost;
    private boolean inStock;
    private long eventVersion; // monotonically increasing, prevents out-of-order writes
    private Instant observedAt;

    // getters and setters omitted for brevity
}

14.1 Microservice boundaries

Services are split by business capability, not by technical layer — the Product Catalog Service owns everything about “what is this product,” while the Price Comparison Service owns everything about “what does it cost right now.” This means each team can deploy, scale, and evolve their service independently, as long as the event and API contracts between them stay stable.

💬
What an interviewer may ask

“Would you use REST or gRPC between these internal services?” gRPC is often preferred for high-throughput, low-latency internal service-to-service calls (e.g., Comparison Service to Cache/DB layers) due to binary serialization and HTTP/2 multiplexing, while REST/JSON remains common for the public-facing API Gateway layer where broad client compatibility matters more than raw throughput.

14.2 API versioning

The public-facing API is versioned explicitly in the URL path (/api/v1/...), and breaking changes are only ever introduced in a new version (/api/v2/...), never by silently altering the existing contract. Both versions are typically kept running in parallel for a deprecation window (often 6–12 months) with clear communication and monitoring of v1 traffic, so client applications (mobile apps that can’t be force-updated instantly, third-party integrations) have a realistic runway to migrate before the old version is retired.

14.3 A token bucket rate limiter

Below is a simplified implementation of the token bucket rate limiter (introduced in Core Concepts) as applied at the Seller API Gateway to cap how often any single seller can push price updates.

TokenBucketRateLimiter.java — per-seller quota enforcement
public class TokenBucketRateLimiter {

    private final int capacity;
    private final double refillRatePerSecond;
    private double availableTokens;
    private long lastRefillTimestamp;

    public TokenBucketRateLimiter(int capacity, double refillRatePerSecond) {
        this.capacity = capacity;
        this.refillRatePerSecond = refillRatePerSecond;
        this.availableTokens = capacity;
        this.lastRefillTimestamp = System.nanoTime();
    }

    public synchronized boolean tryConsume() {
        refill();
        if (availableTokens >= 1) {
            availableTokens -= 1;
            return true;
        }
        return false; // seller must wait, request is rejected with HTTP 429
    }

    private void refill() {
        long now = System.nanoTime();
        double secondsElapsed = (now - lastRefillTimestamp) / 1_000_000_000.0;
        availableTokens = Math.min(capacity, availableTokens + secondsElapsed * refillRatePerSecond);
        lastRefillTimestamp = now;
    }
}

14.4 Additional API endpoints

EndpointPurpose
GET /api/v1/products/{id}/offersRanked list of current valid offers for a product
GET /api/v1/products/search?q=...Search/browse products with price-based sorting and filters
POST /api/v1/sellers/{id}/pricesSeller-authenticated bulk price submission endpoint
WS /ws/v1/products/{id}/subscribeWebSocket subscription for live price updates on one product
15

Design Patterns & Anti-Patterns

15.1 Patterns used

Pattern

Event Sourcing (partial)

Every price change is stored as an immutable event in Kafka before being applied, giving a full audit trail of every price a seller ever offered — useful for disputes and analytics.

Pattern

CQRS

Writes (price updates) and reads (comparisons) go through entirely separate paths optimized independently — the write path prioritizes ordering and durability, the read path prioritizes latency.

Pattern

Circuit Breaker

Isolates failing seller feed adapters so they cannot cascade failures into the shared ingestion pipeline.

Pattern

Strangler Fig

Used when migrating legacy scraping-based ingestion to modern webhook-based ingestion seller-by-seller, without a risky big-bang cutover.

Pattern

Bulkhead

Named after ship compartments that stop one flooded section from sinking the whole vessel — isolated resource pools per seller integration mean one exhausted pool cannot starve resources needed by another seller’s traffic.

Pattern

Backpressure / Queue-Based Load Leveling

The Kafka queue between ingestion and normalization smooths out bursty seller traffic into a steadier stream the downstream consumer can process at its own sustainable pace, rather than being forced to match the producer’s instantaneous rate.

15.2 Saga pattern — where it does (and doesn’t) apply

The Saga pattern coordinates a sequence of local transactions across services with compensating actions if a later step fails — commonly used for, say, an order checkout flow spanning payment, inventory, and shipping services. It’s worth explicitly noting this pattern is not heavily used within the price comparison pipeline itself, since updating cache, database, and search index for a price change are independent, idempotent, retryable writes rather than a multi-step business transaction requiring rollback semantics — recognizing when a pattern doesn’t fit is as valuable in an interview as knowing when it does.

15.3 Domain-specific anti-pattern: the “God Ranking Function”

A subtle anti-pattern specific to comparison systems is letting the ranking logic accumulate an ever-growing pile of special-case rules over time — one condition for a specific seller’s negotiated placement, another for a specific category’s shipping quirks, another for a one-off promotional override — until the function becomes an unreadable, untestable tangle that nobody fully understands or trusts. The fix is treating ranking as a composable pipeline of small, independently testable scoring rules (base price, seller trust, delivery speed, promotional weight) combined through an explicit, documented formula, rather than a monolithic function accumulating exceptions. This keeps the ranking logic auditable — an important property given how directly it affects both shopper trust and seller revenue.

15.4 Anti-patterns to avoid

Common Mistakes

  • Synchronous fan-out on write: Having the ingestion service directly call the database, cache, and search index synchronously for every event — instead of via the queue — creates tight coupling and cascading slowness.
  • No idempotency keys: Without them, retried webhook deliveries from sellers can double-apply price changes.
  • One giant “Pricing Service”: Merging catalog, normalization, ranking, and notification into one monolith removes the independent scaling and fault-isolation this design relies on.
  • Ignoring clock skew: Relying on wall-clock timestamps from seller systems (which may be skewed) instead of a monotonic event version for ordering.

Fixes

  • Always publish through the queue; let each downstream consumer update its own store asynchronously.
  • Attach an idempotency key to every seller event; deduplicate at the ingestion boundary.
  • Split services by capability, each with its own datastore and deployment lifecycle.
  • Use a monotonic version counter per product-seller pair, not wall-clock time, for ordering decisions.
16

Best Practices & Common Mistakes

  • Always show a “last updated” indicator next to prices so shoppers understand freshness rather than assuming absolute real-time accuracy.
  • Validate price sanity automatically before publishing — a $0 or negative price is almost always a bug, not a real offer.
  • Design idempotent consumers everywhere in the pipeline, since at-least-once delivery (the norm for Kafka) means the same event can arrive twice.
  • Decouple product matching from price comparison — matching is a separate, harder ML/rules problem and should not block the comparison service’s critical path.
  • Load test the read path at 10x expected peak — sudden traffic surges (viral products, flash sales) are the norm, not the exception, for this kind of feature.
💬
Common mistake: over-trusting seller data

Teams sometimes assume seller-provided prices are always accurate and publish them instantly. In practice, seller systems have bugs too — an automated sanity/anomaly check before publishing catches a meaningful fraction of erroneous prices before shoppers ever see them.

16.1 Additional practices worth adopting

  • Version every schema change to the internal event format, and support reading both the old and new version for a transition period, so a partial rollout of the Normalization Service doesn’t break on events from a not-yet-updated Ingestion Service.
  • Treat the seller onboarding flow as a first-class product, not an afterthought — the easier it is for a new seller to integrate correctly, the fewer malformed or unreliable feeds the platform has to defensively handle later.
  • Separate “hot” and “long-tail” caching policies explicitly — applying one universal cache TTL to both a viral bestseller and an obscure niche product wastes memory on the long tail or under-serves freshness on hot items; segment cache policy by observed traffic.
  • Run regular reconciliation jobs that compare a sample of live prices against a fresh, direct check of the seller’s source, catching silent pipeline drift (a stuck consumer, a silently failing webhook) before shoppers notice it themselves.
  • Make staleness visible, not just internally measured — surfacing a lightweight “updated moments ago” indicator in the UI turns an internal SLO into user-visible trust.

16.2 Common mistakes beyond trusting seller data

  • Treating cache invalidation as an afterthought: Bolting caching onto an already-built read path late in development often leads to inconsistent invalidation logic scattered across the codebase; designing the cache strategy alongside the data model from day one avoids this.
  • Under-provisioning for known peak events: Flash sales and major shopping holidays are predictable in advance — capacity planning should explicitly model these rather than relying purely on reactive auto-scaling that may not react fast enough for a sudden 50x spike.
  • Skipping load testing of the write path: Teams often load-test the read path heavily (since it’s higher volume) but under-test the write path’s behavior during a coordinated multi-seller repricing event, which is precisely when correctness matters most.
17

Real-World & Industry Examples

Marketplace

Amazon — Buy Box & Other Sellers

Amazon runs continuous, automated evaluation of every seller’s price, shipping, and performance to decide who “wins” the default Buy Box for a listing, recomputed essentially continuously as sellers reprice, at massive scale across hundreds of millions of listings.

Search

Google Shopping

Ingests structured product feeds from millions of merchants worldwide, normalizes currency and shipping, and serves ranked comparison results as part of Search — an event-driven ingestion and normalization pipeline conceptually very similar to this design.

Travel

Skyscanner / Kayak

Same core problem in a different domain: many “sellers” (airlines, OTAs) offering the same “product” (a flight), requiring real-time price aggregation, normalization across currencies/fare rules, and fast fan-out to a live search results page.

Ride-hail

Uber / Lyft Surge Comparison

While not multi-seller in the traditional sense, these platforms solve an analogous real-time pricing propagation problem — a price computed centrally must reach millions of client apps within a second or two of changing.

Auction

eBay Multi-Seller Listings

Many eBay product pages aggregate multiple sellers offering the same or similar item; eBay’s backend must continuously re-rank offers by price, seller rating, and shipping terms as any seller adjusts their listing, at a catalog scale spanning well over a billion active listings.

Retail

Walmart & Best Buy Marketplace

Both retailers run first-party marketplaces where thousands of third-party sellers compete on the same product listing alongside the retailer’s own inventory, requiring the same kind of continuous price and stock reconciliation described throughout this article.

Meta-Search

Booking.com / Google Flights

These platforms aggregate live, constantly-changing fares and room rates from hundreds of airlines, hotels, and OTAs simultaneously — arguably an even harder version of this problem, since travel inventory (a specific seat, a specific room) can sell out mid-comparison, adding an availability dimension on top of price.

📌
The recurring theme

At scale, the hard part is rarely computing one comparison — it’s computing millions of comparisons per second while a thousand independent sellers change the inputs underneath you. Every real-world system above solves this same shape of problem: many concurrent, only-partially-trusted writers competing on a small set of records that millions of readers query at the same time.

18

Advanced Topics

18.1 CAP theorem in this system

The CAP theorem states a distributed system can only fully guarantee two of Consistency, Availability, and Partition tolerance at once. Since network partitions are a fact of life at scale, the real choice is between CP (consistent but may reject requests during a partition) and AP (available but may briefly serve stale data). This system deliberately chooses AP for the read path — shoppers get a fast answer that’s very likely correct, rather than occasionally getting no answer at all while the system re-establishes strict consistency.

18.2 Replication & consensus

Database shards use leader-based replication (e.g., Raft-style consensus) so that writes go to one leader per shard and are replicated to followers, giving durability and read scaling while avoiding the complexity of full multi-leader conflict resolution for what is, per shard, a fairly low write volume.

18.3 Concurrency considerations

Multiple price updates for the same product can arrive concurrently from different sellers (not conflicting — they’re independent offers) but a single seller’s own repricing bot might also send rapid successive updates for one product. Per-product-per-seller sequencing (via Kafka partition key = productId+sellerId) plus optimistic concurrency control (a version check on write) prevents a race where an older update overwrites a newer one.

18.4 Failure recovery

If the Normalization Service crashes mid-processing, Kafka’s consumer offset mechanism ensures no event is lost — processing resumes from the last committed offset on restart. Combined with idempotent writes (keyed by event version), this guarantees at-least-once delivery without risking duplicate or out-of-order application of price changes.

💬
What an interviewer may ask

“How would exactly-once processing change this design, and is it worth it?” True exactly-once semantics (e.g., via Kafka transactions) add coordination overhead and reduce throughput. Because writes here are naturally idempotent (a later price simply overwrites an earlier one, keyed by version), at-least-once delivery combined with idempotent consumers achieves the same observable correctness far more cheaply — a great example of choosing a simpler guarantee that’s sufficient for the actual problem.

18.5 Product matching at scale

Matching offers from different sellers to one canonical product typically combines deterministic matching (shared identifiers like UPC/GTIN/ISBN when available) with probabilistic/ML matching (title similarity, image embeddings, attribute comparison) for the large fraction of listings that lack a clean shared identifier — with a human review queue for low-confidence matches above a certain sales volume.

18.6 Consistent hashing, worked through

Recall from Core Concepts that consistent hashing places both cache nodes and data keys on a conceptual ring using a hash function. To look up which node owns a given product ID, hash the product ID to get a position on the ring, then walk clockwise until the first node is found — that node owns the key. When a node is added, it only takes ownership of the small arc of the ring between itself and the next node clockwise, meaning only keys in that narrow arc need to move. Many production systems also add “virtual nodes” — each physical cache server is represented by many points on the ring rather than just one — which spreads load more evenly and avoids any single physical node ending up responsible for a disproportionately large arc purely by hash chance.

18.7 Distributed locking for concurrent repricing

While most price updates are independent writes that don’t conflict, occasionally two processes might need to update the same offer record concurrently — for example, an automated fraud-review process flagging an offer at the same moment the Normalization Service is writing a legitimate update to it. A distributed lock (commonly implemented using a data store like Redis with a time-bound lock key, or via a coordination service like ZooKeeper/etcd) ensures only one writer touches that specific record at a time, with the lock automatically expiring even if the lock holder crashes, so the system doesn’t deadlock waiting for a lock that will never be released.

18.8 Exactly-once vs. at-least-once, revisited

It’s worth stating plainly why this system explicitly chooses at-least-once delivery combined with idempotent processing over pursuing true exactly-once semantics end-to-end. Exactly-once processing across a distributed pipeline generally requires either distributed transactions (which add coordination latency at every hop) or careful two-phase commit-style protocols — both of which meaningfully reduce throughput and increase complexity. Since every write in this pipeline is naturally idempotent (a later price event with a higher version number simply overwrites an earlier one; applying the same event twice produces the same final state), the weaker, cheaper at-least-once guarantee is fully sufficient. This is a recurring theme in distributed systems design: understand what guarantee the problem actually requires, and resist paying for a stronger, more expensive one than necessary.

19

Frequently Asked Questions

Q1

Why not just poll every seller every second?

At scale (thousands of sellers × millions of products), constant polling wastes enormous bandwidth and compute on products that haven’t changed, and can overwhelm seller systems that aren’t built for that request volume. Event-driven push scales with the actual rate of change, not the size of the catalog.

Q2

What happens if two sellers have the exact same lowest price?

A tiebreaker policy applies secondary ranking factors — typically seller trust/performance score, delivery speed, or simple recency of listing — so the ranking remains deterministic rather than arbitrary.

Q3

How is currency conversion kept accurate?

The Normalization Service pulls exchange rates from a dedicated rates service/provider on a scheduled refresh (e.g., hourly), and every normalized offer stores the rate used at normalization time for auditability, rather than converting on the fly at read time.

Q4

Can this system support flash sales with 100x traffic spikes?

Yes — the stateless service tier auto-scales horizontally, the cache layer absorbs the vast majority of reads, and request coalescing prevents a “hot key” surge from overwhelming the database on cache misses.

Q5

Is WebSocket the only option for real-time push?

No — Server-Sent Events (SSE) is a simpler one-directional alternative when the client only needs to receive updates, and long-polling remains a reasonable fallback for clients/networks that don’t support persistent connections well.

Q6

How does the system handle a product going out of stock mid-comparison?

Stock status is treated as part of the offer itself, alongside price. An out-of-stock offer is immediately excluded from ranking rather than shown with a stale price, and the same event-driven pipeline that propagates price changes propagates stock-status changes with identical urgency.

Q7

What if two regions disagree on the “current” price during a network partition?

This is the CAP theorem in practice. The system favors availability — each region continues serving its own most-recently-known price rather than refusing to answer — and relies on asynchronous replication to reconcile once the partition heals, accepting a brief window of possible cross-region disagreement as the cost of staying available.

Q8

How would you test that the freshness SLO is actually being met in production?

By emitting a synthetic, known price change from a test seller account on a fixed interval and measuring, end-to-end, how long it takes to appear via the public API and the WebSocket channel — a continuous, automated canary that exercises the entire real pipeline rather than relying solely on internal component metrics.

Q9

Why not use a single global cache instead of per-region caches?

A single global cache would force every read, regardless of the shopper’s location, to cross regions — reintroducing exactly the latency problem regional deployment is meant to solve. Per-region caches, kept in sync via the same event pipeline that feeds the database, keep reads local while accepting a small propagation delay between regions.

Q10

How would you extend this design to support price-drop alerts a shopper subscribes to, even when they’re not actively viewing the page?

This reuses the same event pipeline rather than requiring a new one. When a normalized price event is published, the Notification Service already knows the product ID that changed; it checks a separate subscription store (shopper ID to product ID, populated whenever someone taps “notify me”) and, for any match below the shopper’s target price, enqueues a push notification or email job instead of a live WebSocket message — the difference is only the delivery channel, not the underlying detection mechanism.

20

Summary & Key Takeaways

A real-time, multi-seller price comparison system is fundamentally two pipelines sharing one data layer: a durable, ordered, event-driven write path that absorbs constant seller price changes, and a fast, cache-first, horizontally-scaled read path that serves ranked comparisons to millions of shoppers. The two are deliberately decoupled by a message queue so that neither one’s failure modes or scaling needs constrain the other.

📌
The one idea to remember

Every design decision in this system — event-driven ingestion, cache-first reads, eventual consistency, per-product ordering — exists to answer one question, “who has the lowest valid price right now,” as fast and cheaply as possible, at a scale where “ask every seller every time” is simply not an option.

20.1 Key takeaways

  • Separate the seller-facing write path from the shopper-facing read path; connect them with a durable, ordered queue.
  • Normalize every offer to one comparable basis (currency, tax, shipping) before ranking.
  • Cache aggressively for reads; accept eventual consistency as a deliberate, well-understood trade-off.
  • Isolate failure per seller feed so one bad integration never degrades the whole platform.
  • Monitor freshness and correctness explicitly — not just uptime — since stale or wrong prices fail silently.
  • Prefer idempotent, at-least-once processing over expensive exactly-once guarantees the problem doesn’t actually require.
  • Design for failure from the start — circuit breakers, bulkheads, and graceful degradation are not optional extras but core requirements.
  • Treat security (signed feeds, anomaly detection, least privilege) as inseparable from correctness, since a manipulated price is as harmful to trust as a slow one.

20.2 A closing narrative

If you take one narrative away from this entire design: the system starts with an independent, unreliable set of sellers each changing prices whenever they like, and ends with a single, fast, trustworthy number a shopper can act on in under a second. Every layer discussed above — ingestion, queueing, normalization, caching, ranking, push delivery, and the observability wrapped around all of it — exists purely to make that unreliable-to-trustworthy transformation happen reliably, at massive scale, without ever letting one misbehaving seller or one traffic spike take down the experience for everyone else.

20.3 Interview preparation guidance

For anyone studying this as interview preparation, it’s worth practicing explaining this design at three different depths: a thirty-second elevator summary (event-driven ingestion feeding a cache-first read path, connected by a durable queue), a five-minute whiteboard walkthrough (the architecture diagram in Section 4 plus the lifecycle in Section 5), and a deep dive into any single component an interviewer chooses to probe further — whether that’s the sharding strategy, the consistency model, or the failure-handling approach. Being able to zoom fluidly between these three levels of detail, rather than only having one fixed depth of explanation prepared, is usually what distinguishes a strong system design interview performance from an average one. Practicing all three levels ahead of time, on a real whiteboard or blank document rather than only reading passively, is the single highest-leverage preparation step for turning this material into confident, fluent interview delivery.