Caching Part 2: The Landscape — Where to Put Your Cache
Caching Deep Dive(9-part series)
- P1Caching Part 1: Fundamentals — Why, When, and When Not to Cache
- P2Caching Part 2: The Landscape — Where to Put Your Cache(here)
- P3Caching Part 3: Strategies — Read-Through, Write-Through, and Beyond
- P4Caching Part 4: Eviction Policies — LRU, LFU, and Making Room

A single HTTP request can pass through 5 different caching layers before it even reaches your database. Understanding where each layer lives — and what it's good for — is the first step to using them well.
The Full Picture
Each layer caches at a different granularity. Let's walk through them.
Layer 1: Browser Cache
The closest cache to the user. The browser stores HTTP responses locally and reuses them without hitting the network at all.
Controlled by HTTP headers:
@GetMapping("/api/products/{id}")
fun getProduct(@PathVariable id: Long, response: HttpServletResponse): Product {
response.setHeader("Cache-Control", "public, max-age=3600") // Cache for 1 hour
response.setHeader("ETag", "\"${product.hashCode()}\"") // Version fingerprint
return productService.findById(id)
}Key headers to know:
| Header | What It Does |
|---|---|
Cache-Control: max-age=3600 | Cache for 3600 seconds |
Cache-Control: no-cache | Revalidate before using cache |
Cache-Control: no-store | Never cache |
ETag | Fingerprint for validation |
Last-Modified | Timestamp-based validation |
Best for: Static assets (JS, CSS, images), public API responses that don't change often. Not suitable for personalized or real-time data.
Layer 2: CDN / Edge Cache
CDNs (Cloudflare, Fastly, AWS CloudFront) cache responses at edge locations worldwide. A user in Tokyo gets served from a Tokyo edge node — not your server in Virginia.
When to use CDN caching:
- Static content (images, CSS, JS)
- Public API responses (product listings, blog posts)
- Semi-dynamic content with
stale-while-revalidate
Cache key design matters. The CDN typically uses the URL as the cache key. If your response varies by user (e.g., Accept-Language), add the Vary header:
response.setHeader("Vary", "Accept-Language, Authorization")Don't cache authenticated responses at the CDN unless you include the user identifier in the cache key. Otherwise, User A's data gets served to User B.
Layer 3: Application-Level (In-Process) Cache
An in-process cache lives inside your application's memory. No network calls, no serialization — the fastest cache option available.
Popular libraries:
@Configuration
class CacheConfig {
@Bean
fun productCache(): Cache<Long, Product> {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats()
.build()
}
}Pros: Microsecond latency, no network overhead, simple to use.
Cons: Each instance has its own cache — no sharing. If you have 10 app servers, you have 10 independent caches. This means:
- Higher total memory usage
- Potential for stale data across instances
- Cache warming needed per instance
Best for: Hot data that's read frequently, configuration data, reference data. Keep it small — this is your app's JVM heap.
Layer 4: Distributed Cache (Redis / Memcached)
A distributed cache is shared across all your application instances. Redis and Memcached are the two dominant choices.
Redis vs Memcached
| Feature | Redis | Memcached |
|---|---|---|
| Data Structures | Strings, Lists, Sets, Hashes, Sorted Sets | Strings only |
| Persistence | Optional (RDB/AOF) | None (pure in-memory) |
| Replication | Built-in (Primary/Replica) | None |
| Max Value Size | 512 MB | 1 MB |
| Thread Model | Single-threaded | Multi-threaded |
| Use When | Need persistence, complex data, HA | Simple key-value, raw speed |
// Redis with Spring Data Redis
@Service
class ProductCacheService(
private val redisTemplate: RedisTemplate<String, Product>
) {
fun getProduct(id: Long): Product? {
return redisTemplate.opsForValue().get("product:$id")
}
fun cacheProduct(product: Product) {
redisTemplate.opsForValue().set(
"product:${product.id}", product, Duration.ofHours(1)
)
}
}Best for: Shared state across instances, session storage, rate limiting, leaderboards. The backbone of most production caching strategies.
Layer 5: Database Cache
Databases have their own internal caching mechanisms:
- Buffer Pool (MySQL/PostgreSQL) — caches data pages in memory
- Query Cache — caches query result sets (MySQL, deprecated in 8.0)
- Materialized Views — pre-computed query results
- Read Replicas — serve reads from a replica to reduce primary load
-- Materialized view for expensive aggregation
CREATE MATERIALIZED VIEW order_summary AS
SELECT
customer_id,
COUNT(*) as total_orders,
SUM(amount) as total_spent
FROM orders
GROUP BY customer_id;
-- Refresh periodically
REFRESH MATERIALIZED VIEW CONCURRENTLY order_summary;Best for: Expensive aggregations, reporting queries, read-heavy workloads. You get this "for free" with most databases — tune it, don't replace it.
How Layers Work Together
A real request flows through multiple layers. Here's a typical lookup chain:
The pattern: Check fastest/closest first, fall through to slower layers, then populate back up the chain.
Choosing the Right Layer
| Scenario | Primary Cache Layer |
|---|---|
| Static assets (images, JS, CSS) | Browser + CDN |
| Public API (product catalog) | CDN + Redis |
| User-specific data (profile) | Caffeine (local) + Redis |
| Session data | Redis only |
| Expensive DB aggregations | Materialized views + Redis |
| Configuration/reference data | Caffeine (local) |
Start simple. Don't add all layers at once. Start with one layer (usually Redis), measure the improvement, then add more layers where needed.
Key Takeaways
- 5 caching layers: Browser → CDN → In-Process → Distributed (Redis) → Database
- Each layer serves a different purpose — use the right tool for the job
- Browser cache is free — set proper HTTP headers
- CDN for globally-distributed, public content
- In-process (Caffeine) for hot data with microsecond latency needs
- Redis as the backbone — shared, persistent, feature-rich
- Database caching is automatic — tune it, don't ignore it
- Check layers fastest first, populate back up the chain
Tags
Related Posts

Caching Part 3: Strategies — Read-Through, Write-Through, and Beyond
The 5 core caching patterns every backend engineer needs — when to use each one, with real trade-off analysis and Java/Kotlin examples.

Caching Part 1: Fundamentals — Why, When, and When Not to Cache
The complete guide to caching fundamentals — hit ratios, eviction basics, and the real question: when is caching actually a bad idea?

Caching Part 9: Hands-On — Code Labs and Real Implementations
Copy-paste-ready caching implementations — Spring Cache annotations, two-level caching with Caffeine + Redis, and a full Docker Compose setup.