How Read-Through Works
How Read-Through Works
In Read-Through caching, the cache itself is responsible for loading data from the database when there's a miss. The application only talks to the cache.
Flow Diagram
Read-Through Flow:
App → Cache Check
↓
[HIT] → Return Data
↓
[MISS] → Cache loads from DB → Stores in Cache → Return Data
App never directly accesses DB for reads!
Implementation Concept
class ReadThroughCache:
def __init__(self, cache_client, db_client):
self.cache = cache_client
self.db = db_client
def get(self, key, loader_func, ttl=3600):
"""
Read-through get: cache handles loading
"""
# Check cache
value = self.cache.get(key)
if value is not None:
return value
# Cache miss: cache loads from DB
value = loader_func()
self.cache.set(key, value, ttl)
return value
# Application code is simpler:
def get_user(user_id):
return cache.get(
f"user:{user_id}",
loader_func=lambda: db.query("SELECT * FROM users WHERE id = ?", user_id)
)
Key Characteristics
- Cache owns the loading logic
- Application only interacts with cache
- Transparent to the application
- Consistent interface regardless of hit/miss
vs Cache-Aside
Read-Through vs Cache-Aside
Key Differences
| Aspect | Cache-Aside | Read-Through |
|---|---|---|
| Who loads data | Application | Cache |
| App knowledge | App knows about DB | App only knows cache |
| Code complexity | More cache logic in app | Less app code |
| Flexibility | More control | Less control |
| Coupling | App coupled to both | App coupled to cache only |
Code Comparison
# Cache-Aside: App manages loading
def get_user_cache_aside(user_id):
key = f"user:{user_id}"
data = cache.get(key)
if data is None:
data = db.query(...)
cache.set(key, data)
return data
# Read-Through: Cache handles loading
def get_user_read_through(user_id):
key = f"user:{user_id}"
return cache.get(key, loader=lambda: db.query(...))
When to Use Which
Use Cache-Aside when:
- You need fine-grained control over caching logic
- Different data sources need different handling
- You want to implement custom invalidation strategies
Use Read-Through when:
- You want simpler application code
- Consistent caching behavior across all data
- The cache supports built-in read-through
Cache-Backed Read-Through
Many caches (like Redis with read-through libraries) support this natively:
// Java example with a read-through cache
LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build(new CacheLoader<String, User>() {
public User load(String userId) {
return db.query("SELECT * FROM users WHERE id = ?", userId);
}
});
// Usage - cache handles loading automatically
User user = userCache.get(userId);
Implementation
Read-Through Implementation
Production Implementation
class ReadThroughCache:
def __init__(self, redis_client, default_ttl=3600):
self.redis = redis_client
self.default_ttl = default_ttl
self.loaders = {} # Registered loaders per key prefix
def register_loader(self, key_prefix, loader_func):
"""Register a data loader for a key prefix"""
self.loaders[key_prefix] = loader_func
def get(self, key):
"""Read-through get with automatic loading"""
# Try cache first
value = self.redis.get(key)
if value is not None:
return json.loads(value)
# Determine loader from key prefix
prefix = key.split(':')[0]
if prefix not in self.loaders:
raise ValueError(f"No loader registered for prefix: {prefix}")
# Load from source
loader = self.loaders[prefix]
entity_id = key.split(':')[1]
value = loader(entity_id)
# Store in cache
if value is not None:
self.redis.setex(key, self.default_ttl, json.dumps(value))
return value
# Setup
cache = ReadThroughCache(redis_client)
cache.register_loader('user', lambda id: db.query_user(id))
cache.register_loader('product', lambda id: db.query_product(id))
# Usage - transparent to application
user = cache.get('user:123') # Automatically loads from DB if needed
product = cache.get('product:456')
With Stale-While-Revalidate
def get_with_swr(key, loader, ttl=3600, stale_ttl=86400):
"""Read-through with stale-while-revalidate"""
data = redis.get(key)
if data:
data = json.loads(data)
# Check if stale
if data['timestamp'] + ttl > time.time():
return data['value'] # Fresh
else:
# Stale but usable, refresh in background
background_refresh(key, loader, stale_ttl)
return data['value'] # Return stale data
# No data at all, must load synchronously
value = loader()
redis.setex(key, stale_ttl, json.dumps({
'value': value,
'timestamp': time.time()
}))
return value
Best Practices
- Register loaders by data type for clean separation
- Handle null/empty results to prevent caching empty data
- Implement TTLs based on data freshness needs
- Add cache warming for predictable access patterns
- Monitor load from DB to ensure cache is effective
Practice Problems
Design a scalable Read-Through system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Read-Through to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Read-Through and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. In Read-Through caching, who loads data from the database?
2. What is the main advantage of Read-Through over Cache-Aside?
3. How does the application access data in Read-Through?
4. When is Cache-Aside preferred over Read-Through?
5. What should be registered in a Read-Through cache implementation?
Flashcards
Question
What is Read-Through caching?
Click to reveal answer
Answer
A pattern where the cache itself loads data from the database on miss, transparent to the application
Question
Read-Through vs Cache-Aside: Who loads data?
Click to reveal answer
Answer
Cache-Aside: Application loads data. Read-Through: Cache loads data.
Question
What is the main benefit of Read-Through?
Click to reveal answer
Answer
Simpler application code - the app only talks to the cache, never directly to the database for reads
Question
How do you implement Read-Through?
Click to reveal answer
Answer
Register data loaders by key prefix, cache checks first, calls loader on miss, stores and returns result
Question
What is Read-Through?
Click to reveal answer
Answer
Read-Through is a key concept in system design.
Revision Notes
Key Takeaways
- 1.Read-Through: cache loads data from DB on miss, transparent to application
- 2.Main benefit: simpler application code (app only talks to cache)
- 3.Register data loaders by key prefix for clean implementation
- 4.Cache-Aside offers more control; Read-Through offers simplicity
- 5.Many caches support read-through natively with LoadingCache
Interview Tips
- •Clarify the difference: Read-Through (cache loads) vs Cache-Aside (app loads)
- •Draw the flow showing app only talks to cache in Read-Through
- •Mention that many cache libraries support this natively
- •Discuss when to choose each pattern based on control vs simplicity
Cheat Sheet
Cheat Sheet: Read-Through
Flow
App → Cache → [Hit] Return → [Miss] Cache loads from DB → Return
vs Cache-Aside
- Cache-Aside: App loads data
- Read-Through: Cache loads data
- Read-Through = simpler app code
Implementation
- Register loaders per key prefix
- Cache.get(key) checks cache first
- On miss, call registered loader
- Store result, return to app