Microservices Caching Patterns: Complete Architecture Guide
Caching in microservices is fundamentally different from monolithic applications. With dozens of services, each potentially caching data, you face challenges around consistency, coordination, and cache invalidation across service boundaries.
This guide covers proven patterns for implementing caching in microservices architectures.
The Microservices Caching Challenge
In monoliths, caching is straightforward—one application, one cache. Microservices introduce complexity:
- Data ownership: Which service owns which cached data?
- Cross-service consistency: How do you invalidate when data changes?
- Network overhead: Remote cache calls add latency
- Failure isolation: Cache failures shouldn't cascade
Pattern 1: Service-Local Caching
When to Use
Each service maintains its own cache for data it owns or frequently accesses.
// Order Service caches its own orders
class OrderService {
constructor() {
this.cache = new LocalCache({ maxSize: 10000 });
}
async getOrder(orderId) {
const cached = this.cache.get(`order:${orderId}`);
if (cached) return cached;
const order = await this.db.findOrder(orderId);
this.cache.set(`order:${orderId}`, order, { ttl: 300 });
return order;
}
}
Pros: Simple, no network calls, failure isolated
Cons: Memory per instance, no sharing between replicas
Pattern 2: Distributed Cache Layer
When to Use
Shared cache cluster (Redis, Memcached) accessible by all services.
// Shared Redis cache
const redis = new Redis(process.env.REDIS_URL);
async function getCachedUser(userId) {
const cached = await redis.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
const user = await userService.getUser(userId);
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
return user;
}
Pros: Shared across replicas, consistent view
Cons: Network latency, single point of failure if not clustered
Pattern 3: Cache-Aside with Events
When to Use
Services publish events when data changes; consuming services invalidate their caches.
// User Service publishes events
async function updateUser(userId, updates) {
await db.updateUser(userId, updates);
await cache.delete(`user:${userId}`);
// Notify other services
await eventBus.publish('user.updated', { userId, updates });
}
// Order Service subscribes
eventBus.subscribe('user.updated', async ({ userId }) => {
// Invalidate any cached user data
await cache.deletePattern(`orders:user:${userId}:*`);
});
Pros: Cross-service consistency, decoupled
Cons: Event infrastructure required, eventual consistency
Pattern 4: API Gateway Caching
When to Use
Cache responses at the API gateway level before requests reach services.
// Kong/NGINX configuration
location /api/products {
proxy_cache api_cache;
proxy_cache_valid 200 5m;
proxy_cache_key $request_uri$http_authorization;
proxy_pass http://product-service;
}
Pros: Transparent to services, reduces service load
Cons: Limited cache logic, coarse invalidation
Pattern 5: Sidecar Caching
When to Use
Deploy cache proxy as sidecar container alongside each service.
In Kubernetes, deploy a caching sidecar:
containers:
- name: order-service
image: order-service:latest
- name: cache-sidecar
image: cachee-sidecar:latest
ports:
- containerPort: 6380
Pros: Local cache access, consistent caching logic
Cons: Resource overhead per pod
Cross-Service Cache Coordination
When Service A caches data from Service B, you need coordination:
Option 1: TTL-Based Staleness
Accept that cached data may be stale for TTL duration. Simple but imprecise.
Option 2: Event-Driven Invalidation
Service B publishes change events; Service A subscribes and invalidates.
Option 3: Cache Versioning
Include version in cache keys; bump version on changes.
Handling Cache Failures
Cache failures shouldn't break your services:
async function getUserWithFallback(userId) {
try {
const cached = await cache.get(`user:${userId}`);
if (cached) return cached;
} catch (error) {
// Cache unavailable - proceed to database
logger.warn('Cache unavailable', { error });
}
// Fallback to database
return await db.getUser(userId);
}
Monitoring Distributed Caches
Track these metrics across services:
- Hit rate per service: Identify which services benefit most
- Cross-service latency: Measure cache network overhead
- Invalidation lag: Time between change and cache update
- Memory usage: Prevent cache from consuming too much RAM
Conclusion
Effective microservices caching requires choosing the right pattern for each use case. Start with service-local caching for owned data, add distributed caching for shared data, and use events for cross-service coordination.
The key principle: each service should own its caching strategy while coordinating with others through well-defined events, not direct cache manipulation.
Simplify microservices caching
Cachee.ai provides unified caching with automatic cross-service coordination.
Start Free Trial