Module 5: Capstone Project — API with Caching Strategy
Final Project: Production Cached API
Overview
This is the capstone module of the Redis & Caching Strategies guide. Across 4 modules you learned Redis's 5 data types, the 3 caching patterns, sliding window rate limiting with sorted sets, sessions complementing JWT, Pub/Sub for invalidation events, and professional integration with FastAPI using redis.asyncio. Each module delivered something concrete. Now you combine them into a single portfolio-worthy project: an e-commerce API with every production feature a professional backend developer should be able to build.
The Production Cached API isn't a step-by-step tutorial. It's clear specs and reference code. You implement each piece, integrate them into a coherent architecture, measure performance with real benchmarks, and ship it with a professional README + Docker Compose + tests. The difference between "knowing Redis" and "having a project that demonstrates command of it" is exactly this module.
If you're coming from module 4 with all the patterns fresh, this module is the natural close: the project that combined HTTP POST → Pub/Sub → WebSocket in module 4's capsule 05 was one piece; now you assemble ALL the pieces into a real production app. By the end of the module, you'll have a project on GitHub any recruiter can review — with passing tests, measurable metrics, working graceful degradation, and professional deployment with Docker Compose.
What is the Production Cached API?
It's a simulated e-commerce API with every Redis feature you've learned:
┌─────────────────────────────────────────────────────────┐
│ PRODUCTION CACHED API │
│ │
│ Endpoints: │
│ ├── GET /api/products [cache-aside] │
│ ├── GET /api/products/{id} [cache-aside] │
│ ├── POST /api/products [admin, invalidates] │
│ ├── GET /api/categories [cache-aside, long] │
│ ├── GET /api/users/{id} [cache-aside, hash] │
│ ├── PUT /api/users/{id} [write-through] │
│ ├── POST /analytics/event [write-behind] │
│ ├── POST /auth/login [JWT + session] │
│ ├── POST /auth/logout [session revoke] │
│ ├── GET /me [JWT + session check] │
│ └── WS /ws/notifications [Pub/Sub bridge] │
│ │
│ Cross-cutting: │
│ ├── Multi-tier rate limiting (free/pro/enterprise) │
│ ├── Cache invalidation events via Pub/Sub │
│ ├── Metrics: hit rate, latency, error rate │
│ └── A health check that verifies the dependencies │
│ │
│ Stack: │
│ ├── FastAPI (async) │
│ ├── PostgreSQL (mocked with SQLite or an in-memory dict)│
│ ├── Redis 7 (cache + sessions + Pub/Sub + rate limit) │
│ └── Docker Compose │
└─────────────────────────────────────────────────────────┘
The architecture
┌─────────────┐
│ Client │
└──────┬──────┘
│
│ HTTP / WebSocket
▼
┌─────────────────────────────────────────────────┐
│ The FastAPI App │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ The middleware stack (in order) │ │
│ │ 1. CORS │ │
│ │ 2. Rate Limit (sliding window) │ │
│ │ 3. Cache (auto-caches GET endpoints) │ │
│ │ 4. Logging │ │
│ └────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Routers │ │
│ │ ├─ /api/products (cache-aside) │ │
│ │ ├─ /api/users (cache-aside + WT) │ │
│ │ ├─ /analytics (write-behind) │ │
│ │ ├─ /auth (JWT + sessions) │ │
│ │ └─ /ws (a WebSocket bridge) │ │
│ └────────────────────────────────────────────┘ │
│ │ │
└──────────────────────┼────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌─────────────┐ ┌────────────┐
│PostgreSQL│ │ Redis │ │ Worker │
│ │ │ │ │ (write- │
│ Source │◄───│ - Cache │ │ behind │
│ of │ │ - Sessions │ │ flush) │
│ truth │ │ - Rate lim │ │ │
│ │ │ - Pub/Sub │ │ │
└──────────┘ └─────────────┘ └────────────┘
The flow of a GET /api/products
1. The request arrives at FastAPI
2. The CORS middleware (passes)
3. The rate limit middleware:
- Identifies the user_id from the JWT
- Checks the sliding window per user + endpoint category
- If it exceeds → 429 Too Many Requests
- If OK → continue, adding the X-RateLimit-* headers
4. The cache middleware:
- Generates a cache key from the path + query params
- Try GET cache:http:/api/products:hash
- If HIT → return the cached value (with an X-Cache: HIT header)
- If MISS → run the endpoint
5. The endpoint:
- Queries PostgreSQL (with optional internal cache-aside)
- Returns the data
6. The cache middleware stores the response (a 5 min TTL)
7. The logging middleware: logs the request
8. The client receives the response with all the headers
The flow of a POST /api/products (admin)
1. The request arrives
2. Rate limit (the admin has the "enterprise" tier)
3. The cache middleware skips it (POSTs aren't cached)
4. The endpoint:
- INSERT into PostgreSQL
- DELETE the local cache: cache:product:{id}, cache:products:list
- PUBLISH the event "cache:invalidate:product:{id}" on Redis Pub/Sub
5. Other services subscribed to the pattern receive the event and invalidate THEIR caches
6. Return the created product
The complete endpoints
Authentication
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/login | Create a session + JWT |
| POST | /auth/logout | Revoke the current session |
| POST | /auth/logout-all | Revoke all the user's sessions |
| GET | /auth/sessions | A list of active devices |
| POST | /auth/sessions/{sid}/revoke | Revoke a specific device |
The protected API
| Method | Endpoint | Pattern | Rate limit category |
|---|---|---|---|
| GET | /api/products | Cache-aside, TTL 5min | general |
| GET | /api/products/{id} | Cache-aside, TTL 10min | general |
| POST | /api/products | Invalidation + Pub/Sub | admin |
| GET | /api/categories | Cache-aside, TTL 1h | general |
| GET | /api/users/{id} | Cache-aside with a hash, TTL 30min | general |
| PUT | /api/users/{id} | Write-through | general |
| GET | /api/search | Cache the top queries, TTL 2min | search |
| POST | /api/orders | No cache (transactional) | orders |
| POST | /analytics/event | Write-behind | analytics |
System
| Method | Endpoint | Description |
|---|---|---|
| GET | /health | Status + dependencies |
| GET | /metrics | Cache hit rate, request counts, etc. |
| WS | /ws/notifications | A stream of real-time events |
Admin
| Method | Endpoint | Description |
|---|---|---|
| POST | /admin/users/{id}/revoke | Force a global logout |
| POST | /admin/cache/invalidate-all | Mass invalidation (a version bump) |
| GET | /admin/metrics/full | Detailed metrics |
Rate limits per tier
| Tier | General | Search | Orders | Analytics |
|---|---|---|---|---|
| free | 100/hr | 20/hr | 10/hr | 1000/hr |
| pro | 1000/hr | 200/hr | 100/hr | 10000/hr |
| enterprise | 10000/hr | 2000/hr | 1000/hr | 100000/hr |
Standard HTTP headers on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After (on a 429).
The caching strategy per endpoint
| Endpoint | Pattern | TTL | Invalidation | Justification |
|---|---|---|---|---|
GET /api/products | Cache-aside | 5min | DELETE on POST | Massive reads, infrequent changes |
GET /api/products/{id} | Cache-aside | 10min | DELETE + Pub/Sub | Same reasoning, a specific key |
GET /api/categories | Cache-aside | 1h | Manual | Rare changes |
GET /api/users/{id} | Cache-aside with a hash | 30min sliding | DELETE on PUT | A sliding TTL for active users |
PUT /api/users/{id} | Write-through | — | — | The UX requires immediate consistency |
GET /api/search | Cache-aside (top queries) | 2min | The natural TTL | An infinite space, only cache the common ones |
POST /api/orders | NO cache (transactional) | — | — | A critical transaction |
POST /analytics/event | Write-behind | — | — | Massive volume, losing some is fine |
Metrics to track
Cache:
- hits_total (a counter per endpoint)
- misses_total (a counter)
- hit_rate (calculated)
- latency_with_cache (a histogram, p50/p95/p99)
- latency_without_cache (a histogram)
Rate limiting:
- requests_allowed_total
- requests_rate_limited_total (429s)
- rate_limit_by_tier
Sessions:
- active_sessions_total
- sessions_created_total
- sessions_revoked_total
- logout_all_events_total
Pub/Sub:
- events_published_total
- events_received_total
System:
- redis_connected (boolean)
- redis_response_time_ms
- ws_clients_connected
The evaluation rubric (100 points)
| Category | Points | Criterion |
|---|---|---|
| Caching | 25 | The 3 patterns implemented with judgment (cache-aside, write-through, write-behind), a differentiated TTL per endpoint, correct invalidation (manual + event-driven) |
| Rate Limiting | 20 | A sliding window with sorted sets, multi-tier, standard HTTP headers, a correct 429 |
| Sessions + Auth | 15 | JWT + Redis sessions, local + global logout, list devices, a sliding TTL |
| Pub/Sub | 10 | Cache invalidation events, a working WebSocket bridge |
| Async Integration | 10 | redis.asyncio, connection pooling, dependency injection, lifespan events |
| Graceful Degradation | 10 | The app works without Redis (degraded), structured logs, no crash |
| Docker Compose | 5 | The complete stack (API + Redis + DB), docker-compose up works |
| Tests | 10 | Integration tests with pytest-asyncio, at least 10 tests passing |
| Documentation | 5 | A professional README, a caching strategy document, a complete OpenAPI |
| Total | 100 |
The levels:
- 90-100: Excellent — ready for a portfolio
- 75-89: Good — functional with minor details
- 60-74: Acceptable — it works but needs polish
- <60: Needs work — review the previous modules
Technologies
| Tool | Version | Use |
|---|---|---|
| Python | 3.10+ | The runtime |
| FastAPI | 0.109+ | The framework |
| Redis | 7+ | Cache + Sessions + Pub/Sub + Rate limit |
redis-py | >= 5.0 | The official async client (NOT aioredis) |
| PyJWT | 2.8+ | JWT signing |
| Pydantic | v2 | Validation |
| uvicorn | latest | The ASGI server |
| Docker Compose | 2.x | Local orchestration |
| pytest + httpx | latest | Tests |
requirements.txt
fastapi>=0.136
uvicorn[standard]>=0.27.0
redis>=7.4
pyjwt>=2.8.0
pydantic>=2.0
httpx>=0.26.0
pytest>=8.0.0
pytest-asyncio>=0.23.0
websockets>=12.0
python-multipart>=0.0.9
⚠️ Do NOT install aioredis — use redis.asyncio (included in redis-py >= 4.2).
The project's structure
production-cached-api/
├── .venv/
├── app/
│ ├── __init__.py
│ ├── main.py # The FastAPI app + lifespan + middleware
│ ├── config.py # Configuration + tier limits
│ ├── redis_client.py # The pool's singleton
│ ├── models.py # Pydantic models
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── jwt_handler.py
│ │ └── sessions.py
│ ├── caching/
│ │ ├── __init__.py
│ │ ├── cache_aside.py
│ │ ├── write_through.py
│ │ └── write_behind.py
│ ├── rate_limit/
│ │ ├── __init__.py
│ │ ├── sliding_window.py
│ │ └── middleware.py
│ ├── pubsub/
│ │ ├── __init__.py
│ │ ├── publisher.py
│ │ └── listener.py
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── api.py
│ │ ├── analytics.py
│ │ ├── admin.py
│ │ └── system.py
│ └── metrics/
│ ├── __init__.py
│ └── tracker.py
├── tests/
│ ├── __init__.py
│ ├── test_auth.py
│ ├── test_rate_limit.py
│ ├── test_caching.py
│ ├── test_pubsub.py
│ └── test_integration.py
├── scripts/
│ ├── seed_data.py
│ ├── benchmark.py
│ └── verify.sh
├── docker-compose.yml
├── Dockerfile
├── .env.example
├── requirements.txt
├── README.md
└── CACHING-STRATEGY.md # The strategy document (a deliverable!)
How the modules connect
An HTTP request ──────────────────────────────────────►
│ │
├── Module 4: Async + middleware ──────────────────►│
│ │
├── Module 3: Rate limit (sliding window) ─────────►│
│ │
├── Module 2: The cache check (cache-aside) ───────►│
│ │
├── Module 1: Strings/Hashes/Sorted Sets ──────────►│
│ (the fundamental data types everything uses) │
│ │
├── Module 4: A Pub/Sub broadcast on writes ───────►│
│ │
└── Module 4: WebSocket delivery to clients ────────►│
Each module contributes a piece. M5 assembles them.
How to use the next capsules
Capsules 02-04 guide you through the implementation in 3 phases:
- Capsule 02: Caching strategy design + the base architecture — an endpoint audit, the decisions, the folder structure, the models, redis_client
- Capsule 03: Implementation: caching + rate limiting — endpoints with cache-aside, the rate limit middleware, HTTP headers
- Capsule 04: Implementation: sessions + Pub/Sub + monitoring — JWT auth, the session store, invalidation events, metrics
Capsule 05 has the close: end-to-end verification, Docker Compose, a README, tests, portfolio guidance.
A recommendation: Implement each phase without copying the code first. If you get stuck for more than 30 minutes, open the corresponding capsule from the relevant module (M2 for caching, M3 for rate limiting, M4 for sessions/pubsub) and refresh the pattern. M5's capsules are a reference, not an instruction manual.
Prerequisites
- Modules 1-4 completed (with every concept fresh)
- Redis running in Docker
- Python 3.10+ with a virtual environment
redis-py >= 5.0installed- Docker Compose available (for the final deployment)
The initial setup
mkdir -p ~/projects/redis-guide/module-05-final/production-cached-api
cd ~/projects/redis-guide/module-05-final/production-cached-api
python -m venv .venv
source .venv/bin/activate
pip install fastapi "uvicorn[standard]" "redis>=7.4" pyjwt pydantic httpx websockets pytest pytest-asyncio
# The structure
mkdir -p app/auth app/caching app/rate_limit app/pubsub app/routers app/metrics tests scripts static
touch app/__init__.py app/auth/__init__.py app/caching/__init__.py
touch app/rate_limit/__init__.py app/pubsub/__init__.py app/routers/__init__.py app/metrics/__init__.py
touch tests/__init__.py
Resources
- FastAPI Tutorial — A quick reference
- Redis Best Practices for Python — The official patterns
- 12-Factor App — Principles for production-ready apps
- Awesome FastAPI — Additional resources
- Docker Compose for Python apps — The official setup
What's next?
In Capsule 02 you get into the implementation: defining the concrete caching strategy for each endpoint, creating the folder structure, configuring the redis_client.py singleton, and setting up the FastAPI scaffolding with a lifespan + middleware stack. It's the foundation on top of which M5's capsules 03 and 04 build the complete features.
If modules 1-4 are fresh and your workspace is ready, let's go.