Cache¶
Why cache?¶
Caching improves page-load times and reduces load on servers and databases. The dispatcher first checks if a request has been served before and returns the previous result, saving the actual execution.
Databases benefit from uniform read/write distribution. Popular ("hot") items skew the distribution and cause bottlenecks — a cache in front of a database absorbs uneven loads and traffic spikes.
Where caches live¶
- Client caching — browser or OS.
- CDN caching — CDNs are a type of cache.
-
Web server caching — reverse proxies and caches like Varnish serve static/dynamic content directly.
-
Database caching — DBs include some caching by default; tune it for your usage patterns.
-
Application caching — in-memory key-value stores (Memcached, Redis) between the app and data storage.
-
Data in RAM is far faster than disk-backed DBs.
- RAM is limited, so use eviction algorithms like LRU (least recently used).
- Redis adds persistence and built-in data structures (sorted sets, lists).
Levels you can cache¶
Two broad categories — database queries and objects:
- Row level
- Query level
- Fully-formed serializable objects
- Fully-rendered HTML
Avoid file-based caching — it makes cloning and auto-scaling harder.
Caching at the database query level¶
Hash the query as a key and store the result. Downside — expiration issues:
- Hard to delete a cached result for complex queries.
- If one cell changes, you must delete all cached queries that might include it.
Caching at the object level¶
Treat data as objects (like in app code); assemble the DB result into a class instance or data structure:
- Remove the object from cache when its underlying data changes.
- Enables async processing: workers assemble objects from the latest cached object.
Good things to cache: user sessions, fully-rendered pages, activity streams, user graph data.
When to update the cache¶
Cache-aside (lazy loading)¶
The application is responsible for reading/writing storage; the cache doesn't touch storage directly.
- Look for entry in cache → cache miss.
- Load entry from database.
- Add entry to cache.
- Return entry.
def get_user(self, user_id):
user = cache.get("user.{0}".format(user_id))
if user is None:
user = db.query("SELECT * FROM users WHERE user_id = {0}".format(user_id))
if user is not None:
cache.set("user.{0}".format(user_id), json.dumps(user))
return user
Used by Memcached. Only requested data is cached (no filling cache with junk).
Disadvantages: each miss = 3 trips (latency); data can go stale (mitigate with TTL or write-through); a failed node is replaced by an empty one.
Write-through¶
The app uses the cache as the main data store; the cache writes through to the DB.
- App adds/updates entry in cache.
- Cache synchronously writes to data store.
- Return.
Slow overall (write op), but subsequent reads are fast, and cache data is not stale.
Disadvantages: a new node won't cache entries until they're updated in the DB (combine with cache-aside); much written data is never read (use TTL).
Write-behind (write-back)¶
- App adds/updates entry in cache.
- Cache asynchronously writes to data store → faster writes.
Disadvantages: data loss risk if the cache dies before flushing to the store; more complex to implement.
Refresh-ahead¶
Cache automatically refreshes recently-accessed entries before they expire. Reduces latency vs read-through if the cache can predict future needs.
Disadvantage: poor prediction makes performance worse.
Strategy comparison¶
| Strategy | Write path | Read freshness | Risk |
|---|---|---|---|
| Cache-aside | App writes DB directly | Can be stale (TTL) | Miss latency, stale data |
| Write-through | Sync via cache | Fresh | Slow writes, cold cache |
| Write-behind | Async via cache | Fresh-ish | Data loss on crash |
| Refresh-ahead | — | Proactive refresh | Bad predictions |
Disadvantages of caching overall¶
- Must maintain consistency between cache and source of truth (invalidation).
- Cache invalidation is hard — deciding when to update adds complexity.
- Requires app changes (adding Redis/Memcached).
Key takeaways¶
-
Caching is the highest-leverage performance tool, but its cost is staleness and invalidation complexity.
-
"There are only two hard things in Computer Science: cache invalidation and naming things."
Common Interview Questions¶
Design a cache system like Memcached
Why it's asked here: the direct cache question.
Key points to discuss:
- In-memory KV, eviction policy (LRU), and sharding across nodes.
- Consistent hashing to minimize reshuffling when nodes change.
- Cache-aside (lazy loading) flow and the cache-miss cost.
- Invalidation and TTL to manage staleness.
flowchart LR
A[App] -->|get| C[(Cache cluster)]
C -->|miss| DB[(Database)]
DB --> C
C -->|consistent hashing| N1[Node 1]
C -->|consistent hashing| N2[Node 2]
Design a key-value store for a search engine (query cache)
Why it's asked here: caching query results is a classic query-level cache.
Key points to discuss:
- Cache query → results; hit ratio drives value.
- Query-level vs object-level caching trade-offs.
- Invalidation: hard to delete cached results for complex queries.
- Eviction + TTL for the long tail of queries.
flowchart LR
Q[Query] --> C[(Query cache)]
C -->|hit| R[Result]
C -->|miss| E[Search engine]
E --> C
Design a news feed / Twitter timeline (cache fan-out)
Why it's asked here: timelines are served from cache via fan-out.
Key points to discuss:
- Precompute and cache each user's timeline (fan-out on write).
- Redis sorted sets / lists for timeline storage.
- Cache update strategies: write-through vs write-behind vs cache-aside.
- Staleness vs performance for celebrity (hot) users.
flowchart LR
P[Post] --> F[Fan-out on write]
F --> T[(Timeline cache)]
U[User] --> T
T -->|miss| D[(Sharded tweets)]