Designing a Real-Time Stock Alert System for an E-Commerce Platform

Designing a Real-Time Stock Alert System for an E-Commerce Platform

Designing a Real-Time Stock Alert System for an E-Commerce Platform

A ground-up, interview-ready deep-dive into building a notification system that tells millions of waiting customers, within seconds, the moment an out-of-stock product comes back — without melting your database or spamming anyone twice.

01

Introduction & History

Picture this: you go to buy the sneakers, the phone, or the limited-edition action figure you’ve been wanting, and the page says “Out of Stock.” Ten years ago your only option was to keep refreshing the page every hour, hoping to catch it live. Today almost every large e-commerce platform — Amazon, Flipkart, Zara, BestBuy, Nike — offers a small button that says “Notify Me” or “Email me when available.” You click it once, close the tab, and go on with your life. Then, sometimes days later, sometimes seconds later, a push notification, email, or SMS lands in your pocket: “It’s back! Grab it before it’s gone again.”

That tiny feature, from the customer’s point of view, looks trivial. From a systems point of view, it is one of the more interesting real-time, event-driven problems in e-commerce engineering. It touches inventory management, event streaming, fan-out messaging, multi-channel delivery, rate limiting, and extreme scale — all at once.

1.1 A brief history: how stock alerts evolved

Historically, stock alerts started as simple cron jobs: a batch script would run every few hours, query the inventory database for products whose stock count changed from zero to a positive number, join that against a table of “subscribers” for that product, and fire off a batch of emails. This worked fine when a site had thousands of users and dozens of restocks a day. But as e-commerce scaled into hundreds of millions of users and flash-sale culture (Black Friday, festival sales, sneaker drops) became normal, the batch approach became painfully slow and unreliable. A restock during a flash sale might sell out again in 90 seconds — a batch job running every 15 minutes would notify customers about stock that no longer exists, causing anger and lost trust.

Era 1

Batch / Cron Jobs

Nightly or hourly scripts joined inventory tables against subscriber tables and blasted out emails. Cheap to build, but slow and stale — restocks were often already sold out by the time the batch fired.

Era 2

Polling

Clients or backend services polled the inventory API at high frequency. Freshness improved noticeably, but at the cost of massive, wasteful read amplification that competed with real product-page and checkout traffic for database capacity.

Era 3

Event-Driven, Push-Based

Inventory changes are captured as events the instant they happen (via CDC) and streamed straight to a matching and fan-out pipeline. Notifications land in under a second at scales exceeding a million relevant requests per minute.

This tutorial designs a system firmly in that third era — one that can react within a second or two of a stock change and can do so at the scale of a global marketplace processing over a million relevant requests every single minute.

Real-life analogy

Think of an old-fashioned bakery with a waiting list. Instead of everyone crowding the counter asking “is the bread ready yet?” every five minutes (polling), you write your name on a list pinned to the wall. The moment fresh bread comes out of the oven, the baker’s assistant glances at that list and runs out to tell everyone on it, in order, before the bread runs out again. Our system is that assistant — except instead of one bakery, it’s watching millions of “ovens” (products) at once, and instead of running to tell people, it sends a push notification, an email, or a text message within a second or two.

02

Problem & Motivation

Let’s define the problem precisely, because in a system design interview, precision here is what separates a strong answer from a vague one.

2.1 Functional requirements

  • A logged-in (or guest, via email) customer can click “Notify Me” on any out-of-stock product detail page.
  • The moment that product’s stock count moves from 0 to a positive number, every subscribed customer should be notified through their preferred channel(s): push notification, email, SMS, or in-app real-time alert.
  • A customer should not be notified twice for the same restock event, and should not be spammed if stock flickers (0 → 1 → 0 → 5) rapidly, e.g., due to concurrent orders during a flash sale.
  • Customers can unsubscribe, and subscriptions should expire automatically after a configurable period (e.g., 90 days) to avoid unbounded growth.
  • The system must support both single-item restocks and mass restocks (a warehouse receives a huge shipment of 50,000 SKUs at once, e.g., post-holiday replenishment).

2.2 Non-functional requirements

  • Scale: the platform must sustain on the order of 1,000,000+ requests per minute (~16,700 requests/second sustained, with bursts 5–10x higher during flash sales) across subscription writes, stock-change events, and notification dispatch combined.
  • Latency: end-to-end delivery (stock becomes available → user receives a push/in-app alert) should be under 2–5 seconds at p99 for push/in-app, and under 30–60 seconds for email/SMS (which depend on slower third-party providers).
  • Reliability: no lost notifications for high-value subscriptions; the system should be at-least-once for delivery attempts but must dedupe on the client-visible side so users don’t see duplicate alerts.
  • Consistency: eventual consistency is acceptable between “actual warehouse stock” and “what the notification system believes,” but the gap should be small (sub-second to low-seconds via CDC).
  • Availability: 99.95%+ uptime for the subscription-write path (customers must always be able to register interest) and for the notification-dispatch path.
  • Fairness: if 200,000 people are subscribed to a product that restocks with only 500 units, notifications should go out to everyone roughly simultaneously — the system must not favor whoever happens to be processed first in a way that meaningfully changes their odds of purchasing (a fairness/ethics dimension many teams overlook).

2.3 The scale of the problem in numbers

1M+/minCombined requests at sustained peak
~170K/sFlash-sale burst rate (5–10x sustained)
<2–5sp99 push/in-app latency target
99.95%Uptime target for write & dispatch paths
💬
Why naive polling fails at this scale

If the inventory service has 50 million SKUs and even 1% have active subscribers, polling every SKU every 10 seconds to check for stock changes means 500,000 reads every 10 seconds just for polling — before a single notification is even sent. That read load competes directly with the checkout and product-page traffic that actually makes the company money. Event-driven design turns this from “poll everything, all the time” into “react only when something actually changes,” which is dramatically cheaper and faster.

💬
What an interviewer may ask

“Why not just have the client poll a ‘check stock’ endpoint every few seconds while the notify button is active?” A strong answer explains the read amplification problem above, and pivots to change-data-capture plus an event stream as the scalable alternative, while acknowledging polling is fine for a single very popular restock countdown page as a secondary, low-frequency fallback.

03

Core Concepts

Before diving into architecture, let’s build a shared vocabulary. Each concept below is explained the way you’d explain it to someone who has never built a distributed system before.

3.1 Change Data Capture (CDC)

What: CDC is a technique for watching a database’s internal transaction log (like MySQL’s binlog or PostgreSQL’s WAL — Write-Ahead Log) and turning every insert, update, or delete into a stream of events, without the application having to explicitly “publish” anything.

Why: Without CDC, every service that writes to the inventory table would need to remember to also publish an event — easy to forget, easy to get wrong, and it doubles the chance of a bug causing a write to succeed while the event silently fails. CDC guarantees that if the row changed in the database, an event was produced — no code path can “forget.”

Analogy: Imagine a security camera pointed at a warehouse’s stock ledger book. Every time someone writes a new number in the ledger, the camera notices the pen moving and immediately radios “the number on page 42 changed” to everyone who needs to know — the ledger-writer doesn’t need to remember to call anyone.

Practical example: We use Debezium, an open-source CDC connector, watching the inventory PostgreSQL database. Whenever a row in the inventory_stock table changes such that quantity goes from 0 to a positive value, Debezium emits a structured event to a Kafka topic called stock.updated.

3.2 Event Streaming (Kafka)

What: Apache Kafka is a distributed, append-only log system. Producers write messages (“events”) to named topics; many independent consumers can read the same topic at their own pace, and messages are retained for a configurable window even after being read.

