Capstone โ Building a Rate-Limited, Cached API
๐ Capstone: Building a Rate-Limited, Cached API
The Application: A Rate-Limited Product API
Requirements: GET /products/:id should be cached and rate-limited per client; when a product is updated elsewhere (an admin panel), every running API instance should invalidate its own cached copy immediately, without polling.
Step 1: Caching the Product Endpoint (Chapter 5)
The cache-aside pattern, unchanged from Chapter 5:
Step 2: Rate Limiting With a Lua Script
Chapter 4's sliding-window pattern (prune old entries, then count) has a subtle problem under real concurrency: checking the count and recording the new request are two separate commands โ two concurrent requests could both check "under the limit" before either one's new entry is recorded, both slipping through when only one should have. The fix is an atomic Lua script, run with EVAL: Redis executes the entire script as one indivisible unit โ no other command, from any other client, can interleave partway through, turning Chapter 2's single-threaded architecture from a caveat into exactly the guarantee this needs.
Prune, count-check, and record now all happen inside one atomic EVAL call โ no race window exists between checking the limit and recording the request, regardless of how many requests arrive at the same instant.
Step 3: Invalidating the Cache via Pub/Sub (Chapter 6)
When a product is updated, the updating instance publishes the product's ID to a shared channel; every API instance โ including ones that never touched the update request โ subscribes and deletes its own local cache entry.
The next getProduct(id) call on any instance after this point sees a genuine cache miss and re-populates from the database โ every instance stays consistent with no polling and no shared coordination beyond the channel itself.
| Course Concept | Where It's Used in This App |
|---|---|
| Cache-aside pattern (Ch.5) | getProduct() โ cache first, database on miss |
| Sliding-window rate limiting (Ch.4) | The pattern the Lua script implements atomically |
| Atomic scripting / single-threaded execution (Ch.2) | The Lua script's check-then-record race condition fix |
| Pub/Sub (Ch.6) | Broadcasting cache invalidation to every running instance |
๐ป Coding Challenges
Challenge 1: Explain the Race Condition
Explain, concretely, how two concurrent requests could both bypass Chapter 4's non-atomic sliding-window rate limiter, even though each one individually checked the count correctly.
Goal: Practice articulating a check-then-act race condition in terms of the specific commands involved.
Challenge 2: Add a Cache-Warming Step
Modify Step 3's updateProduct function so that, instead of just invalidating the cache, it immediately re-caches the fresh product data โ avoiding a cache miss on the very next read.
Goal: Practice combining the cache-aside write path with the pub/sub invalidation flow.
Challenge 3: Deploying to Cluster
This app is deployed onto Redis Cluster (Chapter 9). Explain what could break in the rate-limiting Lua script if a client's rate-limit key and cache key don't share a hash tag, and how to fix it.
Goal: Practice applying Chapter 9's hash-tag requirement to a script that only ever touches one key per call versus one that might touch several.
Chapter 9's hash-tag gotcha applies just as much to Lua scripts as to plain multi-key commands: if a script's KEYS table ever includes keys that hash to different slots, Redis Cluster rejects the script entirely. This capstone's rate-limit script only ever touches one key per call, so it's safe as written โ but extending it (say, to also update a shared global counter) would need every key involved to share a hash tag, exactly the same discipline Chapter 9 required for any multi-key operation.
๐ Course Complete
That's the full Redis course โ from the in-memory data model through every core data structure, expiration and caching patterns, pub/sub, queues and streams, persistence, replication/failover/sharding, and finally a real rate-limited, cached, pub/sub-synchronized API combining all of it. Together with MySQL and MongoDB, this completes the site's three-engine database coverage โ relational, document, and in-memory.