Entity and Record Caching

Caching single entities or records by stable identity and on the relationship drift that can make this simple pattern harder than it looks.

Entity and record caching stores one logically stable object per key: one user, one product, one account, one profile, one order. This is usually the cleanest form of data-level caching because the identity boundary is obvious. If the system already thinks in terms of stable entity IDs, then cache keys often map naturally to that same model.

That simplicity is real, but it is also slightly deceptive. The cached entity may still depend on related data such as permissions, prices, feature flags, or parent-child relationships. Once those relationships matter, invalidating only the entity’s own key may no longer be enough.

    flowchart LR
	    A["Entity id\nproduct:42"] --> B["Cached entity record"]
	    C["Related changes\nprice, locale, permissions"] -. "may change meaning" .-> B

Why It Matters

Entity caches are attractive because they usually offer:

  • simple keys
  • narrow invalidation targets
  • broad reuse across many application paths

They are often the best starting point for data-level caching because they align with domain identity rather than query shape. But they only stay simple if the cached value remains close to one canonical entity and does not quietly absorb too many derived assumptions.

Where Entity Caching Fits

This pattern works especially well when:

  • reads frequently target one entity by stable ID
  • the entity has a clear authoritative source
  • updates are easy to map to that same entity key
  • related data does not radically change the entity’s safe representation

It is weaker when the cached “entity” is really a stitched projection of many moving parts.

Example

This example shows a cache-aside entity read for a product record. The record is keyed by stable identity, which makes both reading and invalidation straightforward.

 1async function getProduct(productId: string): Promise<Product> {
 2  const key = `product:${productId}`;
 3  const cached = await cache.get(key);
 4
 5  if (cached) {
 6    return JSON.parse(cached) as Product;
 7  }
 8
 9  const product = await productStore.fetch(productId);
10  await cache.set(key, JSON.stringify(product), 300);
11  return product;
12}
13
14async function updateProduct(productId: string, patch: Partial<Product>): Promise<void> {
15  await productStore.update(productId, patch);
16  await cache.delete(`product:${productId}`);
17}

What to notice:

  • stable identity makes the key obvious
  • write invalidation can target one key directly
  • the pattern is cleanest when the cached object is truly one entity, not a hidden aggregate

Relationship Drift Is The Real Complication

Entity caches get harder when the stored value starts to include related information that changes independently. For example:

  • a product object includes computed inventory availability
  • a user profile object includes effective permission summaries
  • an account record includes dynamic billing state

At that point the cache key still looks simple, but invalidation is no longer driven by one table or one domain event. The team has to ask whether the cache still represents one entity or whether it has become a small projection cache in disguise.

Common Mistakes

  • assuming entity identity alone defines freshness
  • caching projections as if they were simple records
  • forgetting related events that change how the entity should be read
  • overloading one entity cache entry with presentation-specific fields

Design Review Question

When does entity caching stop being simple even if the key is still just entity:<id>?

The stronger answer is when the cached value includes independently changing related data. The key may still look stable, but the invalidation model has become wider than the entity identity suggests.

Quiz Time

Loading quiz…
Revised on Thursday, April 23, 2026