Why: Kafka decouples the inventory system from the notification system. The inventory team never needs to know or care who is listening to stock changes; new consumers (like a future “back-in-stock analytics dashboard”) can be added without touching inventory code at all.

Analogy: Kafka is like a public radio broadcast tower. The DJ (producer) just plays the song (event); anyone with a radio tuned to that station (consumer) hears it, whether it’s 1 listener or 1 million. The DJ never needs to know how many people are listening.

3.3 Fan-out

What: Fan-out means taking one event (one product restocked) and expanding it into many downstream actions (one notification per subscriber, which could be 1 or 500,000 people).

Why it matters here: A single restock event for a wildly popular sneaker can fan out into hundreds of thousands of individual notification jobs within the same second. The system must be designed so this “fan-out storm” doesn’t overwhelm downstream push/email/SMS providers or the databases tracking delivery status.

3.4 Idempotency & Deduplication

What: An idempotent operation produces the same end result no matter how many times it’s applied. Deduplication is the practice of detecting and discarding repeat events/messages before they cause a duplicate side-effect (like sending the same email twice).

Why: Distributed systems built on queues and streams are usually “at-least-once” — a message might be delivered/processed more than once due to retries, consumer rebalances, or network blips. Without idempotency keys, a customer could receive five copies of “it’s back in stock!” for one restock.

Practical example: Every notification job carries a deterministic idempotency key: hash(userId + productId + restockEventId). Before actually sending, the dispatcher checks a Redis SETNX (set-if-not-exists) with a short TTL; if the key already exists, the job is dropped as a duplicate.

3.5 Backpressure & Rate Limiting

What: Backpressure is a mechanism that lets a slow downstream component signal to a fast upstream component “slow down, I can’t keep up,” rather than being overwhelmed and falling over. Rate limiting caps how many requests/notifications a component or user can trigger in a given time window.

Why: Email and SMS providers (SES, Twilio) have hard sending rate limits and per-account quotas. Without backpressure and rate limiting, a fan-out of 500,000 notifications could get the platform’s SES account throttled or even suspended for sending “too fast.”

3.6 Eventual Consistency

What: A consistency model where, given no new updates, all parts of the system will eventually agree on the same value — but at any given instant, some parts may be slightly behind.

Why it’s acceptable here: It’s fine if the notification pipeline’s view of “is this product in stock” lags the actual warehouse database by a few hundred milliseconds. It would not be fine if the actual checkout/payment system used the same loose consistency — that needs strong consistency to avoid overselling. This distinction — strong consistency for money-moving paths, eventual consistency for notification/informational paths — is a key system design insight.

💬
What an interviewer may ask

“How do you guarantee exactly-once notification delivery?” The honest, senior-level answer: you generally can’t guarantee exactly-once delivery across a network boundary to a third-party provider (the ack from Twilio/SES could be lost even though the SMS/email was sent). What you CAN guarantee is exactly-once processing internally via idempotency keys, and you design the customer experience to tolerate rare duplicate deliveries (e.g., a duplicate push notification is annoying but not harmful) while treating chargeable actions (an SMS costs money) with stricter dedup.

3.7 Consistent Hashing & Sharding

What: Consistent hashing is a technique for mapping keys (like productId) to a fixed set of shards or nodes in a way that, when the number of nodes changes, only a small fraction of keys need to move — unlike naive modulo hashing, where adding one node reshuffles almost everything.

Why it matters here: The Subscription DB and Redis Cluster both grow over time as the catalog and customer base grow. Using consistent hashing (or Redis Cluster’s built-in hash-slot mechanism, which is a variant of the same idea) means we can add new PostgreSQL shards or Redis nodes during a scale-up event without triggering a full data reshuffle that could take the system offline for hours.

Analogy: Imagine a round clock face with 360 marks, and each database shard “owns” an arc of that clock. A product’s ID hashes to a specific mark on the clock, and whichever shard owns that arc stores its data. Adding a new shard just means giving it a small arc carved out of neighbors — everyone else’s arc, and therefore their data, mostly stays put.

3.8 CAP Theorem in this context

What: The CAP theorem states that a distributed system can provide at most two of three guarantees simultaneously during a network partition: Consistency (every read sees the latest write), Availability (every request gets a response), and Partition tolerance (the system keeps working despite network splits between nodes).

How we apply it: For the subscription-write path, we lean toward AP (availability + partition tolerance) — if a customer clicks “Notify Me” during a network blip between regions, we would rather accept the write locally and reconcile later than reject it outright. For the Inventory Service’s actual stock-decrement-on-purchase path (not covered in depth here, but adjacent), the platform instead leans toward CP (consistency + partition tolerance), because overselling a physical product is a much costlier mistake than a slightly delayed notification.

3.9 Exactly-once semantics vs. at-least-once

What: Kafka can be configured for “exactly-once semantics” (EOS) within a single Kafka-to-Kafka pipeline using idempotent producers and transactional writes. However, once a message leaves Kafka and calls an external HTTP API (FCM, SES, Twilio), true exactly-once delivery is no longer achievable, because the acknowledgment from that external call can itself be lost, leaving the caller unsure whether the action actually happened.

Practical example: We enable Kafka’s EOS between the Stream Processor and the notification.request topic (so a Stream Processor crash-and-restart never double-publishes the same subscriber’s notification-request), but rely on application-level idempotency keys (checked via Redis SETNX) for the final hop to external providers, since EOS cannot extend across that boundary.

3.10 Long Polling vs. WebSockets

What: Long polling is a technique where the client holds an HTTP request open until the server has new data to send, then immediately re-opens a new request. WebSockets, by contrast, establish a single persistent, full-duplex connection that both sides can push messages over at any time without re-opening anything.

Why WebSockets win here: For in-app real-time alerts to users actively browsing the site, WebSockets avoid the overhead of constantly re-establishing HTTP connections and let the WebSocket Gateway push a banner the instant a notification-request arrives — typically the fastest channel in the whole system, often delivering in well under a second.

Trade-off: WebSockets require the gateway layer to hold long-lived stateful connections (unlike the stateless request/response services elsewhere in the architecture), which is why the Connection Registry (Redis, mapping userId to the specific gateway node holding their socket) exists — it lets any backend service route a message to the correct node without needing to know which of potentially hundreds of gateway instances holds a given user’s connection.

04

Architecture & Components

Below is the full architecture, drawn as a component diagram. Every box names the actual technology or role it plays — including the API Gateway and Load Balancer explicitly, as they sit at the very front door of every request in this system.

flowchart TD A1[“Web App React”] –> B1[“CDN CloudFront Static Assets”] A2[“Mobile App iOS Android”] –> B1 A1 –> B2[“API Gateway Kong AuthN Rate Limit Routing”] A2 –> B2 B2 –> B3[“Load Balancer L7 ALB”] B3 –> C1[“Subscription Service Notify Me Handler”] C1 –> C2[“Subscription Cache Redis Cluster”] C1 –> C3[“Subscription DB Sharded PostgreSQL”] D1[“Inventory Service Stock Owner”] –> D2[“Inventory DB PostgreSQL Primary”] D2 –> D3[“CDC Connector Debezium”] D3 –> D4[“Kafka Topic stock updated”] D4 –> D5[“Stream Processor Kafka Streams”] D5 –> C2 D5 –> E2[“Kafka Topic notification request”] E2 –> E1[“Notification Orchestrator Service”] E1 –> E3[“Rate Limiter Service Token Bucket”] E3 –> E4[“Push Worker FCM APNs”] E3 –> E5[“Email Worker SES”] E3 –> E6[“SMS Worker Twilio”] E3 –> E7[“WebSocket Gateway In App Real Time”] E4 –> E8[“Dead Letter Queue Failed Jobs”] E5 –> E8 E6 –> E8 E7 –> F2[“Connection Registry Redis”] E1 –> F1[“Notification Log Store Cassandra”] C1 -.-> G1[“Metrics Prometheus Grafana”] E1 -.-> G2[“Logs ELK Stack”] D5 -.-> G3[“Tracing Jaeger”]
Figure 4.1 — End-to-end architecture: client layer, edge, subscription write path, inventory event capture, fan-out, storage, and observability.

