Phase 4: Architecture & Scaling

Cache invalidation: TTL, write-through, write-behind & cache-aside

Intermediate ~3 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine our school library has a super special "Fast Facts Shelf" – it's incredibly quick to find answers there. The main library, with all its huge shelves, takes much longer to search. Here's the tricky part: what if a fact on the Fast Facts Shelf is wrong or outdated? Like, a book about the "Current Class Roster" still lists a student who moved away last week. Uh oh! That's "stale" information – it’s not the real, true story anymore. We need to make sure our Fast Facts Shelf always has the most accurate information, just like the main, official library.

One way the librarian keeps the Fast Facts Shelf fresh is with "Time-To-Live" (or TTL). On some books, they put a little sticker: "Good until Friday!" or "Valid for 24 hours!" Once that time runs out, the librarian automatically removes that book. The next time someone asks for that information, they’ll have to go to the main library to get the newest version. This is a simple rule: time’s up, data’s out! It prevents very old information, but if a student moved on Tuesday and the sticker says "Good until Friday," we might still have stale info for a little while.

What happens when new information comes in, like a new student? With a "Write-Through" rule, the librarian updates the main library records first, and then immediately puts an identical, updated copy on the Fast Facts Shelf. This way, the Fast Facts Shelf is always perfectly consistent, but it takes a tiny bit longer because two things have to be updated. Another approach is "Write-Behind." Here, the librarian quickly updates the Fast Facts Shelf first, so people get the new info right away. Then, quietly, in the background, a note is sent to the main library to update its records later. This is super fast for getting the info, but there's a small chance if the Fast Facts Shelf computer crashes before the main library gets the note, the main library might not have the latest info.

Most of the time, our librarian uses a "Cache-Aside" strategy. When you ask for a book, the librarian first checks the Fast Facts Shelf. If it’s there, great, you get it super fast! If not, then they go to the big main library. Once they find it, they'll make a copy and put it on the Fast Facts Shelf for next time, then give it to you. This means we only put things on the fast shelf if someone asked for it and it wasn't there. So, when building your own cool apps or games, understanding these rules helps you make sure your users always get correct, up-to-date information super quickly, without waiting for slow searches. You can decide how careful to be versus how fast you want things to feel!

Cache invalidation is the critical challenge of ensuring your cached data remains fresh and doesn't become "stale" – meaning it no longer reflects the true state of your database. Serving outdated information can lead to bad user experiences or even operational errors. The simplest strategy is Time-To-Live (TTL), where each cached item is assigned an expiration time. After this duration, the item is automatically removed from the cache, forcing the application to fetch the latest data from the source on the next request. TTL is easy to implement but can lead to temporary staleness if the underlying data changes before expiration, or unnecessary cache misses if data rarely changes.

When writing data, two main patterns aim to keep caches consistent. Write-Through involves writing data synchronously to both the cache and the primary database. This ensures data in the cache is always up-to-date with the database, providing strong consistency immediately after a write. The downside is increased write latency, as the operation isn't complete until both writes succeed. In contrast, Write-Behind writes data to the cache first, then asynchronously to the database. This offers much lower write latency and higher throughput, as the application doesn't wait for the database write. However, it introduces a risk: if the cache fails before the data is written to the database, that data could be lost.

The most common caching strategy, especially for custom applications, is Cache-Aside (also known as lazy loading). Here, the application code explicitly manages the cache. On a read, the application first checks the cache. If the data is found (a "cache hit"), it's returned immediately. If not (a "cache miss"), the application fetches the data from the database, stores it in the cache for future use (often with a TTL), and then returns it. On a write, the application writes directly to the database and then invalidates (removes) the corresponding entry from the cache. This ensures the next read will be a cache miss, fetching the fresh data from the database. Cache-Aside offers maximum flexibility but requires the developer to manage the caching logic within the application.

Key Takeaways

  • Cache invalidation prevents stale data and is crucial for data consistency.
  • TTL provides automatic cache expiry but can lead to temporary staleness.
  • Write-Through ensures immediate consistency by writing to cache and DB synchronously, but increases write latency.
  • Write-Behind offers fast writes by writing to cache first, then asynchronously to DB, but risks data loss on cache failure.
  • Cache-Aside (most common) requires explicit application logic: read from cache, if miss then read from DB and populate cache; on write, update DB and invalidate cache.

Code Example

python
class DataService:
    def __init__(self, db_client, cache_client):
        self.db = db_client
        self.cache = cache_client

    def get_user_data(self, user_id):
        cache_key = f"user:{user_id}"
        user_data = self.cache.get(cache_key) # Try cache first

        if user_data:
            print(f"Cache hit for user {user_id}")
            return user_data

        print(f"Cache miss for user {user_id}. Fetching from DB.")
        user_data = self.db.fetch_user(user_id) # Fetch from DB
        if user_data:
            self.cache.set(cache_key, user_data, ex=3600) # Store with 1hr TTL
        return user_data

    def update_user_data(self, user_id, new_data):
        # Write to DB
        self.db.update_user(user_id, new_data)
        # Invalidate cache entry
        self.cache.delete(f"user:{user_id}")
        print(f"User {user_id} updated and cache invalidated.")

How this code works

This DataService class demonstrates the "cache-aside" strategy, which aims to serve user data quickly from a cache while ensuring data freshness. The get_user_data method first tries to retrieve user_data from the cache using a unique cache_key. If a cache_hit occurs, the data is returned immediately. If it's a cache_miss, the method fetches the data from the db, then stores it in the cache using self.cache.set. A subtle but important detail here is the ex=3600 parameter, which sets a Time-To-Live (TTL) of 3600 seconds (1 hour) for the cached entry. This acts as a safety net, ensuring even if other invalidation methods fail, the data will eventually expire and be re-fetched.

When update_user_data is called, it first writes the new_data directly to the db, ensuring the primary data source is always up-to-date. Immediately after the database update, the self.cache.delete method is used with the cache_key to remove the old, now-stale entry from the cache. This crucial step is "cache invalidation," ensuring that any subsequent calls to get_user_data for this user will result in a cache_miss, forcing a fresh fetch from the db and updating the cache with the newest information.