Practice this topic in a realistic system design interview
Caching always raises the same question: who keeps the cache and the database aligned?
With cache-aside, the application does the work. It checks the cache, loads missing data from the database, and deletes old cache entries when data changes.
That gives the application full control, but it also creates repeated code. Every service that touches the data has to follow the same cache rules correctly.
Read-through and write-through move some of that responsibility into the cache layer. A read-through cache knows how to load missing data. A write-through cache knows how to write data to the database.
These patterns can make application code cleaner, but they require a cache layer that understands your data access rules. A plain key-value cache usually needs a loader, a writer, or a wrapper library.
This chapter covers how both patterns work, how they compare with cache-aside, what trade-offs they introduce, and when to use each one.
In a read-through cache, the application asks the cache for data. If the value is missing, the cache layer loads it from the database, stores it, and returns it.
The application does not need to contain the cache-miss logic for that data type.
cache.get("user:123")A read-through setup connects a loader function to the cache. The application reads a key, and the cache handles any miss behind the scenes.
The application code stays small because the cache layer handles the miss path.
Read-through reduces repeated application code because services no longer need to reimplement miss handling. One loader defines how a key is fetched. Cache fills happen automatically on every miss, so they are harder to forget. The read path also looks cleaner because application code reads like a normal lookup.
The cache layer becomes more complex because it now owns the loader logic and database access. Cache keys are tied to data access rules, so the cache layer and data model become more connected.
Cold-cache latency does not go away. The first read for a key still pays the database round trip. Miss failures also move into the cache layer, so loader errors need to be handled carefully.
Read-through is useful when many callers need the same read behavior and you want one place to define it.
In a write-through cache, the application writes to the cache layer. The cache layer writes to the database first, then updates the cached value.
The cache reports success only after that write-through path finishes.
cache.set("user:123", user_data)Here the cache takes a writer function. A set call pushes the value through to the database before the cache reports success.
Write-through removes the "write DB, then remember to delete the cache entry" step from application code. But the cache layer is now part of the write path, so failures and latency matter more.
Write-through centralizes the write path, so one writer defines how data is saved. There is no miss immediately after a write because the cache already contains the new value. Reads right after writes become easier because the user usually sees their update straight from the cache. Manual cache-delete code is replaced by one shared path.
Writes are slower because every write waits for the database. The cache layer is now part of the write path, so problems there can block writes.
All writers must use the same path. Any direct database write can leave stale data in the cache. Partial failures also need careful handling because the database update and cache update are still separate operations.
Write-through is useful when reads commonly follow writes and you can force all writers through the same cache path.
Read-through and write-through are often combined.
The application reads and writes through the cache layer. The cache layer handles both loading and writing.
This can make the cache and database feel like one shared data access layer.
That model only works if callers do not bypass it. If another service writes directly to the database, the cache layer may not know the data changed.
Read-through and write-through move logic into the cache layer. Cache-aside keeps that logic in the application.
That difference affects code complexity, freshness, and failure handling. The table below compares the three patterns side by side.
| Aspect | Cache-Aside | Read-Through | Write-Through |
|---|---|---|---|
| Read miss handling | Application | Cache layer | Not a read pattern |
| Write handling | Application writes DB and deletes cache entry | Not a write pattern | Cache layer writes DB and cache |
| Application code | More explicit | Simpler reads | Simpler writes |
| Cache layer complexity | Lower | Higher | Higher |
| Cold cache behavior | Miss handled by app | Miss handled by loader | Not applicable |
| Write latency | DB write plus cache delete | Not applicable | DB write through cache layer |
| Freshness depends on | Cache deletes and TTL | The write strategy used | All writers using write-through |
Use cache-aside when the cache should be optional, when different data types need different caching behavior, when application code needs detailed control, when the cache does not support loader or writer hooks, or when writes should go directly to the database.
Cache-aside is the most common choice because it is simple to add around an existing database.
Use read-through when many services read the same data in the same way, when miss handling is repeated across services, when a reliable loader is available, when first-read latency on cold keys is acceptable, and when cache fills should live in one place.
Read-through does not remove the need to delete or refresh stale cache entries. It only centralizes how missing data is loaded.
Use write-through when reads commonly follow writes, when slower writes are acceptable, when all writers can go through the cache layer, when the cache writer can handle validation, data formatting, and database errors, and when you want fewer manual cache deletes in application code.
Write-through gives fresher reads after writes than cache-aside only when every writer uses the write-through path.
Read-through and write-through move failure handling into the cache layer. That is convenient, but it also makes the cache layer more important.
If the cache misses and the loader cannot reach the database, the cache cannot produce a fresh value.
The options are to return an error, return a default value, serve a stale value if the cache keeps one, or start a background refresh and return a simpler response for now.
The right behavior depends on the data. A stale product description may be fine. A stale permission decision may not be.
Write-through has two operations: write to the database and update the cache.
If the database write fails, the safest behavior is to fail the whole write and leave the cache unchanged.
If the database write succeeds but the cache update fails, the database has the new value while the cache may still have the old value. The cache layer should delete or mark the cached entry stale so the next read reloads from the database.
The example reads the saved value back from the database before caching it. This avoids caching a value that differs from what the database stored, for example after triggers, defaults, or computed columns run.
Write-through is often described as keeping the cache and database in sync. That is only true if the system follows a few rules.
If one service writes through the cache and another writes directly to the database, the cache can become stale.
If two writes happen at nearly the same time, the cache layer must preserve the same order as the database.
Use versions, optimistic locking, compare-and-set, or database-generated update timestamps for data where this matters.
The value written by the application may not be the final value stored by the database. Triggers may update updated_at, defaults may fill in missing fields, computed columns may create derived values, constraints may normalize or reject data, and stored procedures may apply business rules.
If the cache stores the value before the database finishes, it may differ from the real saved value. Reading the saved value back before caching avoids this bug.
Both patterns change where time is spent on reads and writes. Write-through adds the cache layer to every write. Read-through still pays the full cost of a cold cache when keys are missing.
Write-through adds the cache layer to the write path.
| Pattern | Write Path | Latency Shape |
|---|---|---|
| Cache-aside | App writes DB, then deletes cache | DB latency plus cache delete |
| Write-through | App writes cache layer, cache writes DB, then stores cache | DB latency plus cache-layer work |
The difference may be small when the cache is nearby and the database is the slowest part. It can be large if the cache layer does extra validation, data formatting, replication, or retries.
Read-through does not avoid cold-cache latency. It only hides miss handling from the application.
On a cold cache, many first reads still go to the database through the loader. For high-traffic systems, use cache warming, make only one request load a missing value while others wait, add rate limits, and shift traffic gradually.
How you implement these patterns depends on the cache you use. Some caches have loader and writer hooks built in. Others need a wrapper around a plain key-value store.
Read-through is common in local cache libraries. A loader function runs when a key is missing.
Shared caches such as Redis and Memcached are usually plain key-value stores. By default, they do not know your database tables or business rules.
To get read-through or write-through behavior with them, applications commonly use a wrapper library or service that implements the loader and writer.
Some distributed cache and data grid systems provide loader and writer hooks directly. These can be useful, but they make the cache layer an active part of reading and writing data, not just a simple place to store cached values.
That means the cache layer needs the same production care as any other service that reads and writes data: timeouts, retries, metrics, capacity planning, and failure handling.
Read-through and write-through move cache read and write responsibilities from application code into the cache layer. Read-through loads missing values through a loader, which simplifies read code but still has to handle cold-cache misses and loader failures.
Write-through saves writes through the cache layer before reporting success. This improves reads right after writes, but writes are slower and all writers must use the same path. Together, read-through and write-through create a shared data access layer where services read and write through the cache.
What you give up is control. Cache-aside keeps caching logic explicit in the application. Read-through and write-through centralize it, which can reduce duplicated code but makes the cache layer more complex and more important. Use these patterns when having one shared data access path is worth that added responsibility.
10 quizzes