4.1 Component-by-component breakdown

ComponentTechnology ChoiceRole
CDNCloudFront / AkamaiServes static assets (product images, JS bundles) close to the user; not on the write path, but reduces overall edge load so API Gateway capacity is reserved for real requests.
API GatewayKong / AWS API GatewaySingle entry point for all client traffic. Handles authentication (JWT validation), request validation, per-client rate limiting, and routing to the correct backend service by path (e.g., /api/subscriptions).
Load BalancerL7 Application Load Balancer / NginxDistributes traffic across many stateless instances of the Subscription Service using round-robin or least-connections, performs health checks, and terminates TLS.
Subscription ServiceJava Spring Boot microserviceHandles “Notify Me” clicks: validates the user and product, writes to the Subscription DB, and updates the Redis subscription cache.
Subscription CacheRedis ClusterFast lookup structure: productId → set of subscriber userIds. This is the hot path read during fan-out and must be extremely low latency.
Subscription DBSharded PostgreSQL (by productId hash)Durable source of truth for subscriptions, including channel preferences and timestamps, used for audits, unsubscribes, and cache rebuilds.
Inventory ServiceJava microservice owning stock stateThe single writer of truth for stock quantities; every restock, sale, and return flows through it.
CDC ConnectorDebezium on Kafka ConnectTails the Inventory DB’s write-ahead log and emits a structured “stock changed” event for every relevant row change, with zero code changes needed in Inventory Service.
Kafka (stock.updated)Apache Kafka, partitioned by productIdDurable, ordered, replayable stream of stock-change events, decoupling inventory from every downstream consumer.
Stream ProcessorKafka Streams / Apache FlinkConsumes stock events, filters for “0 → positive” transitions, looks up subscribers in the Redis cache, and emits one notification-request event per subscriber.
Notification OrchestratorJava microserviceConsumes notification-request events, applies per-user channel preference, batches, and forwards to the Rate Limiter.
Rate Limiter ServiceToken-bucket algorithm backed by RedisProtects downstream third-party providers (FCM/APNs/SES/Twilio) from being overwhelmed and from violating provider-side sending quotas.
Push / Email / SMS WorkersIndependent Java worker poolsEach channel has its own worker pool and its own Kafka topic partition group so a slow SMS provider never blocks fast push notifications.
WebSocket GatewayNetty-based WebSocket serverMaintains persistent connections for logged-in users currently browsing the site, enabling true real-time in-app “It’s back!” banners without waiting on push/email.
Dead Letter QueueKafka DLQ topicCaptures notification jobs that failed after retries for manual inspection or automated re-drive.
Notification Log StoreApache CassandraWrite-optimized store for “who was notified, when, via which channel, and what was the delivery status” — used for analytics and customer support.
Connection RegistryRedisMaps userId → WebSocket server node so the orchestrator knows which specific server instance holds a given user’s live connection.
💬
What an interviewer may ask

“Why put both an API Gateway and a Load Balancer in the diagram — isn’t that redundant?” No — they solve different problems. The API Gateway is about the request’s meaning (who is this, are they allowed, what is the shape of the request, which service should handle it). The Load Balancer is about traffic distribution across many identical instances of whichever service the Gateway routed to. In smaller systems these can be collapsed into one Nginx layer, but at the scale here you want them to scale independently — Gateway logic (auth, rate limiting) is CPU-light and network-heavy, while service instances behind the LB may be CPU-heavy.

05

Internal Working

Let’s trace exactly what happens inside the system, component by component, for the two core flows: subscribing, and the restock-triggered fan-out.

5.1 Flow A — Customer clicks “Notify Me”

  1. The client sends POST /api/v1/subscriptions with the productId and preferred channel(s).
  2. The request passes through the CDN (bypassed for POST — CDNs only cache GET/static content), hits the API Gateway which validates the JWT and applies a per-user rate limit (e.g., max 50 subscription requests/minute to stop scripted abuse).
  3. The Load Balancer routes the request to a healthy Subscription Service instance using least-connections balancing.
  4. The Subscription Service checks for an existing active subscription (to avoid duplicate rows), then writes a new row to the sharded Subscription DB (shard key = hash(productId) % numShards), and asynchronously updates the Redis set subs:{productId} with the userId.
  5. The service returns 201 Created to the client, which shows a confirmation toast.

5.2 Flow B — Product restocks and notifications fan out

  1. A warehouse management system or the Inventory Service itself updates inventory_stock.quantity from 0 to, say, 500, inside a transaction.
  2. Debezium, tailing PostgreSQL’s WAL, immediately emits a structured “row updated” event to the Kafka topic stock.updated, partitioned by productId so all events for one product stay strictly ordered.
  3. The Stream Processor (a Kafka Streams application) consumes the event. It checks: was the previous quantity 0 (or null) and is the new quantity greater than 0? If yes, this is a genuine restock transition and the processor proceeds; otherwise it’s a normal stock decrement from a sale, which we ignore for alerting purposes.
  4. The processor looks up the Redis set subs:{productId}. If it contains N subscriber userIds, it generates N individual “notification-request” messages, each tagged with a unique restockEventId (so multiple rapid restocks of the same product are distinguishable), and publishes them to the notification.request Kafka topic, partitioned by userId for even worker distribution.
  5. The Notification Orchestrator consumes these in parallel across many partitions, checks each user’s channel preferences (push, email, sms, in-app — possibly more than one), and for each chosen channel emits a task to that channel’s dedicated topic (e.g., push.tasks, email.tasks).
  6. Each channel worker pool (Push, Email, SMS) pulls from its topic, passes through the Rate Limiter (token-bucket per provider account), performs the idempotency check via Redis SETNX, and calls the external provider API (FCM/APNs, SES, Twilio).
  7. The WebSocket Gateway, for users currently connected, receives its own task type and pushes an in-app banner directly over the open socket — this is typically the fastest channel, often under 500ms end-to-end.
  8. Delivery status (sent, failed, retrying) is written asynchronously to the Cassandra Notification Log Store for analytics and customer-support lookups.
  9. Failed jobs, after a bounded number of retries with exponential backoff, are moved to the Dead Letter Queue for offline inspection or automated re-drive once the downstream provider recovers.
📌
Handling the “flicker” problem

During flash sales, stock can flicker rapidly: 0 → 3 → 0 → 10 → 0 within a few seconds as concurrent orders complete and new units get released from reserved-but-cancelled carts. If we naively fired a notification wave on every 0→positive transition, subscribers could get bombarded. The Stream Processor applies a short debounce window (e.g., 2 seconds) per productId using a small in-memory/Redis-backed state machine: it waits briefly to see if the transition “sticks,” and coalesces multiple flickers into a single notification wave referencing the latest stable quantity.

5.3 Sequence diagram — end-to-end restock notification

