0Pricing
AWS Solutions Architect · Lesson

Caching Strategies: Lazy Loading and Write-Through

Implement lazy loading to populate the cache on misses and write-through to keep the cache consistent with every database write.

Caching Strategies: Lazy Loading and Write-Through is a free AWS Solutions Architect lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AWS Solutions Architect learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Caching Strategies Matter

Inserting a cache between your application and database requires a caching strategy — a set of rules that determine when data is put into the cache, when it is read from the cache, and when it is evicted. Choosing the wrong strategy leads to either stale data (cache returns outdated values), cold cache misses (cache is empty and every request hits the database), or cache stampede (many simultaneous requests for the same missing key all hit the database simultaneously). The two foundational strategies are lazy loading and write-through.

Lazy Loading (Cache-Aside): How It Works

Lazy loading (also called cache-aside) is the most common caching pattern. The application checks the cache first. On a cache hit, data is returned directly from the cache — fast path. On a cache miss, the application fetches data from the database, writes the result into the cache with a TTL, and returns it to the caller. The cache is only populated with data that is actually requested — hence 'lazy'. The next request for the same key will find it in the cache.

# Lazy loading pattern (Python with redis-py)
def get_user(user_id, redis_client, db):
    cache_key = f'user:{user_id}'

    # 1. Check cache
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)   # Cache HIT

    # 2. Cache MISS: fetch from DB
    user = db.query('SELECT * FROM users WHERE id = %s', user_id)

    # 3. Populate cache with TTL of 300 seconds
    redis_client.setex(cache_key, 300, json.dumps(user))

    return user

Lazy Loading: Advantages

Lazy loading has three key advantages: only requested data is cached — the cache is not filled with data nobody reads, so memory is used efficiently. A cold cache does not break the application — on cache misses the application falls back to the database, so even if ElastiCache is restarted or a node fails, the application continues to work (with higher latency). Cache is always eventually consistent with the database because stale data expires via TTL even if updates were missed.

Lazy Loading: Disadvantages

Lazy loading has three key disadvantages: cache misses are expensive — three operations (cache check, database read, cache write) versus one for a hit, causing higher latency for cold requests. Stale data — after a database update, the cache still serves the old value until TTL expires or the key is explicitly invalidated (cache inconsistency window). Cache stampede — if a popular key expires, many simultaneous requests all experience a cache miss and hit the database concurrently, potentially overwhelming it.

# Mitigating cache stampede with a probabilistic early expiration
# (Refresh the key before it expires to avoid simultaneous misses)
def get_with_stampede_protection(key, redis_client, db_fetch_fn, ttl=300):
    value = redis_client.get(key)
    ttl_remaining = redis_client.ttl(key)

    # Probabilistically refresh before expiry
    if value is None or (ttl_remaining < 30 and random.random() < 0.1):
        value = db_fetch_fn()
        redis_client.setex(key, ttl, json.dumps(value))
    return json.loads(value)

Write-Through: How It Works

In the write-through strategy, every write to the database is also written to the cache simultaneously. The application writes to both the cache and the database as part of the same operation (or the database triggers a cache update). The cache is therefore always in sync with the database — there is no stale data window. Write-through guarantees that data in the cache is always up-to-date, making subsequent reads always cache hits for recently written data.

# Write-through pattern (Python)
def update_user(user_id, user_data, redis_client, db):
    # 1. Write to database FIRST
    db.execute('UPDATE users SET name=%s WHERE id=%s',
                (user_data['name'], user_id))

    # 2. Update cache immediately (write-through)
    cache_key = f'user:{user_id}'
    redis_client.setex(cache_key, 3600, json.dumps(user_data))

    return user_data

# Every read is now a cache hit for recently updated data

Write-Through: Advantages

Write-through advantages: cache data is always fresh — no stale data because the cache is updated on every write. Reads are always fast — popular data that is written frequently is always in the cache. No cache stampede on reads — because data is pre-populated before it is read, there are no cold misses for recently written data. This strategy is ideal for read-heavy workloads with frequent updates where data freshness is critical, such as product catalogs, pricing systems, or user profile caches.

Write-Through: Disadvantages

Write-through disadvantages: write penalty — every write incurs two operations (database + cache), adding latency to writes. Cache pollution — data is cached even if it is never read again (written once, never requested), wasting cache memory. Cache restart means cold cache — if the cache cluster is restarted, all the proactively written data is lost and the cache must be re-populated through writes or a cache warm-up process. Combine write-through with a TTL to prevent unbounded growth of rarely-read cached data.

Combining Lazy Loading and Write-Through

In practice, many production systems combine both strategies: use write-through for frequently updated, frequently read data (like user sessions or current prices) and lazy loading for rarely updated, frequently read data (like product descriptions or article content). Set appropriate TTLs on both: write-through keys get a long TTL since data is always fresh; lazy-loaded keys get a shorter TTL to bound the stale data window. This hybrid approach maximises cache hit rate while minimising staleness.

# Hybrid: write-through for sessions, lazy loading for product data

