Skip to content
DEE
Database Engineering Essentials

[DEE-452] Read-Through and Write-Through Caching

INFO

In read-through and write-through caching, the cache layer -- not the application -- manages database reads and writes. The application interacts only with the cache, which transparently synchronizes with the database.

Context

In the cache-aside pattern (DEE-451), the application explicitly manages both the cache and the database. This gives full control but scatters cache logic throughout the application code. Read-through and write-through patterns move that responsibility into the cache layer itself, creating a simpler programming model where the application treats the cache as the primary data interface.

These patterns are common in enterprise caching frameworks (Oracle Coherence, NCache, Hazelcast) and CDN architectures. They are less common with raw Redis or Memcached, which do not natively provide database-backed read/write-through behavior -- you must build or configure a middleware layer.

Three related patterns exist:

  • Read-through: On a cache miss, the cache itself fetches the data from the database, stores it, and returns it to the application. Subsequent reads are served from cache.
  • Write-through: On a write, the cache writes to both itself and the database synchronously before returning success to the application.
  • Write-behind (write-back): On a write, the cache updates itself immediately and returns success, then asynchronously flushes the write to the database in the background.

Principle

Developers SHOULD use read-through caching when the application should not contain cache-miss logic and the caching layer can be configured with a data loader.

Developers SHOULD use write-through caching when every write must be persisted to the database before the application proceeds, and the cache should always reflect the latest state.

Developers MAY use write-behind caching when write throughput is critical and the application can tolerate a window of potential data loss if the cache node fails before flushing.

Developers MUST NOT use write-behind caching for data that cannot be lost (e.g., financial transactions, audit logs) unless the cache layer provides durability guarantees (replication, write-ahead log).

Visual

Example

Conceptual read-through implementation

In a read-through setup, you configure the cache with a "loader" function. The application never queries the database directly:

python
class ReadThroughCache:
    def __init__(self, cache_store, db_loader, ttl_seconds=300):
        self.cache = cache_store
        self.loader = db_loader
        self.ttl = ttl_seconds

    def get(self, key: str):
        value = self.cache.get(key)
        if value is not None:
            return value

        # Cache manages the DB fetch -- not the application
        value = self.loader(key)
        if value is not None:
            self.cache.set(key, value, ex=self.ttl)
        return value

# Application code is simple -- no cache-miss logic
cache = ReadThroughCache(
    cache_store=redis_client,
    db_loader=lambda key: db.query_user(key)
)
user = cache.get("user:42")

Conceptual write-through implementation

python
class WriteThroughCache:
    def __init__(self, cache_store, db_writer):
        self.cache = cache_store
        self.writer = db_writer

    def put(self, key: str, value, ttl_seconds=300):
        # Write to database first (synchronous)
        self.writer(key, value)
        # Then update cache
        self.cache.set(key, value, ex=ttl_seconds)

    def get(self, key: str):
        return self.cache.get(key)

Write-behind with batching

python
class WriteBehindCache:
    def __init__(self, cache_store, db_writer, flush_interval=5):
        self.cache = cache_store
        self.writer = db_writer
        self.buffer = {}  # pending writes
        self.flush_interval = flush_interval

    def put(self, key: str, value):
        # Update cache immediately -- return fast
        self.cache.set(key, value)
        self.buffer[key] = value

    def flush(self):
        """Called periodically by a background thread."""
        if self.buffer:
            self.writer.batch_write(self.buffer)
            self.buffer.clear()

Comparison Table

AspectCache-AsideRead-ThroughWrite-ThroughWrite-Behind
Who manages DB interactionApplicationCache layerCache layerCache layer
Read latency (miss)Cache + DB round-tripSame, but transparentN/AN/A
Write latencyDB write + cache invalidationN/AHigher (sync DB + cache)Lowest (cache only)
Write throughputLimited by DBN/ALimited by DBHighest (async batching)
Data loss risk on cache failureNone (DB is source of truth)NoneNoneYes -- buffered writes may be lost
ConsistencyDepends on invalidationStrong for readsStrong for reads + writesEventual
Implementation complexityLow (application logic)Medium (cache must have loader)Medium (cache must have writer)High (async flush, error handling)
Common implementationsRedis + application codeOracle Coherence, NCache, HazelcastOracle Coherence, NCache, DAXOracle Coherence, NCache, Hazelcast

Common Mistakes

  1. Write-behind data loss risk. Write-behind caches buffer writes in memory. If the cache node crashes before flushing, those writes are lost permanently. Mitigate with cache replication, persistent write-ahead logs, or by restricting write-behind to data you can afford to lose (e.g., analytics counters, non-critical metrics).

  2. Assuming write-through eliminates inconsistency. Write-through guarantees the cache and database are updated on every write, but if another system writes directly to the database (migration script, admin tool, another service), the cache will not reflect those changes. You still need a TTL or an invalidation mechanism for external writes.

  3. Over-engineering simple use cases. If your application has a single database and a single cache, cache-aside with explicit invalidation is usually simpler and sufficient. Read-through and write-through add value in frameworks that support them natively (e.g., Hazelcast MapLoader, DynamoDB DAX) or when you want to decouple cache logic from business code in a large codebase.

  4. Not handling loader/writer failures. In read-through, if the database is down, the cache cannot serve a miss. In write-through, if the database write fails, the cache must not be updated. Ensure the cache layer propagates errors to the application rather than silently caching partial or failed results.

  5. Mixing patterns without a clear strategy. Using read-through for reads but cache-aside invalidation for writes creates confusion about who owns the cache lifecycle. Pick a consistent pattern per data domain and document it.

  • DEE-450 Caching and Search Overview
  • DEE-451 Cache-Aside Pattern -- the most common alternative where the application manages cache explicitly
  • DEE-453 Cache Invalidation Strategies -- invalidation applies to all caching patterns

References