sequenceDiagram participant WH as Warehouse System participant INV as Inventory Service participant DB as Inventory DB participant CDC as Debezium CDC participant K1 as Kafka stock topic participant SP as Stream Processor participant R as Redis Subscription Cache participant K2 as Kafka notification topic participant ORCH as Notification Orchestrator participant PUSH as Push Worker participant USER as Customer Device WH->>INV: Restock update quantity to 500 INV->>DB: Write updated row DB–>>CDC: WAL entry captured CDC->>K1: Publish stock updated event K1->>SP: Consume event SP->>R: Lookup subscribers for productId R–>>SP: Return subscriber list SP->>K2: Publish notification request per subscriber K2->>ORCH: Consume notification requests ORCH->>PUSH: Dispatch push task PUSH->>USER: Deliver push notification
Figure 5.1 — End-to-end sequence: warehouse update to customer push, in under a couple of seconds at p99.
💬
What an interviewer may ask

“How do you keep the Kafka events for one product in order?” By partitioning the topic on productId (or a hash of it). Kafka guarantees ordering only within a partition, so as long as all events for a given product always land on the same partition, the Stream Processor sees stock transitions for that product in the exact order they happened, which is essential for correctly detecting 0→positive transitions.

06

Data Flow & Lifecycle

A subscription record moves through a well-defined lifecycle:

  1. Created — user clicks Notify Me; row inserted with status ACTIVE, timestamp, channel preferences, and a default expiry (e.g., createdAt + 90 days).
  2. Cached — mirrored into Redis for O(1) lookup during fan-out; cache and DB are eventually consistent, with the DB as source of truth and periodic reconciliation jobs rebuilding the cache from the DB to heal drift.
  3. Triggered — a restock event matches this subscription; a notification-request is generated referencing this subscription’s id and the specific restockEventId.
  4. Notified — status transitions to NOTIFIED (or, if multi-channel, per-channel sub-status), and the subscription can optionally be marked “consumed” so the same restock doesn’t re-trigger it if flicker occurs after debounce.
  5. Expired / Unsubscribed — after the TTL elapses, or on explicit unsubscribe, the record is soft-deleted (status EXPIRED) and later purged by a background job, and it’s simultaneously removed from the Redis set.

6.1 Subscription state machine

stateDiagram-v2 [*] –> Created Created –> Cached: mirror into Redis Cached –> Triggered: matching restock event fires Triggered –> Notified: delivery attempted Notified –> Expired: TTL elapses or unsubscribe Cached –> Expired: TTL elapses without trigger Expired –> [*]
Figure 6.1 — Subscription lifecycle as a state machine.

A useful design decision here: should a subscription auto-expire after being triggered once, or can a user resubscribe to be notified on every future restock? Most platforms (Amazon included) treat a triggered notification as terminal — if the user still wants alerts for future restocks, they must click “Notify Me” again. This keeps subscriber lists naturally self-pruning and avoids permanently loud subscriber lists ballooning Redis memory forever.

💬
What an interviewer may ask

“What happens if Redis loses the subscription cache entirely?” This should be a non-event operationally: Redis here is a derived cache, never the source of truth. A cache-warming job can rebuild subs:{productId} sets from the sharded PostgreSQL Subscription DB in bulk, and in the interim the Stream Processor can fall back to querying PostgreSQL directly (slower, but correct) until the cache is warm again.

07

Advantages, Disadvantages & Trade-offs

AspectAdvantageTrade-off / Cost
CDC-based event captureZero missed events; inventory team never has to remember to publish anythingAdds operational complexity (Debezium/Kafka Connect cluster to run and monitor)
Redis subscription cacheSub-millisecond subscriber lookups during high-fan-out restocksExtra consistency-management burden between cache and durable DB
Multi-channel, per-channel topicsA slow SMS provider never blocks fast push/in-app deliveryMore Kafka topics and worker pools to operate and scale independently
Debounce window on stock flickerPrevents notification spam during flash-sale stock churnAdds a small, deliberate delay (1–2s) to the fastest-case notification latency
Eventual consistency modelMassive scalability; no cross-service distributed transactions neededA tiny window exists where a notified user could still miss out if stock sells out within seconds of the alert (acceptable, but must be communicated in UX)

7.1 On the tension between speed and fairness

It’s worth dwelling on that last row, because it’s a genuinely important product-and-engineering conversation, not just a technical footnote. Any asynchronous, at-scale notification pipeline necessarily introduces some delay between “stock became available” and “customer sees the alert and acts on it.” For a product with 50 units and 5,000 subscribers, it is mathematically certain that most subscribers will be notified after the item is already gone, no matter how fast the pipeline is. The right engineering response isn’t to chase impossible zero-latency guarantees; it’s to be transparent in the product experience — showing accurate, live stock counts on the landing page the notification links to, and, for genuinely scarce high-demand items, layering a fair queueing or raffle mechanism (as Nike’s SNKRS app does) on top of the notification system rather than treating “who got notified first” as a de facto purchase-priority mechanism. This is a good example of a case where the best system design answer is not purely technical — it requires recognizing the limits of what any architecture can promise and designing the surrounding product experience to match reality honestly.

08

Performance & Scalability

The requirement is to comfortably support over one million requests per minute across the system — roughly 16,700 requests/second sustained, with flash-sale bursts reaching 5–10x that (up to ~150,000–170,000 requests/second momentarily). Here’s how each layer scales to meet that.

8.1 API Gateway & Load Balancer tier

Stateless by design, this tier scales horizontally behind an auto-scaling group. At 170k req/s peak, with each gateway node comfortably handling 5,000–10,000 req/s, roughly 20–35 nodes cover peak load, scaled up proactively ahead of known sale events and reactively via CPU/connection-count-based auto-scaling policies otherwise.

8.2 Kafka tier

The stock.updated topic is partitioned (e.g., 128–256 partitions) so that stream processing parallelism scales linearly with partition count. The notification.request topic needs far more partitions (e.g., 512–1024) since fan-out multiplies event volume by average subscribers-per-product, which can be in the tens of thousands during a hot restock. Kafka’s log-based design means write throughput scales near-linearly with broker count and disk throughput, and consumer groups scale by simply adding more consumer instances up to the partition count.

8.3 Redis subscription cache

Deployed as a Redis Cluster with hash-slot sharding across many nodes. A single hot product (a viral restock) can create a “hot key” problem — one Redis slot receiving disproportionate read traffic. We mitigate this by (a) reading the subscriber set once per restock event and caching it briefly in the Stream Processor’s local memory rather than re-reading per-subscriber, and (b) for exceptionally hot products, splitting the subscriber set across multiple sharded keys (e.g., subs:{productId}:{bucket}) so reads parallelize across Redis nodes.

8.4 Fan-out and worker scaling

Worker pools (Push/Email/SMS) auto-scale based on Kafka consumer lag — if the lag on push.tasks grows, Kubernetes’ Horizontal Pod Autoscaler spins up more worker pods, up to the partition count ceiling. Third-party provider rate limits (e.g., FCM allows very high throughput, but SES/Twilio have stricter per-account limits) are respected via the token-bucket Rate Limiter, which queues excess load rather than dropping it, smoothing bursts over a few seconds to a couple of minutes rather than instantaneously blasting a provider.

8.5 Database scaling

The Subscription DB is sharded by productId hash across many PostgreSQL instances (or migrated to a horizontally-scalable store like DynamoDB/Cassandra if write volume grows further), so no single database instance bears the full write load of “Notify Me” clicks nationwide. The Notification Log Store uses Cassandra specifically because its write-optimized, leaderless architecture handles the enormous write volume of “log every notification attempt” far better than a relational database would at this scale.

