Designing a Real-Time Cross-Sell & Upsell Recommendation Engine at Checkout
How do you show a personalised “you may also like” or “upgrade this order” widget on the checkout page of an e-commerce platform that handles a million requests a minute, without adding even 50 milliseconds of extra latency to the purchase flow? This deep-dive builds the system from first principles — architecture, data flow, scaling, failure handling, and the exact trade-offs an interviewer expects you to reason through out loud.
Introduction & History
Every big e-commerce checkout page you have ever used almost certainly showed you something extra right before you paid: “Add a phone case for $12,” “Upgrade to 2-day shipping,” “Customers also bought this charger.” That little box is not decoration — it is one of the highest-leverage pieces of real estate in all of e-commerce, because it appears at the single moment when the shopper has already decided to buy something and has their wallet, quite literally, already open.
The idea itself is old. Physical retail stores have used “checkout aisle impulse buys” — candy, batteries, magazines — for over a century, because store owners noticed that people standing in a checkout line with their guard down bought things they had not planned to buy. E-commerce simply automated and personalised that same psychological moment. Amazon popularised “Customers who bought this item also bought” recommendations in the late 1990s, largely on the product page. Over the 2000s and 2010s, as companies like Amazon, Netflix, and Alibaba refined recommendation science, they realised that the checkout page specifically was a distinct surface with its own constraints: extremely low tolerance for latency, extremely low tolerance for irrelevant suggestions (since a bad suggestion at checkout can make a user abandon the cart entirely), and an extremely tight technical budget because checkout is already the most performance-sensitive page on the entire site.
This tutorial designs that exact system: a cross-sell and upsell recommendation service that renders inside the checkout flow, at the scale of roughly a million requests per minute (which is about 16,600 requests per second sustained, with bursts far higher during flash sales), while adding no meaningful latency to the actual purchase.
Throughout this tutorial, every architecture diagram box is explicitly named after the real component it represents — API Gateway, Load Balancer, Checkout Service, Recommendation Orchestrator, Feature Store Cache, Candidate Generation, Ranking, Rules Engine, and the asynchronous streaming and offline training layer — so that the diagrams double as a vocabulary you can reuse directly in a live interview whiteboard session, not just an illustration to look at once and forget.
“Why can’t we just reuse the product-page recommendation engine for checkout?” — product-page recommendations tolerate 200-500 ms because the user is browsing. Checkout recommendations must not add to the critical path of payment processing, which typically has an end-to-end SLA under one to two seconds including payment gateway round trips. Any additional service call in that path must be near-zero latency, fully asynchronous, or pre-computed ahead of time.
1.1 Checkout as One Surface Among Many Recommendation Surfaces
Most e-commerce platforms run recommendation logic on at least five distinct surfaces, and it helps in an interview to name all of them and explain why checkout is the odd one out.
| Surface | Typical Latency Budget | Failure Tolerance | Personalisation Depth |
|---|---|---|---|
| Search results | 150–300 ms | Can show empty state | High |
| Product detail page | 200–500 ms | Can show generic related items | High |
| Cart page | 100–200 ms | Can omit widget entirely | Medium |
| Checkout page | Under 40–60 ms, non-blocking | Zero tolerance for added latency or errors | Medium; favours safe, high-confidence items |
| Post-purchase / email | Seconds to minutes, async | Fully tolerant | High |
Notice the pattern: as you move closer to the moment money actually changes hands, the acceptable latency shrinks and the tolerance for any kind of failure shrinks with it. Checkout sits at the strictest end of that spectrum, which is exactly why it deserves its own purpose-built subsystem rather than a bolted-on reuse of the product-page engine.
Problem & Motivation
Let’s state the problem precisely, the way you would in a system design interview, before drawing a single box.
2.1 Functional Requirements
- Given a user, their current cart contents, and checkout context (shipping address, payment method being used, order value), return a ranked list of one to five cross-sell items (complementary products) and/or upsell offers (higher-tier version of something already in cart, or a shipping/warranty upgrade).
- The recommendations must respect real-time inventory (do not recommend an out-of-stock item), pricing rules, and promotional eligibility.
- The system must support A/B experimentation — different ranking models or business rules for different user cohorts.
- Clicking or adding a recommended item must update the cart and re-trigger a fast refresh of remaining suggestions.
2.2 Non-Functional Requirements
- Scale: roughly 1,000,000 requests/minute sustained (~16,700 RPS), with the ability to absorb 5–10× spikes during flash sales like Black Friday within seconds.
- Latency: P99 latency added to the checkout page render must stay under 40–60 milliseconds; the recommendation call must never block order submission.
- Availability: 99.99% for the checkout page itself. The recommendation feature is explicitly a “best-effort, degrade gracefully” feature — if it fails, checkout must still succeed with zero recommendations shown, never an error.
- Consistency: eventual consistency is acceptable for recommendation freshness (inventory can lag by a few seconds); strong consistency is required for the checkout/payment path itself, which is out of scope for this recommendation subsystem but must never be blocked by it.
- Freshness: candidate sets should reflect trends and inventory within minutes, not hours; full model retraining can happen on a daily/hourly cadence offline.
2.3 Back-of-Envelope Capacity Estimation
| Metric | Estimate |
|---|---|
| Peak requests per minute | 1,000,000 (~16,700 RPS average, up to ~150,000 RPS burst) |
| Average recommendation payload | ~2–4 KB (5 ranked items with metadata) |
| Peak outbound bandwidth | 16,700 RPS × 3 KB ≈ 50 MB/s steady, ~450 MB/s at burst |
| Cache read QPS (feature store) | ~2–3× request QPS due to internal fan-out ≈ 40,000–50,000 QPS steady state |
| Acceptable added latency (P99) | ≤ 40 ms server-side compute time |
| Model refresh cadence | Candidate sets: every 5–15 min; Ranking model: hourly to daily retrain, versioned |
“How did you get to 16,700 RPS from ‘a million requests a minute’?” — show the math out loud: 1,000,000 ÷ 60 ≈ 16,667 RPS. This signals you can do capacity estimation under pressure, and it sets up every later scaling conversation (how many pods, how many cache shards, how many Kafka partitions) with a concrete number instead of a vague “a lot.”
2.4 Explicitly Out of Scope
Naming what you are deliberately not designing is as valuable as naming what you are. This tutorial does not re-design payment processing, fraud detection, tax calculation, or the order-fulfilment workflow — it treats those as existing systems the Checkout Service already talks to. It also does not deep-dive into the training math of the ranking model itself (feature engineering, loss functions, embedding dimensionality); it treats the model as a black box that is trained offline and served as a fast-scoring artifact.
2.5 Latency Budget Breakdown
A 40 ms server-side budget sounds generous until you break it into its components. Being able to draw this table in an interview shows you understand exactly where time goes.
| Hop | Target Latency | Notes |
|---|---|---|
| API Gateway auth + routing | ~2 ms | Token validation, cached JWKS keys |
| Load Balancer hop | ~1 ms | Negligible, in-datacenter |
| Candidate Generation lookup | ~5 ms | Single Redis GET, possibly local LRU hit at ~0.1 ms |
| Feature Store lookup | ~5 ms | Parallelised with candidate lookup, not sequential |
| Ranking Service scoring | ~10 ms | Lightweight model, dozens of candidates only |
| Rules Engine filtering | ~5 ms | In-memory rule evaluation plus a cached stock snapshot check |
| Network overhead / serialisation | ~5–8 ms | Internal gRPC calls, protobuf serialisation |
| Buffer / safety margin | ~5–8 ms | Reserved so P99, not just P50, stays inside budget |
The key design implication of this table: candidate generation and feature lookup run in parallel, not sequentially, and every stage has to individually be well under its allotted slice so that the sum across P99 tail latencies — not average latencies — still fits inside 40 ms.
Architecture & Components
Below is the full architecture. Every box is explicitly labelled with the component it represents — API Gateway, Load Balancer, Checkout Service, Recommendation Orchestrator, Feature Store Cache, Candidate Generation, Ranking Service, Business Rules Engine, and the asynchronous/offline layer that keeps everything fresh without ever sitting in the request path.
3.1 Component-by-Component Walkthrough
API Gateway
Single front door for every external request — terminates TLS, authenticates the session token, applies per-user and per-IP rate limiting, and routes to the correct downstream service. Always mention it must itself be horizontally scaled, not treated as a single box.
Load Balancer
Layer 7 routing between the Gateway and the Checkout Service fleet, with active health checks every few seconds. At this scale, run as a managed service (ALB / GCP LB / NGINX Plus cluster) rather than a single NGINX box.
Checkout Service
The system of record for the purchase itself — cart validation, payment orchestration, order creation. Calls the Recommendation Orchestrator as a side-call, never a blocking dependency.
Recommendation Orchestrator
The brain of this feature. Fires parallel calls to Feature Store, Candidate Generation, Ranking, and Rules under one strict timeout budget (e.g., 40 ms). On slow downstream, returns whatever it has, or an empty/fallback list — never waits.
Feature Store Cache
Redis cluster holding pre-computed user features (recent views, purchase-history embeddings), item features (popularity, margin, co-purchase stats), and pre-joined candidate lists, all refreshed by the streaming pipeline.
Candidate Generation Service
Narrows millions of catalog items down to dozens using pre-computed co-occurrence tables, embedding similarity, or simple rules — almost always served from cache rather than computed live.
Ranking Service
Scores/orders the small candidate set using a lightweight model (logistic regression, gradient-boosted trees, or a small neural net) — deliberately not a heavyweight deep model in this hot path.
Business Rules Engine
The final filter — strips out-of-stock items, enforces margin floors, applies promo eligibility, and de-duplicates against items already in the cart.
Kafka Event Stream
Ingests checkout, click, and add-to-cart events asynchronously — never sits in the synchronous path.
Stream Processor (Flink)
Continuously updates the Feature Store Cache and writes aggregate data to the Data Warehouse.
Offline Training + Model Registry
Retrains ranking and candidate-generation models on a schedule and publishes versioned artefacts without any downtime to the live path.
Product Catalog, Inventory, Pricing & Promos
Existing systems of record elsewhere in the platform. The Rules Engine reads from a locally cached, frequently-refreshed snapshot of stock and price for the few dozen candidate SKUs — never a full-consistency call per request.
A subtlety worth calling out explicitly: none of Product Catalog, Inventory, or Pricing sit inside the millisecond-critical fan-out from the Recommendation Orchestrator. They are accessed indirectly, through a cache the Rules Engine maintains and refreshes on its own short cycle (seconds, not minutes), which decouples the checkout-time latency budget from the response time of three services this team likely does not even own.
“Where exactly would you put a strict timeout, and what happens on timeout?” — the single most important question for this system. The timeout lives in the Recommendation Orchestrator, wrapping every downstream call (Feature Store, Candidate Gen, Ranking, Rules). On timeout, it returns the best partial result it has — or an empty list — and the Checkout Service proceeds regardless. The order must never fail or slow down because a recommendation was late.
3.2 What Happens When the Shopper Clicks “Add” on a Recommended Item
This is a second, distinct micro-flow worth walking through, because it is a common interview follow-up. When a shopper taps a cross-sell/upsell suggestion, the client fires a lightweight add-to-cart request that updates the Checkout Service’s cart state and, in the background, publishes an item_added_from_recommendation event to Kafka for attribution and future training data. The checkout page then re-requests recommendations with the updated cart contents — but critically, this second call is treated exactly like the first: same 40 ms budget, same fallback behaviour, never blocking.
public class RecommendationAddToCartHandler {
public AddToCartResult handle(String userId, String sku, String recommendationSourceId) {
// 1. Synchronously update the cart (must succeed for UI consistency).
AddToCartResult result = cartService.addItem(userId, sku);
// 2. Fire-and-forget attribution event; never blocks the response.
eventPublisher.publishAsync(
new ItemAddedFromRecommendationEvent(userId, sku, recommendationSourceId)
);
return result;
}
}
Notice the same discipline repeats: the user-facing action (updating the cart) is synchronous and authoritative, while the analytics/attribution event that feeds tomorrow’s better recommendations is asynchronous and best-effort.
Internal Working
Let’s zoom into exactly what happens, in order, for a single checkout page load.
Notice the request never touches the payment gateway, the fraud check, or any part of the actual money-moving path. It runs entirely in parallel with (or slightly after) the checkout page’s own render, and the widget is designed to “pop in” once ready — if it is not ready within budget, the checkout page simply renders without it, no spinner, no blocking.
Imagine cart contains a laptop. Candidate Generation, using a pre-computed co-purchase table refreshed every ten minutes, instantly returns “laptop sleeve, wireless mouse, 2-year extended warranty” as candidates — this lookup is a single Redis GET, sub-millisecond. Ranking scores these three using recent conversion rates for this user segment. Rules Engine checks: is the mouse in stock in the user’s fulfilment region? Yes. Does the warranty apply to this exact SKU? Yes. Final list: mouse, warranty, sleeve — sent back in under 15 ms total.
4.1 Why Pre-Computation Matters More Than Real-Time Computation Here
A naive design would compute “similar items” live for every request using a nearest-neighbour search over embeddings. At 16,700+ RPS that is an enormous amount of live vector math. Instead, the heavy lifting — co-occurrence tables, embedding similarity, popularity ranks — is computed offline/asynchronously and simply looked up at request time. The request-time path does almost no computation; it does lookups and light scoring. This is the single biggest architectural decision in this whole system.
“Where would you draw the line between what’s computed online vs offline?” — rule of thumb: anything that needs to reflect a change within milliseconds-to-seconds (inventory availability, current cart contents) is computed online (cheap lookups/filters only). Anything that reflects longer-term patterns (what items co-occur, what a user tends to like) is computed offline/asynchronously and just read at request time.
Data Flow & Lifecycle
There are really two lifecycles running side by side: the fast synchronous request lifecycle (covered above) and the slower asynchronous data lifecycle that keeps the cache fresh. Here is the second one.
Walking through the lifecycle stage by stage:
- Event capture — every add-to-cart, checkout view, recommendation impression, recommendation click, and completed purchase is published as an event to Kafka. This is fire-and-forget from the Checkout Service’s point of view; it never waits for an acknowledgment before responding to the user.
- Stream processing — a Flink (or Spark Structured Streaming) job consumes these events in near real time, maintaining rolling aggregates such as “item X was co-purchased with item Y 340 times in the last hour” and writing the freshest slices directly into the Feature Store Cache (Redis), so the next request benefits within minutes.
- Batch analytics — the same events also land in a Data Warehouse for longer-term analysis, dashboards, and as training data for the offline pipeline.
- Offline training — on a schedule (hourly for candidate refresh, daily/weekly for the ranking model), a batch job retrains or updates the ranking model and candidate tables using the full historical dataset, not just the last hour.
- Model publishing — new model artefacts are versioned and pushed to a Model Registry. The live Ranking Service polls or subscribes for new versions and hot-swaps them, with the old version kept warm for instant rollback.
“What happens if the stream processor falls behind or Kafka has a backlog?” — because the Feature Store Cache always serves the last known-good data, a lagging pipeline simply means slightly staler recommendations, not a broken checkout. This is a deliberate trade-off: freshness degrades gracefully, correctness of the purchase flow never does.
5.1 Candidate Generation Techniques, and How They Combine
“Candidate generation” is deliberately abstracted as one box in the diagram, but it is worth unpacking the actual techniques feeding it offline, since interviewers often probe this layer specifically.
Item-Based Collaborative Filtering
The classic “customers who bought X also bought Y” co-occurrence table, computed in batch from historical order data. Cheap to serve (a single lookup) and a strong baseline, though it struggles with brand-new items that have no purchase history yet.
Content-Based Similarity
Uses catalog metadata (category, brand, attributes) or text/image embeddings to find similar items even without behavioural co-occurrence data — this is what covers the cold-start case for new products.
Two-Tower Embeddings
User and item embeddings are trained jointly offline so closeness in vector space approximates likely affinity; at serving time the top-K neighbours per item are pre-materialised directly into the Feature Store so no live vector search is needed at all at the checkout surface.
Business-Curated Rules
Merchandising teams can pin specific bundles (e.g., always suggest a case with a new phone model) — blended in as a high-priority override layer on top of the statistically generated candidates.
In production, all three statistical techniques typically feed a blended candidate list, and the offline pipeline periodically evaluates which technique (or blend) is driving the best conversion lift per category before the next candidate refresh is materialised into the cache.
Advantages, Disadvantages & Trade-offs
No architecture is free of trade-offs, and a design that only lists advantages is usually a sign the trade-offs were not thought through. The choices made so far — pre-computation over live computation, eventual consistency over strong consistency for recommendation data, and aggressive failure isolation over deep integration with the checkout flow — each buy real benefits at a real cost, and it is worth naming both sides plainly before moving on to how the system scales.
Advantages
- Zero added risk to the core purchase flow — recommendations are strictly additive and fail open (fail to “show nothing”), never fail closed (fail to “block checkout”).
- Pre-computation makes the hot path cheap and horizontally scalable to very high QPS with commodity hardware.
- Clean separation lets recommendation, catalog, and checkout teams deploy independently.
- Streaming freshness means the system adapts to trending items or flash-sale demand within minutes, not hours.
Disadvantages / Costs
- Pre-computed candidates can be a few minutes stale, which is unacceptable if you need true real-time personalisation (e.g. reacting instantly to something the user just clicked one second ago).
- Operating two systems (fast-path cache + offline batch/stream pipeline) is materially more operational complexity than one monolithic recommendation call.
- Cache staleness bugs (serving a now out-of-stock item) require a final real-time inventory check, adding one more hop and dependency to reason about.
- A/B testing multiple ranking models at this scale requires careful traffic splitting and consistent-hashing so a user does not flip between experiment arms mid-session.
6.1 Key Trade-off: Freshness vs Latency vs Cost
You cannot maximise all three. Fully real-time recommendation (freshest, but slower and much more compute-expensive per request) vs fully pre-computed (fastest and cheapest, but staler) vs a hybrid (moderate freshness via minute-level cache refresh, near-real-time online rules for hard constraints like stock). This system deliberately picks the hybrid: pre-computed candidates and rankings, refreshed every few minutes, layered with a real-time rules filter for the few constraints that truly cannot tolerate staleness (in-stock status, price).
“Would you ever compute rankings fully live instead of caching them?” — only if the SLA budget were far larger (e.g. product-page browsing at 200–500 ms) or the traffic far lower. At checkout scale and latency budget, live computation for every request is not economically or technically sensible — say this explicitly to show you understand the trade-off is deliberate, not accidental.
6.2 Consistency Trade-off, Explicitly
This system intentionally runs two different consistency models side by side. The purchase itself (payment, order creation, final inventory decrement) demands strong consistency — you cannot sell the same last unit twice. The recommendation subsystem, by contrast, is explicitly eventually consistent: a stock count that is 5–10 seconds stale, or a popularity rank that is 10 minutes stale, causes at worst a slightly suboptimal suggestion, never a business-correctness bug. Naming this split out loud in an interview is a strong signal that you know consistency requirements are not a single, system-wide property but something you choose per-component based on the cost of being wrong.
6.3 Cost vs Personalisation Depth
A deeper personalisation model (larger embeddings, more candidate diversity, more real-time signal) generally costs more compute and more caching infrastructure per served recommendation. Because this system serves roughly 16,700+ requests per second, even a small per-request cost increase multiplies into a large infrastructure bill. The design favours a moderately deep, cheap-to-serve model precisely because the marginal conversion lift from a marginally smarter model rarely justifies the multiplied infrastructure cost at this volume — a trade-off worth stating explicitly rather than assuming “more personalised is always better.”
Performance & Scalability at Millions of Requests per Minute
At ~16,700 RPS sustained (and bursts several times higher), every component in the synchronous path must scale horizontally and independently.
7.1 Horizontal Scaling of the Hot Path
- API Gateway & Load Balancer — run as a managed, auto-scaling fleet across multiple availability zones; each instance is stateless, so scaling out is just adding replicas behind DNS/anycast.
- Checkout Service & Recommendation Orchestrator — stateless services deployed on Kubernetes with Horizontal Pod Autoscaling driven by CPU and, more importantly, by request-latency and queue-depth custom metrics, since latency SLA breaches should trigger scale-out before CPU saturates.
- Feature Store Cache (Redis) — run as a sharded cluster (Redis Cluster mode) with consistent hashing across shards, plus read replicas per shard so read-heavy traffic (candidates/features are read far more than written) can scale reads independently of writes.
- Candidate Generation & Ranking Services — also stateless and horizontally scaled; because they mostly do cache lookups plus lightweight scoring (not heavy model inference), a single instance can handle thousands of RPS, so the fleet size needed is modest relative to Checkout Service.
7.2 Caching Strategy in Depth
Three layers of caching keep the request-time compute near zero:
CDN / Edge Cache
For the static shell of the recommendation widget (HTML/CSS/JS), never for personalised data.
In-Process LRU
Application-level in-memory cache inside each Recommendation Orchestrator pod — caches the very hottest candidate lists (e.g., top 1,000 SKUs’ co-purchase lists) directly in process memory to avoid even a network hop to Redis for the most common lookups.
Distributed Cache (Redis)
The source of truth for pre-computed candidates, features, and rankings, refreshed continuously by the streaming pipeline.
7.3 Handling Flash-Sale Bursts (5–10× Normal Traffic)
- Pre-warm autoscaling groups ahead of known events (Black Friday, flash sales) rather than relying solely on reactive autoscaling, since reactive scale-out has a startup lag of 30–90 seconds that a sudden 10× spike will outrun.
- Apply request-level load shedding at the API Gateway: if the Recommendation Orchestrator fleet is saturated, the Gateway can simply skip forwarding the recommendation sub-request and let the checkout page render without it, rather than queueing requests and risking latency spikes.
- Use a bulkhead pattern — the Recommendation Orchestrator’s thread/connection pool is isolated from the Checkout Service’s own resources, so a recommendation slowdown can never starve the resources needed to actually complete a purchase.
7.4 Capacity Planning Math, Worked Out
Concrete numbers make this section land well in an interview. Assume each Recommendation Orchestrator pod, doing mostly cache lookups and light scoring, can comfortably sustain 800 requests/second before its own latency starts to climb toward the budget ceiling.
| Component | Sustained QPS Needed | Per-Instance Capacity | Instances (with 40% headroom) |
|---|---|---|---|
| Recommendation Orchestrator | 16,700 | 800 RPS / pod | ~30 pods |
| Feature Store Cache (Redis) | ~45,000 (fan-out reads) | ~100,000 ops/s per shard | 3–4 shards, 2 replicas each — run 8+ for headroom + blast-radius reduction |
| Ranking Service | 16,700 | 1,200 RPS / pod | ~20 pods |
| Kafka partitions (checkout-events) | ~5,000 events/s avg | ~10,000 events/s per partition | 8–12 partitions, sized more for consumer parallelism than raw throughput |
The instance counts above are deliberately conservative and rounded, on purpose — in an interview, showing you can reason your way to a plausible order-of-magnitude answer matters far more than reciting an exact number, since real capacity planning always ends in load testing against these estimates.
7.5 Java Example: Strict Timeout Budget With Graceful Fallback
// RecommendationOrchestratorService.java
// Demonstrates a bounded, parallel fan-out with a hard timeout and safe fallback.
import java.util.concurrent.*;
import java.util.List;
import java.util.Collections;
public class RecommendationOrchestratorService {
private final ExecutorService executor = Executors.newFixedThreadPool(64);
private static final long TIMEOUT_MS = 40;
public List<Recommendation> getRecommendations(CheckoutContext context) {
CompletableFuture<List<Candidate>> candidatesFuture =
CompletableFuture.supplyAsync(() -> candidateService.fetch(context), executor);
try {
List<Candidate> candidates = candidatesFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
List<Candidate> ranked = rankingService.rank(candidates, context);
List<Recommendation> filtered = rulesEngine.applyRules(ranked, context);
return filtered;
} catch (TimeoutException e) {
// Never block checkout: fall back to a safe, pre-computed static list.
metrics.increment("recommendation.timeout");
return staticFallbackProvider.getPopularItems(context.getCategory());
} catch (Exception e) {
metrics.increment("recommendation.error");
return Collections.emptyList();
}
}
// Dependencies injected: candidateService, rankingService, rulesEngine,
// staticFallbackProvider, metrics -- omitted here for brevity.
}
“Why a fixed thread pool and an explicit timeout instead of just calling the downstream services directly?” — because an unbounded call can hang indefinitely if a downstream service is slow, silently degrading the whole Checkout Service. A bounded pool plus an explicit timeout guarantees a worst-case response time for the caller no matter what happens downstream — this is the core resilience pattern for this entire system.
High Availability & Reliability
The single most important reliability principle in this system: the recommendation subsystem must be allowed to fail completely without ever failing the checkout. Everything below flows from that one sentence.
8.1 Failure Isolation Patterns Used
Circuit Breaker
The Recommendation Orchestrator wraps each downstream call (Feature Store, Candidate Gen, Ranking, Rules) in a circuit breaker (e.g., Resilience4j). If a dependency’s error rate crosses a threshold, the breaker opens and short-circuits calls immediately, returning fallback data instead of repeatedly hammering a failing service.
Bulkhead Isolation
Separate thread pools and connection pools per downstream dependency, so a slow Ranking Service cannot exhaust the resources needed to call the (healthy) Feature Store.
Timeouts Everywhere
No network call in this system is allowed to be unbounded — this is the single most common production incident cause in recommendation systems (one slow dependency cascades into a full outage).
Static Fallback
A pre-baked, globally-cached “top sellers” list that requires zero personalisation and zero live dependency, used whenever anything upstream fails.
8.2 Redundancy and Multi-AZ / Multi-Region Design
- Every stateless service (Gateway, Checkout, Orchestrator, Candidate Gen, Ranking, Rules) is deployed across at least three availability zones, with the Load Balancer health-checking and routing only to healthy zones.
- Redis Cluster runs with replicas in each shard, and cross-AZ replica placement so a single AZ outage does not lose an entire cache shard.
- Kafka runs with replication factor 3 across AZs so event ingestion survives a broker or AZ failure without data loss for the async pipeline.
“What is your availability target for the recommendation feature specifically, vs the checkout page overall?” — the checkout page target is 99.99%. The recommendation sub-feature can have a lower internal SLA (say 99.9%) because its failure mode is invisible to the user — they just see no recommendations, not an error. Decoupling these two SLAs is exactly what lets you build the recommendation system more cheaply and experimentally without risking the core business metric.
8.3 Regional Failover Walkthrough
If an entire region serving the hot path becomes unhealthy (a data-center network partition, for example), the sequence of automated events looks like this: global DNS/anycast health checks detect the region’s Load Balancer failing checks within seconds; traffic is automatically rerouted to the nearest healthy region’s Gateway; that region’s Recommendation Orchestrator fleet, already running warm (not cold-started on demand), absorbs the shifted load, relying on autoscaling headroom that was provisioned ahead of time specifically to handle one region’s worth of failover traffic. Because the Feature Store Cache in each region is refreshed independently by the same global event stream, the failover region already has reasonably fresh candidate data rather than starting from an empty cache — this is why pre-warming and cross-region cache parity are treated as a reliability requirement, not just a performance nicety.
8.4 Chaos Engineering for This System
Because the entire reliability story rests on “the recommendation subsystem can fail without affecting checkout,” this claim should be actively verified, not just assumed from a design doc. Regular game-day exercises inject controlled failures — killing a percentage of Recommendation Orchestrator pods, artificially delaying Feature Store responses past the timeout budget, or fully cutting network access to the Ranking Service — and verify two things automatically: that the checkout page’s own success rate and latency are completely unaffected, and that the fallback path actually engages and serves the static list correctly. Any regression in either of those two checks is treated as a release-blocking bug, since it directly contradicts the system’s core promise.
Security
AuthN/AuthZ at the Gateway
Every request must carry a valid session token validated at the API Gateway before it reaches any internal service; internal services trust the Gateway’s validation via signed internal tokens (mTLS or JWT) rather than re-validating raw credentials.
Rate Limiting
Per-user and per-IP request quotas at the Gateway and Load Balancer layer to prevent scraping of recommendation data or automated cart manipulation designed to game promotions.
Data Minimisation in the Hot Path
The Feature Store should store derived features (e.g., “user affinity score for category X”) rather than raw PII, so a cache breach exposes far less sensitive information.
Encryption
TLS in transit everywhere (client to Gateway, service to service), and encryption at rest for the Data Warehouse and any store holding purchase history.
Model / Rules Integrity
The Rules Engine, not the Ranking model, is the final authority on price, margin, and stock — this prevents an ML model, which could be manipulated via adversarial inputs or simply be wrong, from ever emitting an incorrect price or promoting an item the business does not want promoted.
Privacy Compliance
User behavioural data feeding the offline training pipeline should honour consent settings (e.g., GDPR/CCPA opt-outs) — a user who opts out of personalisation should fall back to the same static “top sellers” list used for failure cases.
9.1 Threat Modelling for This Specific Feature
Beyond generic web security hygiene, this feature has a few threat vectors worth naming explicitly: competitors or scrapers hitting the recommendation endpoint at high volume to reverse-engineer pricing or bundling strategy (mitigated by rate limiting and by never exposing raw model scores or internal candidate-source metadata to the client); promo-abuse attempts where a user manipulates cart contents to trigger an upsell discount they should not qualify for (mitigated by re-validating promo eligibility server-side in the Rules Engine, never trusting client-supplied discount flags); and replay of a stale recommendation response to purchase an item at an outdated price (mitigated by the Checkout Service re-validating price and stock authoritatively at order submission, regardless of what the recommendation response said).
Letting the ML ranking model be the last word on price or stock. ML rankings optimise for a learned objective (click/conversion likelihood) which can drift or be gamed, while price and stock are hard business/legal constraints. Keep them in a deterministic, auditable rules layer, applied after ranking.
“Why should the Rules Engine, not the ML model, own pricing and inventory decisions?” — because ML rankings optimise for a learned objective (click/conversion likelihood) which can drift or be gamed, while price and stock are hard business/legal constraints. Keeping them in a deterministic, auditable rules layer, applied after ranking, is both safer and easier to reason about, test, and audit.
Monitoring, Logging & Metrics
Because this system’s core promise is “never slow down checkout,” its most important dashboards are latency-focused, not just error-focused.
10.1 Key Metrics to Track
| Metric | Why It Matters |
|---|---|
| Latency percentiles (P50 / P95 / P99) | Isolated per component — tells you exactly which hop is responsible if the checkout page slows down |
| Timeout rate & fallback rate | Fraction of requests that hit the 40 ms budget and fell back to static content — earliest warning sign before a full outage |
| Cache hit ratio (Feature Store + LRU) | A dropping hit ratio usually means a cold cache after a deploy or stream-processing lag |
| Recommendation impression / CTR / add-to-cart rate | Business metrics, tracked separately per experiment arm |
| Incremental revenue attributable to recs | Ultimate business justification for the whole system |
| Model drift metrics | Live CTR vs predicted probabilities — detects when the ranking model needs retraining sooner than scheduled |
10.2 Tooling
Prometheus scrapes per-service metrics (latency histograms, error counters, timeout counters); Grafana dashboards visualise P99 latency and fallback rate in real time; distributed tracing (Jaeger or Zipkin, via OpenTelemetry instrumentation) traces a single request across Gateway → Checkout → Orchestrator → Feature Store/Ranking/Rules so an on-call engineer can pinpoint exactly which hop is slow during an incident. Centralised structured logging (e.g., via the ELK stack or a managed equivalent) captures request-level context (which fallback path was taken, which model version served the request) for post-incident analysis.
A concrete example of tracing paying off: if the P99 latency dashboard shows a spike, the on-call engineer opens a sample of slow traces from that time window and can immediately see, span by span, whether the extra time was spent waiting on the Feature Store network call, inside the Ranking Service’s scoring loop, or in serialisation overhead — turning a vague “recommendations feel slow” report into a precise, actionable finding within minutes instead of hours of guesswork across service logs.
10.3 SLOs and Error Budgets
A concrete Service Level Objective for this feature might read: “99% of recommendation responses complete within 40 ms, and the Recommendation Orchestrator’s own error rate stays below 0.1%, measured over a rolling 28-day window.” The error budget derived from that (the 1% of requests allowed to miss the latency target, and the 0.1% allowed to error) gives the team an explicit, pre-agreed threshold for how much risk they can take on with new model rollouts or infrastructure changes before pausing releases and focusing purely on stability — a standard SRE practice that keeps “is this safe to ship” a data-driven question rather than a debate.
“What single alert would you set up first for this system?” — a P99 latency alert on the Recommendation Orchestrator combined with a fallback-rate alert. If P99 latency crosses the 40 ms budget or fallback rate spikes above a small baseline, that is the earliest, most actionable signal that something upstream (cache, ranking, rules) is degrading — well before it could ever threaten the checkout page itself.
Deployment & Cloud Considerations
Containerisation + Kubernetes
Every stateless service ships as a Docker container and runs on Kubernetes (EKS/GKE/AKS or self-managed), with Horizontal Pod Autoscalers tuned to latency and queue-depth metrics, not just CPU.
Progressive Delivery
New Ranking Service model versions and Rules Engine logic changes are rolled out via canary deployments — 1%, then 10%, then 100% — with automatic rollback if latency or error metrics regress.
Multi-Region Strategy
The synchronous hot path (Gateway, Checkout, Orchestrator, Candidate Gen, Ranking, Rules, Redis) is deployed per region close to users to minimise network latency; the offline pipeline (Kafka, Flink, Data Warehouse, training) can be centralised or regional depending on data residency requirements.
Infrastructure as Code
The entire topology (Kubernetes manifests, Redis cluster topology, Kafka topics/partitions, autoscaling policies) is defined via Terraform or equivalent, so a full region can be reproduced or disaster-recovered predictably.
Cost Optimisation
Hot-path services do mostly cache lookups and light scoring, so they run on smaller, cheaper instance types than the offline training pipeline, which benefits from larger, possibly spot/preemptible instances for batch jobs that can tolerate interruption.
11.1 Canary vs Blue-Green for This System, Specifically
| Strategy | Best Fit Here | Why |
|---|---|---|
| Canary (gradual % ramp) | Ranking model updates, rules changes | Lets you compare business metrics (CTR, conversion) between old and new versions on live but limited traffic before full rollout |
| Blue-Green (instant full cutover + fast rollback) | Infrastructure / platform changes (new Kubernetes version, Redis cluster topology change) | These changes are binary and operational, not something you want to A/B on business metrics — you want a fast, clean rollback path if anything breaks |
“Would you run the offline training pipeline in the same region as the live traffic?” — not necessarily. Training is not latency-sensitive, so it can run wherever compute is cheapest or where the Data Warehouse already lives, as long as the resulting model artefacts are replicated to every serving region before being hot-swapped into the Ranking Service.
Databases, Caching & Load Balancing In Depth
12.1 Why Redis (or an Equivalent In-Memory Store) Instead of a Relational Database
The hot path needs sub-millisecond key-value lookups at tens of thousands of QPS. A relational database, even well-indexed, is not designed for this access pattern at this scale without heavy read-replica fan-out, and it carries far more overhead (query planning, locking, connection overhead) than a purpose-built in-memory key-value store. Redis Cluster, sharded by consistent hashing on a key like candidates:{itemId}, gives predictable, horizontally scalable, low-latency reads.
12.2 Data Model Example
Key: candidates:SKU-88213
Value (JSON): {
"co_purchase": ["SKU-77102", "SKU-90341", "SKU-10045"],
"updated_at": "2026-08-03T10:15:00Z",
"popularity_rank": 12
}
Key: features:user:USR-556210
Value (JSON): {
"affinity_electronics": 0.82,
"affinity_home": 0.14,
"recent_categories": ["electronics", "accessories"]
}
12.3 Load-Balancing Strategy
At the edge, Layer 7 load balancing with weighted round robin and active health checks distributes traffic across Checkout Service and Recommendation Orchestrator replicas. Internally, client-side load balancing (e.g., via a service mesh like Istio/Linkerd, or client libraries with built-in load-aware routing) lets each service pick the least-loaded healthy replica of its downstream dependency rather than relying solely on a central load balancer for every internal hop, reducing latency and central bottlenecks.
12.4 Database for the Offline / Analytical Side
The Data Warehouse (Snowflake, BigQuery, or Redshift) stores the full historical event log for training and analytics — optimised for large scans and aggregations, not point lookups. This is intentionally a separate system from the hot-path Redis cache; mixing analytical and transactional/lookup workloads on one datastore is a common anti-pattern this design deliberately avoids.
12.5 TTL Strategy by Data Type
| Cached Data | TTL | Reasoning |
|---|---|---|
| Candidate co-purchase lists | 15–30 minutes | Refreshed continuously by streaming pipeline; TTL is a safety net, not the primary refresh mechanism |
| User affinity features | 1–6 hours | Behavioural affinity shifts slowly within a single session |
| Stock / price snapshot (Rules Engine) | 5–15 seconds | The one place true near-real-time freshness genuinely matters |
| Static fallback / popular items list | 1–24 hours | Deliberately long-lived and simple; must be available even during upstream outages |
“How do you keep the Redis cache from becoming a single point of failure at this scale?” — cluster mode with multiple shards (so no single node holds all keys), replicas per shard for read scaling and failover, and a client library that automatically retries against a replica or the static fallback path if a shard is temporarily unreachable.
APIs & Microservices
Each component in the diagram is its own microservice with a narrow, well-defined API contract. Below are simplified interface examples.
POST /internal/v1/recommendations
Request:
{
"userId": "USR-556210",
"cartItems": ["SKU-88213"],
"checkoutContext": {
"region": "US-WEST",
"currency": "USD"
},
"maxResults": 5
}
Response (200 OK, within 40ms budget):
{
"recommendations": [
{"sku": "SKU-77102", "type": "cross_sell", "score": 0.91},
{"sku": "WARR-88213", "type": "upsell", "score": 0.77}
],
"servedFrom": "cache",
"modelVersion": "v42"
}
Response (degraded / fallback):
{
"recommendations": [
{"sku": "SKU-00001", "type": "popular_fallback", "score": null}
],
"servedFrom": "static_fallback",
"modelVersion": null
}
13.1 Java Interface Example: Contract Between Checkout Service and Orchestrator
public interface RecommendationClient {
// Must be a non-blocking call with a caller-supplied timeout.
CompletableFuture<List<Recommendation>> fetchRecommendations(
String userId,
List<String> cartItemSkus,
CheckoutContext context,
Duration timeoutBudget
);
}
public class Recommendation {
private final String sku;
private final String type; // "cross_sell" or "upsell"
private final Double score; // nullable for fallback items
private final String servedFrom; // "cache" or "static_fallback"
// getters/constructor omitted
}
Keeping this contract narrow and stable lets the Recommendation team change candidate generation algorithms, ranking models, or even the entire internal architecture of the Orchestrator without ever requiring a change in the Checkout Service — a textbook example of why microservice boundaries should align with team and change-cadence boundaries, not just technical convenience.
13.2 API Versioning and Backward Compatibility
Because the Checkout Service and Recommendation Orchestrator are owned by different teams and deployed independently, the internal API contract is versioned explicitly (the /v1/ prefix shown above) and evolved only in backward-compatible ways — adding new optional fields, never removing or repurposing existing ones — so either side can deploy on its own schedule without a coordinated release. Breaking changes, when unavoidable, are introduced as a new version path served in parallel until every caller has migrated.
“Would you use REST, gRPC, or something else for internal calls at this scale?” — gRPC (over HTTP/2) is generally preferred for internal service-to-service calls at high QPS because of binary serialisation (smaller payloads, faster parsing) and multiplexed connections, both of which matter when you are making tens of thousands of internal calls per second under a tight latency budget.
Design Patterns & Anti-patterns
14.1 Patterns Used
Circuit Breaker
Isolates failures in downstream dependencies (covered in the HA section).
Bulkhead
Isolates resource pools per dependency so one slow call cannot starve others.
Scatter-Gather with Bounded Timeout
The Orchestrator calls multiple services in parallel and gathers whatever returns within budget.
Cache-Aside
Services read from Redis first and only fall back to a rare, slow recompute path if truly necessary — in this design, that recompute path is itself replaced by “return fallback,” not a live recomputation, to preserve the latency budget.
CQRS
The write path (events flowing through Kafka into the Feature Store) is entirely separate from the read path (Orchestrator reading from Redis), letting each scale and evolve independently.
Strangler-Fig Rollout
New ranking models or rules are introduced behind a feature flag / experiment framework and gradually ramped up, never a hard cutover.
14.2 Anti-Patterns to Avoid
Common Mistakes
- Synchronous chained calls with no timeout — the classic mistake: Checkout calls Orchestrator calls Feature Store calls Catalog calls Pricing, all synchronously, with no bounded timeout anywhere. One slow link makes the whole chain (and the checkout page) slow.
- Computing recommendations live from the full catalog on every request — does not scale at this QPS and adds unacceptable latency; always narrow via pre-computed candidates first.
- Coupling recommendation failure to checkout success — if a try/catch around the recommendation call is missing anywhere in the Checkout Service, a bug in the recommendation subsystem can take down purchases entirely. This must be structurally prevented, not just tested for.
- One giant shared cache namespace with no key structure or TTLs — leads to stale data lingering indefinitely and cache eviction unpredictability; always use clear key namespaces and explicit TTLs matched to how fast that data actually changes.
14.3 Idempotency for the Add-to-Cart-from-Recommendation Action
Because clients sometimes retry requests after a network blip, the add-to-cart-from-recommendation endpoint accepts an idempotency key generated by the client per tap, so a retried request updates the cart at most once rather than double-adding the item — a small but easy-to-miss detail that prevents a subtle class of duplicate-charge-adjacent bugs.
“Give an example of an anti-pattern you specifically avoided in this design and why.” — a strong answer: avoiding synchronous, unbounded chained calls by putting a hard timeout at the single entry point (the Orchestrator) rather than hoping every downstream service independently times out correctly — centralising the timeout removes an entire class of cascading failure.
Best Practices & Common Mistakes
Most production incidents in systems like this one trace back not to the ranking algorithm being wrong, but to one of the operational disciplines below being skipped under deadline pressure. Treat this list as the minimum bar, not an aspirational one.
Best Practices
- Always make the recommendation call asynchronous/non-blocking relative to order submission.
- Set and enforce a hard timeout budget at a single, well-known layer (the Orchestrator).
- Pre-compute anything that can be pre-computed; keep the request-time path to lookups and light scoring only.
- Always have a zero-dependency static fallback ready to serve.
- Version models explicitly and support instant rollback.
- Track latency and fallback-rate metrics as seriously as error-rate metrics.
Common Mistakes
- Treating the recommendation call as “just another required field” in the checkout response, making it implicitly blocking.
- Retraining models without a canary/gradual rollout, causing a sudden shift in recommendation quality or latency profile.
- Under-provisioning cache capacity for flash-sale traffic spikes, causing a cache-miss storm that hits downstream services simultaneously.
- Ignoring cold-start (new users / new products with no history) — always define an explicit fallback strategy for cold-start cases, typically popularity-based.
15.1 Pre-Launch Checklist
- Confirmed the Checkout Service treats every recommendation call as best-effort, wrapped in try/catch, with a timeout enforced by the caller as well as the callee.
- Load-tested the full fan-out path at 2–3× expected peak QPS, not just average QPS.
- Verified the static fallback path works with zero live dependencies, including during a full Redis outage in a game-day exercise.
- Confirmed model rollout is gated behind a canary with automatic rollback on latency or conversion-rate regression.
- Confirmed cold-start behaviour is explicitly tested for both new users and newly listed products.
“How do you handle cold start — a brand-new user with no history, or a brand-new product with no co-purchase data?” — for new users, fall back to popularity-based or category-based recommendations. For new products, seed initial candidate scores using catalog metadata similarity (same category/brand) until enough real co-purchase signal accumulates through the streaming pipeline.
Real-World Industry Examples
Amazon
Widely documented to use pre-computed “frequently bought together” and “customers who bought this also bought” tables, refreshed through large-scale batch and streaming pipelines, with strict separation between the checkout/order system and the recommendation subsystem so recommendation failures never affect order placement.
Netflix
Though focused on content rather than checkout, Netflix’s recommendation architecture pioneered the pattern of pre-computed candidate generation plus lightweight online ranking, with heavy offline model training — the same two-speed architecture (fast online path, slow offline path) used in this design.
Uber / Food-Delivery
Uses similar “upsell at checkout” patterns for add-on items (drinks, sides) with strict SLAs so the add-on suggestion never delays order confirmation, using cached, pre-scored suggestions per restaurant/cart combination.
Alibaba
At massive scale during events like Singles’ Day, relies heavily on pre-computed candidate sets and aggressive caching, with real-time inventory checks as the final gate, since live inventory changes second-by-second during flash sales at that volume.
16.1 How the Underlying Algorithms Evolved Over Time
It is worth noting how this space has evolved, since interviewers sometimes ask “what’s the modern approach vs the old approach.” Early systems (2000s) relied almost entirely on simple item-based collaborative filtering — static co-occurrence tables recomputed in nightly batch jobs. The 2010s introduced matrix factorisation and, later, deep-learning embeddings (two-tower neural networks) that could capture more nuanced similarity than raw co-occurrence counts. The most recent evolution, seen at companies like Amazon and Alibaba, blends near-real-time streaming feature updates (rather than purely nightly batch) with these embedding-based models, so that a trending item can start showing up in recommendations within minutes of a demand spike rather than waiting for the next day’s batch job — precisely the streaming architecture this tutorial’s data-flow section describes.
Across every one of these companies, the same architectural principle repeats: separate the fast, cheap, cacheable request-time path from the slow, expensive, offline model-building path, and never let a failure in the recommendation subsystem touch the core transaction.
“What is the one architectural idea that shows up across every major company’s recommendation system at this scale?” — two-speed architecture: an offline/batch/streaming layer that does the expensive work ahead of time, and an online layer that does almost nothing but fast lookups and light scoring at request time.
Frequently Asked Questions
Post-purchase recommendations are valid and commonly used too, but they lose the “wallet is already open” psychological moment and convert at a meaningfully lower rate than in-checkout upsells. This design’s whole purpose is to keep that moment while engineering away the latency risk, rather than avoiding the moment altogether.
Typically a few seconds of staleness is acceptable, since the Checkout Service itself performs the authoritative final inventory check at order submission time regardless — the Rules Engine’s check is a best-effort filter to avoid recommending obviously unavailable items, not the system of record for stock.
Route a small, consistently-hashed percentage of users (so the same user always lands in the same arm during their session) to the new model version via a feature flag in the Recommendation Orchestrator, monitor latency and business metrics per arm, and ramp gradually with automatic rollback triggers on latency or error regressions.
The Orchestrator’s circuit breaker opens on repeated Feature Store failures and serves the static, zero-dependency fallback list for all requests until the cluster recovers — checkout itself is entirely unaffected.
Yes — the same architectural shape (pre-computed candidates, cached lookups, bounded timeout, static fallback) works at far smaller scale, just with fewer shards, fewer replicas, and possibly a simpler single-instance Redis rather than a full cluster. The principles do not change; only the degree of horizontal scaling does.
The Ranking Service can incorporate a lightweight recency/frequency penalty using recently-seen-impressions data from the Feature Store, slightly down-weighting items the same user has already been shown several times without engaging, which is a cheap addition on top of the existing scoring step rather than a separate system.
No — the checkout page should render fully functional (able to submit payment) immediately, with the recommendation widget area reserved as an empty placeholder that populates asynchronously once (and if) the recommendation response arrives within budget. This is the same “progressive enhancement” principle applied at the architecture level.
Most production systems settle on one to three for checkout specifically (versus 10–20 on a product page), since checkout is meant to be fast and low-friction — showing too many options risks distracting the shopper or, worse, causing decision fatigue that increases cart abandonment rather than incremental revenue.
Summary & Key Takeaways
Pulling everything together, this design succeeds not because any single component is exotic, but because the boundaries between components are drawn deliberately, in service of one non-negotiable requirement: the checkout page must never wait on, or fail because of, a recommendation.
Key Takeaways
- One non-negotiable constraint: recommendations must never add meaningful latency to, or ever block, the checkout/purchase flow.
- Two-speed architecture: a fast, cheap, cache-driven synchronous path (API Gateway, Load Balancer, Checkout Service, Recommendation Orchestrator, Feature Store Cache, Candidate Generation, Ranking, Rules Engine) and a slower, expensive asynchronous/offline path (Kafka, Stream Processor, Data Warehouse, Offline Training, Model Registry) that keeps the fast path fresh without ever sitting inside a request.
- Resilience patterns are load-bearing: bounded timeouts, circuit breakers, bulkheads, and a zero-dependency static fallback are what make “fail open, never fail closed” actually true in production, not just a slide in a design doc.
- Three-level caching: at ~1,000,000 requests/minute, every hot-path component must be stateless and horizontally scalable, with caching layered at three levels (edge, in-process, distributed) to keep request-time compute close to zero.
- Rules Engine owns hard constraints: the Rules Engine, not the ML model, remains the final authority on inventory, price, and promotions — keeping business-critical correctness deterministic and auditable, separate from statistically learned ranking.
- The industry converges here: every major company that has solved this problem at scale (Amazon, Netflix, Uber, Alibaba) converges on the same shape — pre-compute what you can, cache aggressively, and isolate the recommendation feature so its failure is invisible to the customer.
- The one-sentence takeaway: separate what must be strongly consistent and synchronous (the purchase) from what can be eventually consistent and best-effort (the suggestion), and build the failure-isolation mechanics — timeouts, circuit breakers, bulkheads, static fallbacks — that make that separation actually hold under real production failure conditions, not just on the architecture diagram.
Every architectural decision in this system — from parallel fan-out with a bounded timeout, to pre-computing candidates offline, to letting the Rules Engine be the final word on price and stock — exists in service of one sentence: the recommendation feature is allowed to fail, but the purchase never is. If your design keeps that separation intact from the outermost API Gateway all the way down to the innermost cache read, everything else is tuning.