# Write-through for session data (always fresh, critical)
def save_session(session_id, data, redis_client, db):
    db.upsert('sessions', session_id, data)
    redis_client.setex(f'session:{session_id}', 3600, json.dumps(data))

# Lazy loading for product catalog (infrequent updates OK)
def get_product(product_id, redis_client, db):
    cached = redis_client.get(f'product:{product_id}')
    if cached:
        return json.loads(cached)
    product = db.query('SELECT * FROM products WHERE id=%s', product_id)
    redis_client.setex(f'product:{product_id}', 86400, json.dumps(product))
    return product

TTL Design Principles

The Time-To-Live (TTL) on cached data controls the maximum stale data window and the cache's memory footprint. Design TTLs based on: data update frequency (session data changes often → short TTL; static content changes rarely → long TTL), staleness tolerance (financial prices → very short; blog post text → hours or days), and cache memory capacity (low memory → shorter TTL to evict stale data faster). Always set a TTL — never cache data without expiry or the cache will eventually fill with stale data.

# TTL examples by data type

# API rate limit counter: 60 seconds
redis_client.setex(f'ratelimit:{ip}', 60, count)

# User session: 30 minutes
redis_client.setex(f'session:{id}', 1800, json.dumps(session))

# Product catalog: 24 hours
redis_client.setex(f'product:{id}', 86400, json.dumps(product))

# Stock price: 10 seconds
redis_client.setex(f'price:{symbol}', 10, price)

# Static site content: 7 days
redis_client.setex(f'page:{slug}', 604800, html_content)

Cache Invalidation on Update

Instead of relying solely on TTL expiry, you can explicitly invalidate (delete) cache keys when the underlying data changes. This eliminates the stale data window entirely. Common patterns: delete-on-write (delete the cache key after every database update — the next read repopulates via lazy loading), event-driven invalidation (DynamoDB Streams or RDS change capture triggers a Lambda that deletes affected keys). Cache invalidation is one of the hardest problems in distributed systems; the simpler the invalidation logic, the more reliable your cache.

# Delete-on-write invalidation pattern
def update_product(product_id, new_data, redis_client, db):
    # Update the database
    db.execute('UPDATE products SET ... WHERE id=%s', (product_id,))

    # Invalidate the cache key
    redis_client.delete(f'product:{product_id}')

    # Also invalidate any list/search caches that may include this product
    redis_client.delete('products:list:page:1')
    redis_client.delete(f'products:category:{new_data["category_id"]}')

    # Next read will trigger lazy loading with fresh data

Write-Behind (Write-Back) Caching

A less common but powerful pattern is write-behind (write-back): the application writes only to the cache, and the cache asynchronously flushes data to the database in the background. This gives extremely fast writes (in-memory only) at the cost of potential data loss if the cache fails before flushing. Write-behind is appropriate for high-frequency, high-volume writes where the data can be reconstructed or small losses are acceptable — such as hit counters, analytics events, or gaming score updates. ElastiCache does not natively support write-behind; it must be implemented at the application layer.

# Write-behind pattern: write to cache, flush to DB asynchronously
# Application writes:
# redis_client.incr('post:42:views')      # fast, in-memory only

# Background job (runs every 60 seconds):
def flush_view_counts(redis_client, db):
    for key in redis_client.scan_iter('post:*:views'):
        count = redis_client.getdel(key)  # atomic get-and-delete
        post_id = key.split(':')[1]
        db.execute('UPDATE posts SET views = views + %s WHERE id = %s',
                   (int(count), post_id))

Quick Check

Test your understanding of AWS Solutions Architect (SAA-C03) concepts from this lesson.

Lesson Recap

In this lesson you learned: lazy loading populates the cache on read misses and uses memory efficiently but can serve stale data until TTL expires, write-through updates the cache on every write for zero stale data but wastes memory on unread data, and explicit invalidation deletes cache keys on database updates to eliminate stale windows. Next up we explore session storage and leaderboard patterns with ElastiCache.

Frequently asked questions

Is the “Caching Strategies: Lazy Loading and Write-Through” lesson free?

Yes — the full text of “Caching Strategies: Lazy Loading and Write-Through” is free to read here on the web, and the AWS Solutions Architect course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AWS Solutions Architect course, upgrade to CoddyKit PRO.

What will I learn in “Caching Strategies: Lazy Loading and Write-Through”?

Implement lazy loading to populate the cache on misses and write-through to keep the cache consistent with every database write. You practise AWS Solutions Architect with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start AWS Solutions Architect?

No prior experience is required. AWS Solutions Architect on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Caching Strategies: Lazy Loading and Write-Through” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this AWS Solutions Architect lesson?

Yes. Every AWS Solutions Architect lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Redis vs Memcached: Choosing the Right Engine
  2. ElastiCache Redis Replication Groups and Cluster Mode
  3. Caching Strategies: Lazy Loading and Write-Through
  4. Session Storage and Leaderboard Patterns
← Back to AWS Solutions Architect