8.6 The token bucket algorithm, explained

The Rate Limiter’s core algorithm deserves a closer look since it’s a frequently-asked interview topic in its own right. A token bucket is a simple, elegant data structure: imagine a bucket that holds up to $N$ tokens (say, 1,000). Every time we want to send a notification through a given provider, we must first remove one token from the bucket. Tokens are refilled at a steady rate (say, 500 tokens/second) regardless of how many are being consumed. If the bucket is empty when a request arrives, that request waits (or is queued) until a new token becomes available.

Why this shape fits our problem: it naturally absorbs short bursts (the bucket has capacity to send a quick burst up to $N$ tokens instantly) while enforcing a hard steady-state ceiling (the refill rate) that matches exactly what the downstream provider (SES, Twilio) allows contractually. This is superior to a naive fixed-window counter (e.g., “max 30,000 per minute”), which allows a full window’s worth of traffic to burst right at a window boundary, doubling the effective peak rate for a brief moment.

TokenBucketRateLimiter.java — smoothing provider bursts
public class TokenBucketRateLimiter {

    private final long capacity;
    private final long refillTokensPerSecond;
    private final AtomicLong availableTokens;
    private volatile long lastRefillTimestamp;

    public TokenBucketRateLimiter(long capacity, long refillTokensPerSecond) {
        this.capacity = capacity;
        this.refillTokensPerSecond = refillTokensPerSecond;
        this.availableTokens = new AtomicLong(capacity);
        this.lastRefillTimestamp = System.nanoTime();
    }

    public boolean tryAcquire() {
        refill();
        long current;
        do {
            current = availableTokens.get();
            if (current <= 0) {
                return false; // no tokens left, caller should queue and retry
            }
        } while (!availableTokens.compareAndSet(current, current - 1));
        return true;
    }

    private void refill() {
        long now = System.nanoTime();
        long elapsedNanos = now - lastRefillTimestamp;
        long tokensToAdd = (elapsedNanos * refillTokensPerSecond) / 1_000_000_000L;

        if (tokensToAdd > 0) {
            long newValue = Math.min(capacity, availableTokens.get() + tokensToAdd);
            availableTokens.set(newValue);
            lastRefillTimestamp = now;
        }
    }
}

In production, this logic runs against a shared Redis instance (using a Lua script for atomicity) rather than local JVM memory, since the Rate Limiter Service runs as many replicated instances and they must all agree on one shared token count per provider account.

💬
What an interviewer may ask

“A single viral product restocks with 2 million subscribers. Walk me through how you avoid a system meltdown.” Key points: (1) the Stream Processor doesn’t loop and call a service per subscriber — it reads the full subscriber set once and streams individual notification-request events into a highly-partitioned topic, so fan-out is a pure Kafka write, not 2 million synchronous service calls; (2) downstream worker pools scale horizontally to drain that topic over seconds to low minutes rather than instantly, which is an acceptable trade-off since not every user needs to be notified in the exact same millisecond; (3) the Rate Limiter smooths bursts against provider quotas so SES/Twilio never see an instantaneous 2-million-message spike.

09

High Availability & Reliability

  • Multi-AZ deployment: every stateful component (Kafka brokers, Redis Cluster, PostgreSQL, Cassandra) is deployed across at least three availability zones, with Kafka replication factor 3 and Cassandra replication factor 3 using a quorum consistency level for reads/writes on critical paths.
  • No single point of failure at the edge: API Gateway and Load Balancer tiers run as auto-scaled fleets with health checks; unhealthy nodes are automatically removed from rotation.
  • Circuit breakers: calls from the Notification Orchestrator (and channel workers) to external providers (FCM, SES, Twilio) are wrapped in circuit breakers (e.g., Resilience4j in Java) so that if a provider starts failing, we stop hammering it and instead fail fast, queueing jobs for later retry rather than piling up threads waiting on a dying dependency.
  • Dead Letter Queues: any notification job that exhausts its retry budget lands in a DLQ topic rather than being silently dropped, enabling manual or automated re-drive once the root cause is fixed.
  • Idempotent consumers: since Kafka delivers at-least-once, every consumer (Stream Processor, Orchestrator, channel workers) is written to be safely re-runnable on the same message without duplicating customer-visible side effects.
  • Graceful degradation: if Redis subscription cache is fully down, the Stream Processor falls back to a (slower) direct PostgreSQL read path rather than failing the entire restock-notification flow.
  • Disaster recovery: Kafka topics have cross-region mirroring (MirrorMaker 2) to a secondary region; PostgreSQL uses continuous WAL archiving and point-in-time-recovery backups; game-day drills simulate full primary-region failure to validate the runbook.
💬
What an interviewer may ask

“What’s your RPO/RTO for this system if the primary region goes down?” A realistic answer: subscription data (durable, low write-rate relative to overall traffic) can target an RPO of a few seconds via synchronous-ish cross-AZ replication and cross-region async replication, with an RTO of a few minutes for full regional failover. Notifications in-flight during the outage are treated as best-effort — some may be delayed rather than lost, since the underlying stock event remains durably stored in Kafka and can be reprocessed once service resumes.

9.1 Consensus & leader election underneath the platform

Several components in this architecture quietly rely on consensus algorithms to stay coordinated, and understanding this is often what separates a candidate who has “used Kafka” from one who understands it. Modern Kafka clusters use KRaft (a Raft-based consensus protocol built into Kafka itself, replacing the older ZooKeeper dependency) to elect a controller node responsible for partition leader assignment and cluster metadata. Similarly, PostgreSQL high-availability setups typically use tools like Patroni, which itself relies on a consensus store (etcd, which uses Raft) to safely elect a single primary and avoid two nodes believing they’re both the primary at once — a dangerous “split-brain” scenario that could cause the Subscription DB to accept conflicting writes on different nodes simultaneously.

Why this matters for stock alerts specifically: if a Kafka partition leader fails mid-fan-out for a viral restock, Raft-based leader election typically completes within a few hundred milliseconds to a couple of seconds, during which writes to that specific partition briefly pause. Designing partition counts and replication factors thoughtfully (replication factor 3, min in-sync replicas of 2) ensures the system tolerates a single broker failure without losing any already-acknowledged stock-change events, and resumes fan-out automatically once a new leader is elected — no human intervention required for the common case of a single-node failure.

9.2 Failure recovery scenarios worth rehearsing

  • Debezium/CDC connector crashes: Kafka Connect automatically restarts the connector task on another worker node, resuming from the last committed WAL offset, so no stock-change events are lost — only delayed by the restart time (typically a few seconds).
  • A single channel worker pool (e.g., SMS) fails entirely: because channels are isolated via the bulkhead pattern, push and email notifications continue unaffected; SMS tasks simply accumulate in their Kafka topic until the pool recovers, then drain normally (Kafka’s retention window, typically 3–7 days, comfortably covers any realistic outage).
  • Redis subscription cache node failure: Redis Cluster automatically promotes a replica to primary for the affected hash slots within seconds; any requests during that brief window fail over to the PostgreSQL fallback path described earlier.
💬
What an interviewer may ask

“Kafka replaced ZooKeeper with KRaft — why does that matter for a system like this?” Removing the separate ZooKeeper cluster reduces the number of independently-failing distributed systems the platform must operate, simplifies operational runbooks, and lowers end-to-end metadata-change latency (like partition leader failover), which directly improves how quickly the fan-out pipeline recovers from a broker failure during a high-traffic restock event.

