Caching and Materialized Views

Describe caching patterns, precomputed views, TTL-based lookups, and read-optimization strategies that improve performance without breaking serverless simplicity.

Caching and materialized views improve serverless read performance by reducing expensive recomputation and shielding synchronous paths from slow backing stores. They look similar from the outside because both make reads faster, but they serve different purposes. A cache keeps recent or likely-needed data closer to the read path. A materialized view stores a deliberate alternate read model shaped for a specific query.

That distinction matters in serverless systems because function billing is sensitive to latency, and external calls are often the real cost center. A good design shortens the read path without confusing optimization with source-of-truth state.

    flowchart LR
	    A["Source-of-truth write store"] --> B["Event or change feed"]
	    B --> C["Projector or updater"]
	    C --> D["Materialized view"]
	    D --> E["Read function"]
	    E --> F["Client"]
	    E --> G["Cache"]
	    G --> F

What to notice:

  • the write model and the read model do not have to be identical
  • a cache sits in front of a read path, while a materialized view is itself a read-optimized store
  • neither should quietly replace the source of truth for correctness rules

Cache First, but Only for the Right Problem

Caches are useful when the system repeatedly asks for:

  • the same product or profile data
  • short-lived configuration or reference data
  • expensive aggregations that can tolerate bounded staleness
  • computed responses that are much costlier to rebuild than to reuse

The key design question is not “can this be cached?” It is “what staleness is acceptable here?” If the answer is “none,” a TTL cache may be the wrong first move.

 1export async function getProfileSummary(userId: string) {
 2  const cacheKey = `profile-summary:${userId}`;
 3  const cached = await cache.get(cacheKey);
 4
 5  if (cached) {
 6    return cached;
 7  }
 8
 9  const summary = await profileViewStore.get(userId);
10  await cache.put(cacheKey, summary, { ttlSeconds: 120 });
11  return summary;
12}

What this demonstrates:

  • the cache is an optimization in front of the read model
  • TTL is explicit, which makes staleness a visible design choice
  • the backing read model still exists if the cache misses or is cleared

Materialized Views Are for Query Shape, Not Just Speed

A materialized view is stronger than a cache when the application repeatedly needs a read shape that the source-of-truth store is poor at serving. For example:

  • an order system writes normalized transactional rows
  • the UI needs one document per customer with recent orders, totals, and status
  • a projector updates that read model whenever source events or row changes occur

That is not just cached data. It is a deliberate read model designed for the consuming path.

 1read_models:
 2  customer-order-summary:
 3    source: order-events
 4    updater: project-order-summary
 5    target_store: document-view-store
 6    refresh_mode: incremental
 7
 8cache:
 9  profile-summary:
10    ttl_seconds: 120
11    invalidation: on-demand-or-expiry

TTL, Invalidation, and Freshness Contracts

The most dangerous cache bugs are usually not syntax bugs. They are freshness bugs. A serverless team should be able to answer:

  • how stale can this value be?
  • what event or write should invalidate it?
  • what happens after a cache miss or eviction?
  • can the product tolerate read-after-write lag?

Materialized views have similar questions, just at a different layer:

  • what source drives projection updates?
  • what lag is acceptable?
  • how is the projector repaired after failure?

If those answers are vague, the system will be fast until it becomes confusing.

Common Mistakes

  • using a cache as the only source of correctness-critical state
  • adding cache layers before understanding the real slow query
  • calling a precomputed read model “the database” and forgetting its lag behavior
  • treating invalidation as a minor detail instead of the main design challenge

Design Review Question

A dashboard endpoint is slow because each request joins several operational tables and computes totals on demand. The team plans to add a five-minute cache in front of the existing query. Is that enough?

The stronger answer is “not necessarily.” A cache may hide the latency for repeated reads, but if the underlying read shape is fundamentally wrong for the workload, a materialized view is usually the cleaner long-term design. The team should decide whether the problem is repeated cost for the same answer or a read model that is poorly aligned with the query.

Check Your Understanding

Loading quiz…
Revised on Thursday, April 23, 2026