11. How would you design ad frequency and order tracking?
Design a low-latency, high-throughput system that tracks ad frequency and delivery order across requests. Cover data flow, storage, freshness, and how you would keep reads fast under load.
At a high level, this system must decide how often a user can see an ad and which ad comes next. The main challenge is keeping that decision fast while the delivery state keeps changing. I would explain it in three flows: the request path, the state lookup path, and the background update path. Reads use a Hot State Cache first, while delivery events update durable state later. The trade-off is that cached state can become stale, so freshness checks and conservative decisions are important.
The system must quickly decide whether a user can receive another ad and which ad should come next. Every delivery changes the user's frequency count and delivery order. Requests still need to stay fast under load. The diagram separates the work into a fast request path and a background update path. The request path reads rules and current state, then returns a decision. The background path records deliveries, updates saved state, and keeps the cache fresh.
- Should frequency limits be tracked per user, per household, or both?
- How fresh must the frequency and order state be before serving an ad?
- What should happen when the cache or current-state store is temporarily unavailable?
I would say that an ad request first goes to the API / Edge. It handles Authentication, Validation, and Rate Limiting. Then it forwards the request to the Java Ad Frequency & Order Tracking Service.
The Java service is stateless across separate JVM replicas. Each JVM Replica runs Java 21/25 and contains a Request Handler, Eligibility / Order Engine, and Response Builder. Virtual Threads can help while waiting on blocking I/O. Separate JVM replicas do not share memory, so shared state stays outside them.
The service reads the Campaign Rules / Config Store for frequency caps, delivery order rules, and creative pools. It then checks the Hot State Cache first.
The cache stores state by user or household plus campaign. That includes count, next order, version, and timestamp. Cache-first reads keep requests fast.
If the cache misses or the entry is stale, the service reads the Durable Current-State Store. This store keeps materialized current state, meaning the useful counters and order values are already prepared. The service does not scan raw delivery events on each request.
The Eligibility / Order Engine uses the rules and current state. It checks the frequency cap and decides the next ad order.
The Response Builder creates the ad decision. The Java service sends that result back through the API / Edge to the Client / Ad Request Source.
After delivery or acknowledgement, the Java service writes a Delivery Event to the append-only Event Log / Queue. The queue is partitioned by user or household plus campaign.
The replicated Java Consumer / Updater Service reads those events. It performs idempotent per-key updates, meaning retries do not apply the same state change twice. It updates the Durable Current-State Store, then refreshes or invalidates the Hot State Cache.
The stored state carries a version or timestamp so the service can judge freshness. If the cache is unavailable, the service reads the current-state store. If freshness is uncertain, the engine makes a conservative cap or order decision.
Failed background processing goes to the Retry Queue with backoff. After retries are exhausted, the event moves to the Dead Letter Queue. Metrics, Logs, and Traces watch latency, cache hit rate, errors, queue lag, retries, and end-to-end behavior.
The trade-off is speed versus freshness. Caching keeps reads fast, but stale state must be handled carefully.
The benefit is that normal ad decisions stay fast because the service reads the Hot State Cache first. Materialized current state also keeps fallback reads simple and avoids scanning old delivery events. The downside is that cached state can become stale after a recent delivery. Versions and timestamps help detect that risk. The background event path adds more moving parts, including the Event Log / Queue, consumer, Retry Queue, and Dead Letter Queue. Partitioning helps the system grow, but updates for the same user or household plus campaign must stay logically consistent. We accept this extra complexity because it keeps the main request path fast.
Interviewers ask this question to see whether you can separate a fast request path from reliable background state updates. They want good judgment about caching, durable state, freshness, partitioning, retries, and failure handling. They also want to see whether you understand that separate Java JVM replicas do not share memory. Most importantly, they are checking whether you can explain the trade-off between fast reads and fresh, correct delivery state.