10

Security

  • Authentication & Authorization: the API Gateway validates JWTs (short-lived access tokens plus refresh tokens) before any request reaches the Subscription Service; guest email-based subscriptions require email verification (double opt-in) to prevent someone subscribing another person’s email address without consent.
  • Rate limiting & abuse prevention: per-user and per-IP rate limits at the Gateway prevent scripted mass-subscription abuse (e.g., a bot subscribing millions of fake emails to a product to inflate perceived demand or to spam a competitor’s inbox).
  • PII protection: emails and phone numbers used for notification are encrypted at rest (column-level encryption in PostgreSQL / Cassandra) and access to raw contact info is scoped to the specific services that need it (Email/SMS workers), not broadly readable across the platform.
  • Provider credential isolation: API keys for FCM/APNs/SES/Twilio are stored in a secrets manager (AWS Secrets Manager / HashiCorp Vault), never in code or config files, and rotated on a schedule.
  • Unsubscribe compliance: every email includes a one-click unsubscribe link (CAN-SPAM/GDPR requirement); SMS includes “reply STOP to opt out,” both wired back into the Subscription Service to immediately deactivate the relevant record.
  • mTLS between internal services: a service mesh (Istio) enforces mutual TLS between the Orchestrator, workers, and databases, so traffic within the cluster is encrypted and mutually authenticated, limiting blast radius if one pod is compromised.
  • Input validation: productId and userId inputs are strictly validated to prevent injection attacks against the Subscription DB, and all queries use parameterized statements.
💬
What an interviewer may ask

“How would you prevent someone from subscribing thousands of other people’s email addresses without consent?” Double opt-in: the first “Notify Me” click for an unauthenticated email sends a confirmation email requiring a click before the subscription becomes ACTIVE. Combined with rate limiting on both the submitting IP and the target email address, this makes bulk-spam subscription abuse impractical.

11

Monitoring, Logging & Metrics

  • Metrics (Prometheus + Grafana): subscription write rate, Kafka consumer lag per topic/partition, end-to-end notification latency (p50/p95/p99) from restock event to delivery confirmation, per-channel delivery success/failure rate, Redis hit/miss ratio on the subscription cache, and rate-limiter queue depth per provider.
  • Logging (ELK / EFK stack): structured JSON logs from every service, correlated by a shared restockEventId and traceId, so an engineer can search “all logs for this restock event” across every microservice in one query.
  • Distributed tracing (Jaeger/Zipkin): a single trace spans from the Debezium event ingestion through the Stream Processor, Orchestrator, Rate Limiter, and channel worker, making it possible to see exactly where time is spent for a slow notification.
  • Alerting: PagerDuty/OpsGenie alerts fire on: Kafka consumer lag exceeding a threshold (signals the pipeline is falling behind), notification p99 latency breaching SLA, DLQ growth rate spiking, and third-party provider error-rate spikes (early warning of an FCM/SES/Twilio outage).
  • Business dashboards: beyond pure ops metrics, product teams track “notify-to-purchase conversion rate” — of everyone notified, what fraction actually completed a purchase within, say, 1 hour — which is a strong signal of whether the notification speed and channel mix are effective.
💬
What an interviewer may ask

“What single metric would you put on a wall-mounted dashboard for on-call engineers?” Kafka consumer lag on the notification.request topic is a strong pick — a rising lag is the earliest, most reliable signal that the fan-out pipeline can’t keep up with demand, well before customers start complaining about missing notifications.

12

Deployment & Cloud

All stateless services (Subscription Service, Stream Processor, Notification Orchestrator, channel workers, WebSocket Gateway) run as containerized workloads on Kubernetes, packaged via Docker images built and pushed through a CI/CD pipeline (GitHub Actions or Jenkins → container registry → ArgoCD for GitOps-style deployment).

  • Auto-scaling: Horizontal Pod Autoscalers scale channel workers based on Kafka consumer lag (via KEDA — Kubernetes Event-Driven Autoscaling) rather than plain CPU, since these workloads are I/O-bound waiting on third-party providers.
  • Canary and blue-green deployments: new versions of the Stream Processor or Orchestrator are rolled out to a small percentage of traffic first, with automated rollback if error rates or latency regress, since a bug in restock-matching logic could otherwise silently under- or over-notify millions of people.
  • Multi-region: the system is deployed across at least two AWS regions for global e-commerce brands, with Kafka MirrorMaker 2 replicating topics cross-region, and geo-aware routing at the CDN/DNS layer (Route 53 latency-based routing) directing users to their nearest region.
  • Infrastructure as Code: the entire stack (Kafka cluster, Redis Cluster, Kubernetes node pools, IAM policies) is defined in Terraform, enabling reproducible environments and disaster-recovery region rebuilds.
  • Cost optimization: Kafka broker and Kubernetes worker node pools use a mix of reserved instances for baseline load and spot/preemptible instances for stateless worker pools that can tolerate interruption (with jobs safely re-queued via Kafka’s consumer-group rebalancing).

Operational maturity for a system like this is measured less by how it behaves on an ordinary Tuesday and more by how gracefully it handles the extraordinary Friday — a viral product drop, a scheduled flash sale, or an unplanned regional outage. This is why capacity planning for this platform is done against a modeled worst-case scenario (a single SKU restocking with several million active subscribers, timed to coincide with a broader site-wide traffic spike) rather than simply extrapolating from average daily load, and why the deployment pipeline treats load and chaos testing as a required, non-optional gate before any major sale event, not an occasional nice-to-have exercise.

13

Databases, Caching & Load Balancing

13.1 Why sharded PostgreSQL for subscriptions

Subscriptions are structured, relational-shaped data (userId, productId, channel, timestamps, status) that benefit from strong schema guarantees and ACID transactions on individual writes. Sharding by hash(productId) spreads both write load and, more importantly, the read load during fan-out reconciliation across many independent database instances, avoiding a single hot primary.

13.2 Why Cassandra for the notification log

The notification log is an append-heavy, rarely-updated dataset (billions of rows over time: one row per notification attempt per user per channel) queried mostly by simple keys (userId, or notificationId) rather than complex joins. Cassandra’s leaderless, masterless architecture handles enormous write throughput with linear horizontal scalability, which is exactly the shape of this workload — unlike PostgreSQL, which would require increasingly complex sharding to sustain the same write rate.

13.3 Why Redis for the subscription cache and connection registry

Both use cases need sub-millisecond lookups on simple key structures (sets and hash maps) under very high read concurrency during fan-out bursts — exactly Redis’s sweet spot. Redis Cluster’s hash-slot sharding lets us scale horizontally as subscriber-set volume grows.

13.4 Load balancing strategy

The L7 Load Balancer uses least-connections balancing for the Subscription Service (since request processing time can vary slightly based on DB shard latency) and the Kafka partition assignment itself acts as the “load balancer” for the Stream Processor and Orchestrator tiers — each consumer instance is assigned a subset of partitions, which is a natural, built-in load-balancing mechanism for stream-processing workloads.

💬
What an interviewer may ask

“Why not just use one database for everything?” Polyglot persistence — using the right database for each access pattern — is a deliberate trade-off of added operational complexity (more systems to run and monitor) in exchange for each workload getting a data store that’s actually good at its specific read/write shape. A single relational database sized to handle both ACID subscription writes and billions of append-only notification-log rows would either be wildly over-provisioned for one workload or under-provisioned for the other.

14

APIs & Microservices

