caching
questions that
would be asked to an experienced associate (mid-to-senior level) in
microservices. These typically probe not just “what is caching,” but how you
design, implement, and troubleshoot caching end-to-end in distributed systems.
🔑 Core Interview Questions on
Microservices Caching
Conceptual & Design
- What are the different caching layers in a
microservices architecture (client, gateway, service, DB)?
Great
question 👌 — in microservices, caching is most effective when
applied in layers, each serving a different purpose. Here’s a clear
breakdown:
Caching Layers in Microservices
1. Client-side (Browser/App cache)
- What: Data cached directly on the
end user’s device (browser, mobile app).
- How: HTTP headers
(Cache-Control, ETag, Expires) or local storage.
- Use cases: Static assets (CSS, JS,
images), API responses that don’t change often.
- Benefit: Eliminates network calls,
fastest response for the user.
2. Edge/API Gateway cache
- What: Reverse proxy, CDN, or API
gateway caches responses before they hit backend services.
- How: Configured TTLs, response
revalidation, key-based caching.
- Use cases: Public APIs, read-heavy
endpoints (e.g., product catalog, search results).
- Benefit: Reduces load on
microservices, improves latency globally.
3. Service-level cache
- What: Distributed cache (Redis,
Memcached) or local in-memory cache (Caffeine, Guava).
- How: Patterns like cache-aside,
read-through, write-through, write-behind.
- Use cases: Frequently accessed domain
data (user profiles, session tokens, computed results).
- Benefit: Offloads database, speeds
up service-to-service calls.
4. Database/query cache
- What: Database engine’s internal
caching, materialized views, query result caches.
- How: DB buffer pools,
precomputed aggregates, query caching.
- Use cases: Expensive queries,
analytics, reporting.
- Benefit: Optimizes DB performance,
reduces repeated computation.
⚖️ Putting it all together
- Client cache → avoids unnecessary
requests.
- Gateway cache → reduces traffic into
microservices.
- Service cache → accelerates business
logic and cross-service calls.
- DB cache → speeds up persistence
layer.
Think of
it as a layered defense system: each layer catches requests earlier,
reducing load on deeper layers.
✅ Interview Tip: When asked this, don’t just list the
layers—explain how they interact. For example:
“A product details request may
first be served from the browser cache. If expired, the API gateway checks its
cache. If that misses, the service queries Redis. Finally, if Redis misses, the
DB query executes and result is cached back through the stack.”
Would you
like me to also prepare a diagram-style flow (Application → Gateway →
Service → DB) so you can visualize how caching works end-to-end in
microservices?

