Skip to content
intermediatePhase 46 · Caching

Read-Through

Cache transparently loads data on first access from the database.

30m
0 problems
Topic Progress0%

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

  1. Cache owns the loading logic
  2. Application only interacts with cache
  3. Transparent to the application
  4. 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

  1. Register loaders by data type for clean separation
  2. Handle null/empty results to prevent caching empty data
  3. Implement TTLs based on data freshness needs
  4. Add cache warming for predictable access patterns
  5. Monitor load from DB to ensure cache is effective

Practice Problems

0/3solved
Design Read-Through System

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 & reliability
Read-Through Scaling

How 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 decomposition
Read-Through Failure Modes

Analyze 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 degradation

Quiz

1. In Read-Through caching, who loads data from the database?

Question 1 options

2. What is the main advantage of Read-Through over Cache-Aside?

Question 2 options

3. How does the application access data in Read-Through?

Question 3 options

4. When is Cache-Aside preferred over Read-Through?

Question 4 options

5. What should be registered in a Read-Through cache implementation?

Question 5 options

Flashcards

Question

What is Read-Through caching?

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?

Answer

Cache-Aside: Application loads data. Read-Through: Cache loads data.

Question

What is the main benefit of Read-Through?

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?

Answer

Register data loaders by key prefix, cache checks first, calls loader on miss, stores and returns result

Question

What is Read-Through?

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

  1. Register loaders per key prefix
  2. Cache.get(key) checks cache first
  3. On miss, call registered loader
  4. Store result, return to app