Below is a representative Java implementation sketch of the Subscription Service’s core endpoint, using Spring Boot, showing the write-through cache pattern discussed above.

SubscriptionController.java — write-through subscription creation
@RestController
@RequestMapping("/api/v1/subscriptions")
public class SubscriptionController {

    private final SubscriptionRepository subscriptionRepository;
    private final RedisTemplate<String, String> redisTemplate;

    public SubscriptionController(SubscriptionRepository subscriptionRepository,
                                   RedisTemplate<String, String> redisTemplate) {
        this.subscriptionRepository = subscriptionRepository;
        this.redisTemplate = redisTemplate;
    }

    @PostMapping
    public ResponseEntity<SubscriptionResponse> createSubscription(
            @RequestHeader("X-User-Id") String userId,
            @Valid @RequestBody SubscriptionRequest request) {

        // Prevent duplicate active subscriptions for the same product
        boolean alreadyExists = subscriptionRepository
                .existsActiveByUserAndProduct(userId, request.getProductId());

        if (alreadyExists) {
            return ResponseEntity.status(HttpStatus.CONFLICT)
                    .body(new SubscriptionResponse("Already subscribed", null));
        }

        Subscription subscription = new Subscription(
                UUID.randomUUID().toString(),
                userId,
                request.getProductId(),
                request.getChannels(),
                SubscriptionStatus.ACTIVE,
                Instant.now(),
                Instant.now().plus(90, ChronoUnit.DAYS)
        );

        subscriptionRepository.save(subscription);

        // Write-through: update the fast Redis lookup used during restock fan-out
        String cacheKey = "subs:" + request.getProductId();
        redisTemplate.opsForSet().add(cacheKey, userId);
        redisTemplate.expire(cacheKey, Duration.ofDays(95));

        return ResponseEntity.status(HttpStatus.CREATED)
                .body(new SubscriptionResponse("Subscribed successfully", subscription.getId()));
    }
}

And here is the Kafka Streams logic (Java) that detects genuine restock transitions and fans out notification-request events:

RestockDetectionProcessor.java — 0-to-positive transition fan-out
public class RestockDetectionProcessor implements Processor<String, StockChangeEvent, String, NotificationRequest> {

    private ProcessorContext<String, NotificationRequest> context;
    private final RedisSubscriptionLookup subscriptionLookup;

    public RestockDetectionProcessor(RedisSubscriptionLookup subscriptionLookup) {
        this.subscriptionLookup = subscriptionLookup;
    }

    @Override
    public void init(ProcessorContext<String, NotificationRequest> context) {
        this.context = context;
    }

    @Override
    public void process(Record<String, StockChangeEvent> record) {
        StockChangeEvent event = record.value();

        boolean isRestockTransition = event.getPreviousQuantity() <= 0
                && event.getNewQuantity() > 0;

        if (!isRestockTransition) {
            return; // ignore normal sale decrements and other unrelated updates
        }

        String productId = event.getProductId();
        String restockEventId = UUID.randomUUID().toString();

        Set<String> subscriberIds = subscriptionLookup.getSubscribers(productId);

        for (String userId : subscriberIds) {
            NotificationRequest notification = new NotificationRequest(
                    userId, productId, restockEventId, Instant.now());

            context.forward(new Record<>(userId, notification, context.currentSystemTimeMs()));
        }
    }
}
💬
What an interviewer may ask

“Why use the Kafka Processor API here instead of the higher-level Streams DSL (map/filter)?” The Processor API gives explicit control over one-to-many forwarding (one input record producing N output records for N subscribers), which the simpler DSL’s map/filter (one-to-one) doesn’t directly support as cleanly; flatMap in the DSL is the higher-level equivalent and would also be a valid answer.

15

Design Patterns & Anti-Patterns

15.1 Patterns used

Pattern

Change Data Capture

Decouples the source-of-truth database from every downstream consumer without requiring dual writes.

Pattern

Event Sourcing (partial)

The stock.updated Kafka topic acts as a replayable log of all stock transitions, useful for rebuilding derived state or debugging historical incidents.

Pattern

Fan-out / Fan-in via Queue

One restock event expands into many notification tasks, processed independently and in parallel across worker pools.

Pattern

Circuit Breaker

Protects the system from cascading failure when a third-party notification provider (FCM, SES, Twilio) degrades.

Pattern

Idempotent Consumer

Guards against Kafka’s at-least-once delivery causing duplicate customer-visible notifications.

Pattern

Bulkhead

Isolating channel worker pools (push/email/sms) so failure or slowness in one channel cannot exhaust thread pools or resources shared by another.

15.2 Anti-patterns to avoid

Anti-pattern

Dual writes without CDC

Having the Inventory Service directly publish an event AND write to the DB in the same request, without a transactional outbox or CDC — this can silently drop events if the publish step fails after the DB commit succeeds (or vice versa).

Anti-pattern

Synchronous fan-out

Looping over subscribers and calling the notification API synchronously within the request/event-handling thread — this doesn’t scale past a few dozen subscribers and ties up processing threads for seconds during a hot restock.

Anti-pattern

Polling as primary mechanism

As discussed in the Problem section, this wastes enormous read capacity and adds unnecessary latency at scale.

Anti-pattern

Unbounded subscriber lists

Never expiring subscriptions leads to Redis sets and DB tables growing forever, eventually causing memory pressure and slower fan-out lookups on old, “cold” products nobody still cares about.

Anti-pattern

No debounce on flicker

Naively notifying on every 0→positive transition during flash-sale stock churn results in duplicate, spammy notifications that erode customer trust.

16

Best Practices & Common Mistakes

Best PracticeCommon Mistake It Prevents
Partition Kafka topics by productId/userId for ordering and parallelismUsing a single partition “to keep things simple,” which serializes all processing and destroys throughput
Idempotency keys on every notification jobAssuming Kafka’s at-least-once delivery means “exactly once,” leading to duplicate customer-visible alerts
Separate topics/worker pools per channelOne shared queue for all channels, where a slow SMS provider backs up and delays fast push notifications
Debounce restock detection during flickerNotifying on every micro-transition, spamming users during flash sales
Treat Redis subscription cache as derived, rebuildable stateTreating Redis as the source of truth, risking permanent data loss if it’s flushed
Rate-limit outbound calls to third-party providersGetting the platform’s SES/Twilio account throttled or suspended during a viral restock
Auto-expire stale subscriptionsUnbounded growth of subscriber lists degrading fan-out performance over years of operation
📌
The common thread

A theme runs through nearly every mistake in this table: they are all forms of premature simplification — decisions that look harmless at low scale (a single Kafka partition, a shared notification queue, no debounce logic) but silently accumulate into serious production incidents once traffic crosses a threshold. The most experienced engineers on a system like this tend to build in the scaling seams (partitioning, channel isolation, expiry policies) from day one, even while running at a fraction of eventual scale, precisely because retrofitting these seams into a live, traffic-bearing system later is far riskier and more expensive than designing them in up front.

17

Real-World / Industry Examples

Marketplace

Amazon

Uses a similar “Notify Me” pattern across its catalog, integrating restock alerts with its recommendation and advertising systems so that a restock event can also trigger a marketing email highlighting related products, not just the specific SKU.

Sneaker Drops

Nike SNKRS App

A well-known example of extreme fan-out at a moment in time: limited sneaker “drops” notify millions of subscribed users simultaneously, and the app is explicitly engineered around fairness (randomized queueing/raffle mechanics) precisely because naive first-come-first-served notification delivery would otherwise systematically favor users with lower network latency — an equity concern this tutorial’s architecture addresses by decoupling “notified” from “guaranteed purchase.”