How do you decide what data to cache and what not to
cache?
Great
question, Deciding what to cache and what not to cache is one of the
most important architectural decisions in distributed systems. Let’s break it
down systematically:
🔑 Principles for Deciding What to
Cache
- Read Frequency (Hot Data)
- Cache data that is read
often but changes infrequently.
- Example: Product catalog,
country codes, configuration values.
- Computation Cost
- Cache results of expensive
computations or aggregations.
- Example: Aggregated
analytics, recommendation results.
- Latency Sensitivity
- Cache data that must be
retrieved quickly for user experience.
- Example: User session
details, authentication tokens.
- Data Volatility
- Avoid caching highly
volatile data that changes every second.
- Example: Stock prices,
real-time sensor readings.
- Instead, use streaming or
event-driven updates.
- Consistency Requirements
- If strong consistency is
required (e.g., financial transactions), caching may introduce risks.
- Cache only if you can
tolerate eventual consistency.
- Size and Memory Constraints
- Large datasets may not fit
in cache.
- Cache only subsets (e.g.,
top 100 trending items instead of all items).
⚙️ Practical Guidelines
- ✅ Cache:
- Frequently accessed
reference data (e.g., country list, currency codes).
- Session tokens or user
preferences.
- Results of expensive DB
queries or API calls.
- Static content (images,
CSS, JS).
- ❌ Do Not Cache:
- Highly dynamic data (e.g.,
live auction bids).
- Sensitive data requiring
strict consistency (e.g., account balances).
- Rarely accessed data
(wastes memory).
- Data with unpredictable
size (risk of cache overflow).
📊 Example in Java (Spring Boot +
Redis)
@Service
public class CountryService {
@Autowired
private RedisTemplate<String,
Country> redisTemplate;
@Autowired
private CountryRepository
countryRepository;
public Country getCountry(String code) {
// Try cache first
Country country =
redisTemplate.opsForValue().get(code);
if (country != null) {
return country;
}
// Fallback to DB
country =
countryRepository.findByCode(code);
// Cache if found
if (country != null) {
redisTemplate.opsForValue().set(code, country);
}
return country;
}
}
Here:
- Country data is cached
because it’s relatively static and frequently accessed.
- If data changes, you can set
a TTL (time-to-live) or use cache invalidation strategies.
✅ Best Practices
- Use TTL to avoid
stale data.
- Apply cache-aside pattern
(read-through, write-through depending on use case).
- Monitor cache hit ratio
to ensure caching is effective.
- Always design a fallback
path (DB or API) if cache misses occur.
👉 In short:
- Cache hot, stable, and
expensive-to-fetch data.
- Avoid caching volatile,
sensitive, or rarely used data.
Explain cache-aside vs. read-through vs. write-through
vs. write-behind strategies.
Quick
Answer:
Cache-aside means the application manages the cache explicitly, read-through
lets the cache fetch missing data automatically, write-through synchronously
writes to both cache and database, and write-behind writes to the cache first
and asynchronously updates the database later.
🗂 Detailed Explanation of Caching
Strategies
1. Cache-Aside (Lazy Loading)
- How it works:
- The application checks the
cache first.
- If data is missing, it
loads from the database and then populates the cache.
- Writes go directly to the
database, and the cache is updated or invalidated manually.
- Pros:
- Simple to implement.
- Cache only stores
frequently accessed data.
- Cons:
- First read after a cache
miss is slow.
- Risk of stale data if cache
invalidation isn’t handled properly.
- Use case:
- Systems where reads
dominate writes, e.g., product catalog lookups.
2. Read-Through
- How it works:
- The application always
interacts with the cache.
- If data is missing, the
cache itself fetches from the database and stores it.
- Pros:
- Transparent to the
application (simplifies code).
- Ensures cache consistency
on reads.
- Cons:
- Cache provider must support
this pattern.
- Still suffers from slow
first read on a miss.
- Use case:
- Applications using managed
caching solutions (like Redis with read-through plugins).
3. Write-Through
- How it works:
- Every write goes through
the cache.
- Cache synchronously writes
to the database as part of the operation.
- Pros:
- Cache and database are always
consistent.
- Simplifies read logic since
cache is always up-to-date.
- Cons:
- Slower writes (because both
cache and DB are updated).
- Cache may store data that
is rarely read.
- Use case:
- Systems needing strong
consistency, e.g., financial transactions.
4. Write-Behind (Write-Back)
- How it works:
- Data is written to the
cache first.
- Cache asynchronously writes
changes to the database later.
- Pros:
- Fast writes (low latency).
- Can batch updates to reduce
DB load.
- Cons:
- Risk of data loss if cache
fails before flushing.
- Database may lag behind
cache (eventual consistency).
- Use case:
- High-write systems where
performance is critical, e.g., logging or analytics.
📊 Comparison Table
|
Strategy |
Read Path |
Write Path |
Consistency |
Performance |
Best For |
|
Cache-Aside |
App → Cache
→ DB |
App →
DB (+cache update) |
Risk of
stale cache |
Good
for reads |
Read-heavy
workloads |
|
Read-Through |
App →
Cache (cache fetches DB if miss) |
App →
DB (+cache update optional) |
Consistent
reads |
Simplifies
app logic |
Managed
caching |
|
Write-Through |
App → Cache
→ DB |
App →
Cache → DB |
Strong
consistency |
Slower
writes |
Financial/critical
systems |
|
Write-Behind |
App →
Cache |
Cache →
DB (async) |
Eventual
consistency |
Fast
writes |
High-write,
performance-focused |
In short:
- Cache-aside gives you control but
requires careful invalidation.
- Read-through simplifies reads by letting
the cache fetch data.
- Write-through ensures strong consistency
but slows writes.
- Write-behind boosts performance but
trades off immediate consistency.
- How do
you handle cache invalidation
when data changes across multiple services?
- What’s the difference between local in-memory cache (e.g.,
Guava, Caffeine) and distributed
cache (e.g., Redis, Memcached)? When would you use each?
Great
question — this is a fundamental design choice in microservice and cloud-native
architectures. Let’s break it down clearly:
🧠 Local In-Memory Cache (Guava, Caffeine,
Ehcache)
- Where it lives: Inside the application
process (JVM memory).
- Scope: Only available to that
single service instance.
- Latency: Extremely fast (nanoseconds
to microseconds) since it’s just memory access.
- Consistency: Each instance has its own
cache; no automatic synchronization across services.
- Examples: Guava Cache, Caffeine,
Ehcache (when embedded).
Pros:
- Ultra-low latency.
- Simple to set up (no
external infra).
- Great for per-instance hot
data (e.g., config, small lookup tables).
Cons:
- Not shared across multiple
service instances.
- Risk of stale or
inconsistent data in distributed systems.
- Limited by JVM heap size.
Use
cases:
- Single-instance apps or
monoliths.
- Microservices where cached
data is instance-local (e.g., rate-limiting counters, small
reference data).
- Situations where consistency
across nodes isn’t critical.
🌐 Distributed Cache (Redis,
Memcached, Hazelcast)
- Where it lives: External cache cluster
accessible over the network.
- Scope: Shared across multiple
service instances.
- Latency: Higher than local
(microseconds to milliseconds) due to network hop.
- Consistency: Centralized store ensures
all services see the same cached data.
- Examples: Redis, Memcached,
Hazelcast, Apache Ignite.
Pros:
- Shared across services →
consistent view of cached data.
- Can scale horizontally
(clustered Redis, Memcached).
- Supports advanced features
(pub/sub, persistence, eviction policies).
Cons:
- Network overhead compared to
local cache.
- Requires external
infrastructure and monitoring.
- Potential bottleneck if not
scaled properly.
Use
cases:
- Microservices needing shared
state (e.g., session management, distributed locks).
- High-scale apps where
multiple nodes must see the same cached data.
- Scenarios requiring
persistence or replication (e.g., Redis with RDB/AOF).
📊 Comparison Table
|
Feature |
Local In-Memory (Guava, Caffeine) |
Distributed (Redis, Memcached) |
|
Location |
Inside
JVM process |
External
cluster (network) |
|
Scope |
Per-instance
only |
Shared
across services |
|
Latency |
Nanoseconds–µs |
µs–ms
(network hop) |
|
Consistency |
No
cross-instance sync |
Consistent
across nodes |
|
Scalability |
Limited
by JVM heap |
Horizontally
scalable cluster |
|
Complexity |
Very
simple |
Requires
infra + ops |
|
Best
For |
Local
lookups, configs, rate limits |
Shared
sessions, global data, distributed systems |
⚡ Practical Guidance for You (Java
+ Spring Boot Microservices)
- Use Caffeine/Guava
for per-instance hot data like method-level caching (@Cacheable
with local provider).
- Use Redis for cross-service
consistency — e.g., user sessions, product catalog, or shared rate
limits.
- Hybrid approach is common:
- Local cache for ultra-fast reads of
frequently accessed data.
- Distributed cache for consistency across
services.
👉 since you’re building Spring Boot microservices
with DDD + EDA, a two-tier caching strategy often works best:
- Caffeine for local method-level
caching.
- Redis for distributed,
event-driven invalidation across services.
Would you
like me to sketch out a Spring Boot configuration example showing how to
combine Caffeine (local) and Redis (distributed) in the same
service? That’s a very common production-ready pattern.
Working Example:
Perfect —
let’s sketch out a Spring Boot configuration that combines Caffeine
(local in-memory cache) and Redis (distributed cache) in the same
microservice. This is often called a two-tier caching strategy:
- Tier 1 (Caffeine): Ultra-fast, per-instance
cache for hot data.
- Tier 2 (Redis): Shared distributed cache
for consistency across services.
⚙️ Step 1: Add Dependencies (Maven)
<dependencies>
<!-- Spring Cache abstraction -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
⚙️ Step 2: Configure Cache Managers
import
com.github.benmanes.caffeine.cache.Caffeine;
import
org.springframework.cache.CacheManager;
import
org.springframework.cache.annotation.EnableCaching;
import
org.springframework.cache.caffeine.CaffeineCacheManager;
import
org.springframework.cache.redis.RedisCacheManager;
import
org.springframework.context.annotation.Bean;
import
org.springframework.context.annotation.Configuration;
import
org.springframework.data.redis.connection.RedisConnectionFactory;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
// Local Caffeine cache
@Bean
public CacheManager caffeineCacheManager()
{
CaffeineCacheManager cacheManager
= new CaffeineCacheManager("localCache");
cacheManager.setCaffeine(
Caffeine.newBuilder()
.expireAfterWrite(10,
TimeUnit.MINUTES)
.maximumSize(1000)
);
return cacheManager;
}
// Distributed Redis cache
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory
connectionFactory) {
return
RedisCacheManager.builder(connectionFactory)
.build();
}
}
⚙️ Step 3: Use Composite Cache
Manager
Spring
allows you to combine multiple cache managers. The composite will check Caffeine first,
then fall back to Redis.
import
org.springframework.cache.support.CompositeCacheManager;
@Bean
public CacheManager
cacheManager(CacheManager caffeineCacheManager,
CacheManager
redisCacheManager) {
CompositeCacheManager compositeCacheManager
=
new
CompositeCacheManager(caffeineCacheManager, redisCacheManager);
compositeCacheManager.setFallbackToNoOpCache(false);
return compositeCacheManager;
}
⚙️ Step 4: Usage in Service Layer
import
org.springframework.cache.annotation.Cacheable;
import
org.springframework.stereotype.Service;
@Service
public class ProductService {
@Cacheable(cacheNames = {"localCache",
"redisCache"})
public Product getProductById(String id) {
// Simulate DB call
System.out.println("Fetching from
DB for id: " + id);
return new Product(id,
"Laptop", 75000);
}
}
- First call → Misses both
caches → Hits DB → Stores in Caffeine and Redis.
- Subsequent calls → Served
from Caffeine (fastest).
- If local cache expires →
Falls back to Redis (shared).
📊 Flow Diagram
Request → Caffeine (local JVM
cache)
↳ Hit → Return immediately
↳ Miss → Redis (distributed cache)
↳ Hit →
Return + populate Caffeine
↳ Miss →
DB → Populate Redis + Caffeine
✅ When to Use This Hybrid Approach
- High-read workloads where latency matters
(Caffeine handles hot data).
- Multi-instance microservices where consistency matters
(Redis ensures shared state).
- Event-driven invalidation can be added: publish cache
invalidation events to Kafka so all services evict/update Redis entries.
👉 Brijendra, this pattern is production-ready for
Spring Boot microservices. It gives you speed + consistency without
overloading Redis.
Would you
like me to extend this sketch to show event-driven invalidation with Kafka
so multiple services can stay in sync when data changes? That would tie
directly into your DDD + EDA architecture.
Implementation & Patterns
- How would you implement caching in a Spring
Boot microservice using Redis?
Great
question — let’s walk through how to implement Redis caching in a Spring
Boot microservice step by step. This is the most common production-ready
approach when you want distributed, consistent caching across multiple
service instances.
⚙️ Step 1: Add Dependencies (Maven)
<dependencies>
<!-- Spring Cache abstraction -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis integration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Optional: for Redis client -->
<dependency>
<groupId>io.lettuce.core</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
</dependencies>
⚙️ Step 2: Configure Redis in
application.yml
spring:
cache:
type: redis
redis:
host: localhost
port: 6379
# optional settings
timeout: 60000
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
⚙️ Step 3: Enable Caching in Spring
Boot
import
org.springframework.cache.annotation.EnableCaching;
import
org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CacheConfig {
// Spring Boot auto-configures
RedisCacheManager
}
⚙️ Step 4: Use @Cacheable,
@CachePut, @CacheEvict
import
org.springframework.cache.annotation.Cacheable;
import
org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import
org.springframework.stereotype.Service;
@Service
public class ProductService {
// Read-through caching
@Cacheable(value = "products",
key = "#id")
public Product getProductById(String id) {
System.out.println("Fetching from
DB for id: " + id);
return new Product(id,
"Laptop", 75000);
}
// Update cache when DB is updated
@CachePut(value = "products", key
= "#product.id")
public Product updateProduct(Product product)
{
System.out.println("Updating DB
for id: " + product.getId());
return product;
}
// Evict cache entry when product is
deleted
@CacheEvict(value = "products",
key = "#id")
public void deleteProduct(String id) {
System.out.println("Deleting from DB
for id: " + id);
}
}
⚙️ Step 5: Redis Cache Behavior
- First read: Cache miss → fetch from DB
→ store in Redis.
- Subsequent reads: Served directly from Redis
(fast).
- Updates: @CachePut ensures Redis is
updated.
- Deletes: @CacheEvict removes stale
entries.
📊 Advantages of Redis Caching in
Microservices
- Shared across instances → consistent data view.
- Supports TTL/expiry → automatic invalidation.
- Scalable → cluster mode for high
availability.
- Advanced features → pub/sub, persistence,
distributed locks.
⚡ Practical Tip for You
Since
you’re working with Spring Boot microservices + DDD/EDA, you can combine
Redis caching with Kafka events for invalidation across services. For
example:
- Service A updates a product
→ publishes ProductUpdatedEvent.
- Other services consume the
event → evict/update Redis cache entries.
This
ensures event-driven cache consistency across your microservice
ecosystem.
👉 Would you like me to extend this with a Kafka +
Redis integration example showing how cache invalidation can be
event-driven across multiple services? That would tie directly into your
architecture style.
Got it — let’s build you a concise
interview-ready cheat sheet for caching in microservices with Spring
Boot + Redis (and related caching strategies). This will give you crisp,
bullet-point answers you can deliver confidently in interviews.
📝 Caching Interview
Cheat Sheet
🔹 Cache Strategies
- Cache-Aside (Lazy Loading): App loads
from DB → populates cache → future reads hit cache.
- Read-Through: App reads
from cache → cache fetches DB if miss.
- Write-Through: Writes go to
cache → cache synchronously writes to DB.
- Write-Behind: Writes go to
cache → DB updated asynchronously later.
🔹 Local vs Distributed Cache
- Local (Caffeine/Guava):
- Per-instance,
fastest (in-memory).
- No
cross-service consistency.
- Best for
small, hot data (configs, rate limits).
- Distributed (Redis/Memcached):
- Shared
across services.
- Ensures
consistency.
- Best for
sessions, shared state, product catalogs.
🔹 Cache Invalidation Across Services
- Event-driven: Publish
domain events (Kafka/RabbitMQ) → other services evict/update cache.
- Centralized cache: Use
Redis/Hazelcast cluster → all services share same cache.
- TTL expiry: Simple
fallback, avoids stale data.
- Versioning: Use
timestamps/versions to detect stale entries.
- API-based: Services expose cache
invalidation endpoints.
🔹 Spring Boot + Redis Implementation
- Enable caching:
@EnableCachingin config. - Configure Redis: via
application.yml(spring.cache.type=redis). - Use annotations:
@Cacheable→ read-through.@CachePut→ update cache on write.@CacheEvict→ remove cache on delete.- Composite caching: Combine
Caffeine (local) + Redis (distributed) with
CompositeCacheManager.
🔹 Best Practices
- Set TTLs: Prevent stale data.
- Use proper keys: Include IDs,
tenant info, etc.
- Monitor cache hit/miss ratio: Optimize
usage.
- Avoid over-caching: Cache only
frequently accessed data.
- Event-driven invalidation: Keep
distributed caches consistent.
🔹 Sample Interview Responses (Quick Fire)
- “Cache-aside is simplest: app loads from DB and
populates cache. Read-through lets cache fetch DB automatically.
Write-through ensures consistency but slows writes. Write-behind improves
performance but risks eventual consistency.”
- “Local caches like Caffeine are fastest but
per-instance. Distributed caches like Redis ensure consistency across
services.”
- “For invalidation, I prefer event-driven
approaches with Kafka so all services evict/update Redis entries in real
time.”
- “In Spring Boot, I’d enable caching, configure
Redis, and use
@Cacheable,@CachePut, and@CacheEvictto manage lifecycle.”
📝 Caching Interview
Cheat Sheet
🔹 Cache Strategies
- Cache-Aside (Lazy Loading): App loads
from DB → populates cache → future reads hit cache.
- Read-Through: App reads
from cache → cache fetches DB if miss.
- Write-Through: Writes go to
cache → cache synchronously writes to DB.
- Write-Behind: Writes go to
cache → DB updated asynchronously later.
🔹 Local vs Distributed Cache
- Local (Caffeine/Guava):
- Per-instance,
fastest (in-memory).
- No
cross-service consistency.
- Best for
small, hot data (configs, rate limits).
- Distributed (Redis/Memcached):
- Shared
across services.
- Ensures
consistency.
- Best for
sessions, shared state, product catalogs.
🔹 Cache Invalidation Across Services
- Event-driven: Publish
domain events (Kafka/RabbitMQ) → other services evict/update cache.
- Centralized cache: Use
Redis/Hazelcast cluster → all services share same cache.
- TTL expiry: Simple
fallback, avoids stale data.
- Versioning: Use
timestamps/versions to detect stale entries.
- API-based: Services expose cache
invalidation endpoints.
🔹 Spring Boot + Redis Implementation
- Enable caching:
@EnableCachingin config. - Configure Redis: via
application.yml(spring.cache.type=redis). - Use annotations:
@Cacheable→ read-through.@CachePut→ update cache on write.@CacheEvict→ remove cache on delete.- Composite caching: Combine
Caffeine (local) + Redis (distributed) with
CompositeCacheManager.
🔹 Best Practices
- Set TTLs: Prevent stale data.
- Use proper keys: Include IDs,
tenant info, etc.
- Monitor cache hit/miss ratio: Optimize
usage.
- Avoid over-caching: Cache only
frequently accessed data.
- Event-driven invalidation: Keep
distributed caches consistent.
🔹 Sample Interview Responses (Quick Fire)
- “Cache-aside is simplest: app loads from DB and
populates cache. Read-through lets cache fetch DB automatically.
Write-through ensures consistency but slows writes. Write-behind improves
performance but risks eventual consistency.”
- “Local caches like Caffeine are fastest but
per-instance. Distributed caches like Redis ensure consistency across
services.”
- “For invalidation, I prefer event-driven approaches
with Kafka so all services evict/update Redis entries in real time.”
- “In Spring Boot, I’d enable caching, configure
Redis, and use
@Cacheable,@CachePut, and@CacheEvictto manage lifecycle.”
- How do you ensure idempotency
and avoid stale data when caching API responses?
Excellent
— this is a classic interview question that blends caching with
distributed systems design. Let’s break it down into crisp, interview-ready
points:
🧩 Ensuring Idempotency in Cached API
Responses
- Idempotency definition: Multiple identical requests
should produce the same effect/result without unintended side effects.
- How to achieve:
- Stable cache keys: Use deterministic keys
(e.g., GET /users/{id} → user:{id}) so repeated requests map to the same
entry.
- Immutable responses: Cache only data that
doesn’t change per request (avoid caching with random tokens or
timestamps unless normalized).
- Safe HTTP methods: Cache GET responses; avoid
caching POST/PUT unless explicitly idempotent.
- Versioning: Include resource version
or ETag in cache key to ensure correctness.
🧩 Avoiding Stale Data
- TTL (Time-to-Live): Set expiry times to
automatically evict old entries.
- Cache invalidation
strategies:
- Event-driven invalidation: Publish domain events
(e.g., UserUpdatedEvent) → evict/update cache in all services.
- Write-through / Cache
update:
Update cache immediately when DB changes.
- Cache eviction (@CacheEvict
in Spring):
Remove stale entries on update/delete.
- Version checks: Store version/timestamp
with cached data; reject stale entries if DB version is newer.
- Background refresh: Use async refresh
(refresh-ahead) to update cache before expiry.
📊 Practical Example (Spring Boot +
Redis)
@Cacheable(value =
"users", key = "#id")
public User getUserById(String
id) {
// Fetch from DB if not cached
}
@CachePut(value =
"users", key = "#user.id")
public User updateUser(User user)
{
// Update DB and refresh cache
}
@CacheEvict(value =
"users", key = "#id")
public void deleteUser(String id)
{
// Remove from DB and evict cache
}
- Idempotency: Same id → same cache key →
same response.
- Avoid stale data: @CachePut and @CacheEvict
keep cache aligned with DB.
✅ Interview-Ready Bullet Points
- Use deterministic cache
keys to ensure idempotency.
- Cache only safe,
idempotent operations (GET).
- Apply TTL + event-driven
invalidation to avoid stale data.
- Use versioning/ETags
for correctness.
- In Spring Boot, combine @Cacheable, @CachePut, and
@CacheEvict with Redis for lifecycle management.
- How do you design cache
keys in a multi-tenant system?
This is a
great interview-style question — cache key design in multi-tenant systems
is critical to avoid data leaks and stale data. Let’s break it down into crisp,
structured points you can use directly in interviews:
🧩 Principles of Cache Key Design in
Multi-Tenant Systems
1. Tenant Isolation
- Always include tenant
identifier (e.g., tenantId) in the cache key.
- Prevents one tenant’s data
from being served to another.
- Example:
tenant:123:user:456.
2. Stable and Deterministic Keys
- Keys must be consistent
for the same resource.
- Avoid randomness unless part
of versioning.
- Example: tenant:{tenantId}:product:{productId}.
3. Namespace Segmentation
- Use prefixes to
separate domains/entities.
- Example:
tenant:{tenantId}:orders:{orderId} vs tenant:{tenantId}:users:{userId}.
- Helps with bulk eviction
(e.g., evict all orders for a tenant).
4. Versioning / ETags
- Add version numbers
or timestamps to keys to avoid stale data.
- Example:
tenant:{tenantId}:user:{userId}:v2.
- When data changes, bump
version → ensures fresh cache.
5. Composite Keys
- Combine multiple dimensions
(tenant, entity, filters).
- Example:
tenant:{tenantId}:report:{reportId}:date:{yyyyMMdd}.
- Supports fine-grained
caching for queries.
6. TTL and Eviction
- Apply per-tenant TTLs
to balance freshness vs performance.
- Optionally, use bulk
eviction by tenant prefix when tenant data changes massively.
7. Security Considerations
- Never expose raw tenant IDs
in public APIs → use hashed/encoded keys internally.
- Prevents cache key guessing
attacks.
📊 Example Cache Key Patterns
|
Use Case |
Cache Key Example |
|
User
profile |
tenant:42:user:1001 |
|
Product
catalog |
tenant:42:product:sku:ABC123 |
|
Report
query |
tenant:42:report:monthly:2025-11 |
|
Versioned
entity |
tenant:42:user:1001:v3 |
✅ Interview-Ready Bullet Points
- Always include tenant ID
in cache keys to ensure isolation.
- Use namespaces/prefixes
for entities to simplify eviction.
- Add version/timestamp
to avoid stale data.
- Design composite keys
for queries with filters.
- Apply TTL + bulk eviction
strategies per tenant.
- Ensure security by
hashing tenant IDs if exposed externally.
- What’s your approach to TTL (time-to-live) and refresh-ahead strategies?
Great —
TTL and refresh-ahead are two key levers for balancing performance vs.
freshness in caching. Here’s a crisp, interview-ready breakdown:
🧩 TTL (Time-to-Live)
- Definition: Expiry time after which
cached data is automatically invalidated.
- Approach:
- Short TTL: Ensures freshness but
increases DB load.
- Long TTL: Improves performance but
risks stale data.
- Dynamic TTL: Adjust TTL based on data
volatility (e.g., 5 min for stock prices, 24 hrs for product catalog).
- Fallback: Use TTL as a safety net
even if you have event-driven invalidation.
Best
practice:
- Align TTL with business
requirements (e.g., financial data → seconds; static config → hours).
- Monitor hit/miss ratio and
adjust TTL accordingly.
🧩 Refresh-Ahead (Cache Warming)
- Definition: Cache proactively refreshes
entries before they expire, so users don’t experience a cache miss.
- Approach:
- Background refresh: When TTL is near expiry,
trigger async refresh.
- Refresh-ahead window: Define a threshold (e.g.,
refresh if <10% TTL left).
- Batch refresh: For heavy queries, refresh
in bulk to avoid thundering herd.
- Async tasks: Use scheduled jobs or
thread pools to refresh without blocking user requests.
Best
practice:
- Use refresh-ahead for high-read,
latency-sensitive workloads (e.g., dashboards, product listings).
- Combine with event-driven
updates to keep cache aligned with DB changes.
- Avoid over-refreshing →
monitor usage patterns to refresh only hot keys.
📊 Comparison
|
Strategy |
Pros |
Cons |
Best Use Case |
|
TTL |
Simple,
automatic eviction |
Risk of
stale data until expiry |
General
caching, fallback safety |
|
Refresh-Ahead |
Prevents
cache misses, smooth UX |
More
infra complexity, risk of wasted refreshes |
High-read,
latency-sensitive workloads |
✅ Interview-Ready Bullet Points
- “I set TTLs based on data
volatility — short for fast-changing data, long for static data.”
- “I combine TTL with
event-driven invalidation to avoid stale data.”
- “For high-read workloads, I
use refresh-ahead so users don’t hit cache misses.”
- “I monitor hit/miss ratios
and adjust TTL + refresh windows dynamically.”
- How do you use ETag/Cache-Control headers for client-side caching in REST
APIs?
This is a
classic REST API interview question — ETag and Cache-Control are HTTP
mechanisms that help clients cache responses efficiently and avoid stale data.
Here’s a crisp, structured answer you can use:
🧩 ETag (Entity Tag)
- Definition: A unique identifier (hash,
version, timestamp) representing the current state of a resource.
- Usage:
- Server includes ETag in
response headers:
o
ETag:
"v2-user-12345"
- Client stores the ETag with
the cached response.
- On subsequent requests,
client sends If-None-Match header with the ETag:
o
If-None-Match:
"v2-user-12345"
- Server compares ETag:
- If unchanged → returns 304
Not Modified (client uses cached copy).
- If changed → returns new
resource with updated ETag.
- Benefit: Prevents stale data,
ensures idempotency, reduces bandwidth.
🧩 Cache-Control
- Definition: HTTP header that tells
clients and proxies how to cache responses.
- Usage Examples:
- Cache-Control: max-age=3600
→ Cache for 1 hour.
- Cache-Control: no-cache →
Must revalidate with server before using cached copy.
- Cache-Control: no-store →
Do not cache at all (sensitive data).
- Cache-Control: public →
Cacheable by browsers and proxies.
- Cache-Control: private →
Cacheable only by the client.
- Benefit: Controls freshness and
caching behavior explicitly.
📊 How They Work Together
- ETag: Validates whether cached data
is still fresh.
- Cache-Control: Defines how long cached
data can be reused before revalidation.
- Combined Example:
·
Cache-Control:
max-age=600, must-revalidate
·
ETag:
"product-v5"
- Client caches response for
10 minutes.
- After 10 minutes, client
revalidates with ETag.
- If unchanged → 304 Not
Modified.
- If changed → new response
returned.
✅ Interview-Ready Bullet Points
- “ETag is a version identifier for a resource; clients use it with
If-None-Match to avoid downloading unchanged data.”
- “Cache-Control defines caching rules like max-age, no-cache, or
no-store.”
- “Together, they reduce bandwidth, improve performance, and prevent
stale data.”
- “I use ETag for validation and Cache-Control for freshness policies
in REST APIs.”
Scalability & Consistency
- How do you maintain cache
consistency across multiple microservices?
Structuring
the answer
Okay, so
I need to keep the answer well-structured according to the rules. I won’t need
citations here since no search results are involved. I'll keep the response
concise, limiting it to six sections at most. I'll use bold labels for the
bullets to make each section easier to follow. I'm aiming for content that’s
interview-ready and relevant to Brijendra. I’ll focus on practical Spring Boot
steps, common patterns, pitfalls to avoid, and monitoring tips. It’s about
providing short, actionable advice without being repetitive.
Quick answer
Maintain
cache consistency by combining event-driven invalidation (Kafka/RabbitMQ), a
shared distributed cache (Redis/Hazelcast), versioned keys, and sensible TTLs.
Use write-through for strong consistency and write-behind only when eventual
consistency is acceptable. Add idempotent consumers, retries, and monitoring
for cache hit/miss and lag.
Core strategies
- Event-driven invalidation:
Publish domain events (e.g., OrderUpdated, UserProfileChanged) on writes; subscribers evict or refresh affected keys. Use idempotent handlers, exactly-once-or-at-least-once semantics, and deduplication to avoid double-evictions. - Centralized distributed
cache:
Redis/Hazelcast as the shared cache for all services. Combine with Redis Pub/Sub or streams to broadcast invalidations and reduce per-service coupling. - Write-through /
read-through:
Write-through updates DB and cache atomically; read-through populates cache on misses via a loader, reducing divergence. - TTL + refresh-ahead:
Short, data-aware TTLs as a safety net. Refresh-ahead for hot keys to preempt cache misses and smooth latency. - Versioned keys / ETags:
Embed version/timestamp (e.g., user:{tenantId}:{id}:v{version}) so updates naturally invalidate by changing the key. Clients can use ETag for revalidation. - Invalidation APIs
(fallback):
Targeted endpoints for rare bulk operations (e.g., tenant-wide evictions), guarded and rate-limited.
Design patterns that help
- Outbox + change data
capture:
Outbox table in the write service; a relay publishes events reliably after DB commit. Prevents missed invalidations. - Saga/transaction boundaries:
Emit events only after successful commit; consumers treat events as the source of invalidation truth. - Hierarchical namespaces:
Keys like tenant:{t}:product:{sku} enable precise or bulk eviction (tenant:{t}:*), simplifying operations. - Two-tier caching:
Caffeine (local) for hot reads + Redis (distributed) for shared state. On local miss, hit Redis; on Redis miss, go to DB and backfill both.
Practical steps in Spring Boot + Redis + Kafka
- Consistent keying:
Prefix keys with tenant/domain (tenant:{t}:orders:{id}) and include version when applicable. - Annotations and policy:
Use @Cacheable for reads, @CachePut on updates, and @CacheEvict on deletes. Pair with RedisCacheManager configured with TTLs per cache. - Event pipeline:
On write: persist → publish event (Kafka) with identifiers/versions → subscribers evict or refresh Redis entries. Handlers must be idempotent, with retry/backoff. - Atomicity:
For counters or aggregates, use Redis atomic ops (Lua scripts, transactions) to avoid race-induced inconsistency. - Observability:
Emit metrics: cache hit/miss, eviction count, event lag, consumer failures. Alert on rising miss ratios or lag spikes.
Pitfalls to avoid
- Thundering herd on eviction:
Use staggered refresh, request coalescing, or single-flight per key to prevent DB storms. - Partial failures:
If DB commit succeeds but event publish fails, the cache diverges. Use outbox and reconciliation jobs. - Overly long TTLs:
Leads to stale data. Prefer shorter TTLs with event-driven invalidation over “set-and-forget.” - Global cache wipes:
Avoid broad FLUSHALL. Favor namespace-targeted evictions to reduce collateral impact.
Interview-ready bullets
- Primary approach: Event-driven invalidation
with a shared Redis cache, versioned keys, and short TTLs as
a safety net.
- Strong consistency: Prefer write-through
and transactional boundaries; publish events post-commit via outbox.
- Operational resilience: Idempotent consumers,
retries/backoff, monitor lag + hit/miss, and regular reconciliation
for drift.
- Performance balance: Two-tier caching (Caffeine
+ Redis) with refresh-ahead to keep latency low without sacrificing
correctness.
If you
want, I can map this to a concrete Spring Boot snippet showing the Kafka event,
Redis eviction, and versioned key strategy aligned to a DDD aggregate (e.g.,
Product).
- How do you handle distributed cache eviction when one service updates data?
This is a
core distributed systems interview question — cache eviction across
services is tricky because you need to keep all caches aligned when one service
updates the source of truth. Here’s a structured answer you can use:
🧩 Key Approaches to Distributed Cache
Eviction
1. Event-Driven Invalidation (Best Practice)
- When Service A updates data
in the DB, it publishes a domain event (e.g., UserUpdatedEvent,
OrderCancelledEvent) to a broker like Kafka/RabbitMQ.
- Other services subscribe and
evict or refresh their cache entries.
- ✅ Near real-time consistency.
- ⚠️ Requires reliable event delivery and
idempotent consumers.
2. Centralized Distributed Cache
- Use a shared cache
cluster (Redis, Hazelcast, Memcached).
- All services read/write from
the same cache.
- Eviction is global —
when one service evicts, all others see the change.
- ✅ Simplifies consistency.
- ⚠️ Can become a bottleneck if not scaled
properly.
3. Write-Through / Write-Behind
- Write-through: Service updates both DB and
cache synchronously.
- Write-behind: Service updates cache
first, DB asynchronously later.
- ✅ Keeps cache aligned with DB.
- ⚠️ Write-behind risks temporary inconsistency.
4. TTL (Time-to-Live) Expiry
- Cached entries expire
automatically after a set time.
- ✅ Simple fallback mechanism.
- ⚠️ Risk of stale data until expiry.
5. Versioned Keys
- Include version/timestamp
in cache keys (e.g., user:123:v2).
- When data changes, bump
version → old entries naturally invalidated.
- ✅ Prevents stale reads.
- ⚠️ Requires consistent version management.
📊 Comparison
|
Approach |
Consistency |
Complexity |
Best Use Case |
|
Event-driven |
High |
Medium |
Microservices
with Kafka/RabbitMQ |
|
Centralized
cache |
High |
Low |
Shared
Redis/Hazelcast clusters |
|
TTL
expiry |
Low-Medium |
Low |
Non-critical
data |
|
Write-through/behind |
High/Medium |
Medium |
Financial/critical
systems |
|
Versioned
keys |
High |
High |
Strong
consistency needs |
✅ Interview-Ready Bullet Points
- “When one service updates
data, I use event-driven invalidation — publish an event, other services
evict/update their cache.”
- “If all services share
Redis, eviction is global and automatic.”
- “I combine TTLs as a safety
net with event-driven invalidation for freshness.”
- “Versioned cache keys ensure
correctness when data changes.”
- “Consistency strategy
depends on workload: financial systems need write-through, catalogs can
tolerate TTL.”
- What’s the trade-off between
strong consistency vs. eventual consistency in caching?
This is a
classic systems design trade-off question — interviewers want to see
that you understand the balance between performance, scalability, and
correctness. Here’s a crisp, structured answer you can use:
🧩 Strong Consistency in Caching
- Definition: Cache and database are
always in sync; reads always return the latest committed value.
- How it’s achieved:
- Write-through caching
(update DB + cache together).
- Distributed locks or
consensus protocols (e.g., Raft, Paxos).
- Pros:
- No stale data.
- Predictable correctness
(important in financial or mission-critical systems).
- Cons:
- Higher latency (extra
writes, coordination overhead).
- Reduced availability if
cache or DB is slow.
- Harder to scale
horizontally.
🧩 Eventual Consistency in Caching
- Definition: Cache may temporarily serve
stale data, but will converge to the correct value over time.
- How it’s achieved:
- Cache-aside with TTLs.
- Write-behind caching (async
DB updates).
- Event-driven invalidation
(Kafka/RabbitMQ).
- Pros:
- Faster writes (low
latency).
- Higher availability and
scalability.
- Works well for read-heavy
workloads.
- Cons:
- Risk of stale reads.
- Clients may see different
values at different times.
- Requires careful design to
avoid user confusion.
📊 Trade-Off Summary
|
Aspect |
Strong Consistency |
Eventual Consistency |
|
Freshness |
Always
up-to-date |
May
serve stale data temporarily |
|
Latency |
Higher
(extra coordination) |
Lower
(fast writes, async updates) |
|
Scalability |
Harder
to scale |
Easier
to scale horizontally |
|
Availability |
Lower
(depends on DB/cache sync) |
Higher
(cache can serve even if DB slow) |
|
Best
Use Case |
Financial
transactions, inventory |
Product
catalogs, social feeds, analytics |
✅ Interview-Ready Bullet Points
- “Strong consistency ensures
cache and DB are always aligned, but adds latency and reduces
scalability.”
- “Eventual consistency allows
faster writes and higher availability, but risks serving stale data
temporarily.”
- “I choose strong consistency
for critical domains like payments, and eventual consistency for
read-heavy workloads like catalogs or feeds.”
- “Often, I combine both:
strong consistency for critical paths, eventual consistency with TTLs for
non-critical data.”
- How do you prevent cache stampede/thundering herd problem when many requests hit
an expired key?
- How do you design caching
for high-traffic endpoints like product catalog or user sessions?
Troubleshooting
& Observability
- How do you monitor cache hit/miss ratio and decide if caching is effective?
o
“I monitor cache hits, misses, and eviction rates using Redis/Micrometer
metrics.”
o
“A high hit ratio (>70%) and reduced DB load indicate caching is
effective.”
o
“Low hit ratio or frequent evictions mean poor key design, TTL issues,
or cache size problems.”
o
“I use dashboards (Grafana/Prometheus) to visualize trends and alerts
for miss spikes.
- What happens if
Redis/Memcached goes down? How do you design fallbacks?
- How do you debug stale cache
issues in production?
- How do you ensure security
when caching sensitive data (e.g., user tokens)?
- How do you test caching
logic in unit/integration tests?
No comments:
Post a Comment