Flash Sales

Flipkart / South Asian Marketplaces

Handle “Big Billion Day”-style flash sales with exactly the kind of stock-flicker patterns discussed above, using Kafka-based event pipelines with dedicated debounce and reconciliation logic for restock notifications during these events.

Food Delivery

Uber Eats / DoorDash

Not e-commerce stock alerts per se, but they use structurally identical architecture (CDC from a restaurant-availability database, Kafka fan-out, multi-channel push) for “this restaurant/item is available again” notifications, validating that this pattern generalizes well beyond pure retail.

Retail

Walmart

Has publicly discussed moving significant parts of its inventory and fulfillment platform to event-driven architectures built on Kafka specifically to reduce the latency between a physical stock change (received at a store or fulfillment center) and that change being reflected across the website, mobile app, and any subscriber-facing alerting logic layered on top of it.

Electronics

BestBuy

“Add to Cart” and “Notify Me” flows are explicitly separated in the UX: a customer can choose immediate cart-reservation for high-demand items still technically in stock, versus a pure notification subscription for genuinely out-of-stock items — a UX distinction that maps cleanly onto the architectural distinction between the strongly-consistent checkout path and the eventually-consistent notification path.

📌
The recurring theme

Across all of these examples, a consistent theme emerges: the companies that scaled restock-alert systems successfully all eventually converged on the same core shape — CDC or an equivalent explicit event-publishing step at the source of truth, a durable and replayable event log (almost always Kafka or a close equivalent), a fast subscriber-matching cache layer, and channel-isolated, rate-limited fan-out workers. This convergence is a strong signal, in an interview setting, that this architecture isn’t just theoretically sound but battle-tested at genuine internet scale.

18

Frequently Asked Questions

Q1

Why use Kafka instead of a simpler message queue like RabbitMQ or SQS?

Kafka’s partitioned, replayable log model is a strong fit here because we need both high-throughput fan-out AND the ability to replay stock-change history (for debugging, reconciliation, or rebuilding caches). RabbitMQ/SQS are perfectly valid for simpler queueing needs, but Kafka’s retention and partition-ordering guarantees map more naturally onto “ordered stock events per product” and “massively parallel consumer groups,” which is why it’s the industry-standard choice for this class of problem.

Q2

How do you handle a user subscribed via both push and email — do they get both?

Yes, by design, based on their explicit channel preferences captured at subscribe time; the Notification Orchestrator fans out one task per selected channel per user, and each channel is tracked and rate-limited independently.

Q3

What happens if a product restocks and sells out again before all notifications are sent?

This is an accepted trade-off of the eventual-consistency, asynchronous fan-out model. The product page itself always reflects live inventory (read directly from the Inventory Service, not from the notification pipeline), so a user who clicks through from a notification but finds it sold out again sees an accurate, real-time page — a slightly disappointing but honest experience, versus showing stale “in stock” information.

Q4

How would you test this system before a major flash sale?

Load-test the full pipeline with synthetic restock events at 5–10x expected peak volume, chaos-test by killing Kafka brokers/Redis nodes mid-fan-out to validate failover, and run a “game day” simulating a viral product with millions of subscribers to validate rate limiting and provider quota handling under realistic third-party latency and error conditions.

Q5

How do you decide how many Kafka partitions to allocate to each topic?

Partition count is chosen based on the target consumer parallelism and expected peak throughput, not an arbitrary round number. A rough formula: desired peak throughput ÷ throughput a single partition/consumer can sustain = minimum partitions needed, then round up and add headroom for future growth, since increasing partitions later is possible but can disrupt existing key-to-partition ordering guarantees. For notification.request, we size for the worst realistic single-restock fan-out (millions of subscribers draining within a couple of minutes), which is why it needs an order of magnitude more partitions than stock.updated.

Q6

What if a customer wants to be notified the moment stock crosses a specific threshold, not just “any positive quantity”?

This is a natural extension: instead of a simple 0→positive check, the Stream Processor’s condition becomes configurable per-subscription (e.g., “notify me when quantity ≥ 10,” useful for bulk buyers). The subscription record simply carries an optional threshold field, and the restock-detection logic reads it when evaluating each stock-change event, at negligible additional cost.

Q7

How do you avoid notifying a customer about a restock for a variant they don’t actually want (e.g., wrong size or color)?

The subscription is keyed at the SKU (specific variant) level, not the parent product level. The “Notify Me” button on a product page is contextual to the exact size/color combination currently selected, so the Subscription DB and Redis cache both use SKU-level identifiers as their productId, ensuring a restock of size 9 doesn’t spuriously notify someone waiting on size 11.

Q8

Could this same architecture support “price drop” alerts instead of stock alerts?

Yes, with minimal changes — the CDC source becomes the pricing table instead of (or in addition to) the inventory table, and the Stream Processor’s transition condition changes from “0 → positive quantity” to “new price < subscribed target price.” Everything downstream (subscription cache, fan-out, multi-channel dispatch, rate limiting, idempotency) is identical, which is a strong argument for designing the fan-out and notification-dispatch layers as a reusable, event-agnostic platform rather than something hard-coded to stock alerts specifically.

Q9

How would you measure whether this system is actually “worth it” from a business perspective?

Beyond pure engineering health metrics, the strongest business signal is notify-to-purchase conversion rate combined with incremental revenue attributable to recovered sales that would otherwise have gone to a competitor while the item was out of stock. Comparing this against the infrastructure cost of running the Kafka cluster, Redis Cluster, and worker fleets gives a clear return-on-investment picture, and is usually the number that justifies continued investment in lowering end-to-end notification latency even further.

19

Summary & Key Takeaways

📌
Key takeaways
  • A real-time stock alert system is fundamentally an event-driven fan-out problem: capture inventory changes the instant they happen (via CDC), match them against subscriber lists (via a fast cache), and deliver through multiple independent channels (push, email, SMS, in-app).
  • CDC plus Kafka decouples inventory ownership from notification concerns entirely, removing the risk of “forgotten” event publishes and enabling independent scaling of every stage.
  • Redis subscription caches provide the sub-millisecond lookups fan-out needs, while durable relational/wide-column stores (PostgreSQL, Cassandra) remain the source of truth and audit trail.
  • Idempotency keys, per-channel isolation, rate limiting, and debounce logic are what separate a toy notification system from one that survives a real flash sale without spamming or crashing.
  • At the scale of 1M+ requests/minute, every tier — Gateway, Load Balancer, Kafka, Redis, worker pools, databases — must scale horizontally and independently, guided by the natural parallelism unit of each technology (partitions for Kafka, hash slots for Redis, shards for PostgreSQL).
  • Reliability comes from designing for failure at every layer: circuit breakers, dead letter queues, multi-AZ/multi-region replication, and graceful degradation when any single dependency (like the Redis cache) becomes unavailable.

19.1 The one idea to remember

The essence of this system is a small, disciplined transformation: an inventory row changing from 0 to a positive number becomes, within seconds, a personal alert on the phone of every customer who cared enough to raise their hand. Everything in the architecture — CDC to guarantee no missed events, Kafka to buffer bursts and preserve order, Redis to answer “who cares?” in microseconds, a rate-limiter to protect the downstream providers that actually reach a human, and a bulkhead per channel so one slow provider doesn’t drag the others down — exists in service of making that transformation happen at a scale, speed, and reliability that a simple polling loop or nightly batch job simply cannot match.