Your API returns a user profile: name, preferences, orders, billing. You cache the whole response. The user updates their display name. The entire cache entry is gone — orders, preferences, billing, all of it. You recompute the full response from scratch because one field changed. This is monolithic caching, and it is how every production cache works today. It is also why your hit rates are stuck at 65%.
The Over-Invalidation Problem
Over-invalidation is the most common and least discussed failure mode in caching. It happens when you evict more data than actually changed. With monolithic caching, this is not a bug — it is the only option. The cache stores the entire response as a single entry. There is no way to say “invalidate the name but keep the orders.” The cache has no concept of what a “name” is. It sees bytes.
The consequences compound. A SaaS dashboard with 6 widgets — revenue, active users, recent activity, usage metrics, alerts, team members — is cached as one blob. The alerts widget updates every 30 seconds. So the entire dashboard is invalidated every 30 seconds. Five widgets that change hourly are recomputed every 30 seconds because one widget changes frequently. The cache exists but barely helps.
GraphQL makes this worse. Every query shape — every unique combination of fields a client requests — produces a different cache key. A mobile client requesting 4 fields and a web client requesting 12 fields get zero cache reuse, even though they share 4 fields of data. The combinatorial explosion of query shapes makes full-response caching for GraphQL effectively impossible. Teams end up caching at the resolver level with manual invalidation logic, which breaks the moment resolvers have dependencies on each other.
Personalized APIs face the same problem. A product page with a shared layout and user-specific recommendations is cached per user. Change the product description and every user’s cached page is invalidated, even though only the shared portion changed. The personalization layer multiplies the over-invalidation by the number of users.
Cache Fusion: Fragment-Level Caching with Read-Time Composition
The solution is to stop caching responses and start caching fragments. Each piece of a response — the name, the preferences, the orders, the billing data — is stored as an independent cache entry. At read time, the cache composes them into a complete response using a new primitive: FUSE.
# Store fragments independently
SET user:123:name "Jane Doe"
SET user:123:prefs '{"theme":"dark","lang":"en"}'
SET user:123:orders '[{"id":901,"total":49.99}]'
SET user:123:billing '{"plan":"pro","renewal":"2026-04-15"}'
# Compose at read time
FUSE user:123:profile FROM user:123:name user:123:prefs user:123:orders user:123:billing
# Now update the name:
SET user:123:name "Jane Smith"
# Only user:123:name is invalidated
# prefs, orders, billing --> still cached (HIT)
# Next FUSE: 1 fresh fragment + 3 cached = full response
The FUSE command reads each fragment pointer, assembles the composite structure, and returns the result. There is no serialization, no deserialization, no merge logic. It is pointer assembly. The cost is typically under 5 microseconds for a 4-fragment composition, regardless of fragment size. A 50-byte name and a 10KB order history are both a single pointer read.
When user:123:name is updated, only that fragment is invalidated. The orders fragment, the preferences fragment, and the billing fragment remain hot. The next FUSE reads one fresh value and three cached values. Instead of a full cache miss and a complete recomputation, you get a partial miss on the single field that actually changed.
Hit Rates: Monolithic vs Fragment
The hit rate improvement is not incremental. It is structural.
Consider a user profile endpoint with 4 fragments, receiving 10 writes per second distributed across all fields. With monolithic caching, every write invalidates the entire entry. At 10 writes/sec, the monolithic entry is invalidated 10 times per second. Between invalidations, reads hit the cache. Depending on your read-to-write ratio, you land somewhere around 60–70% hit rate.
With fragment caching, each write invalidates only the affected fragment. A write to the name field invalidates user:123:name but leaves the other 3 fragments cached. At any given moment, 3 out of 4 fragments are hot. The effective hit rate across all fragments is 90–95%. The same write volume, the same read volume, but the cache is serving 3x more data from memory instead of recomputing it.
user:123:profile —— MISS (recompute everything)
Fragment: 1 write = 1 fragment miss, rest stay hot
user:123:name —— MISS (fetch name only)
user:123:prefs —— HIT
user:123:orders —— HIT
user:123:billing —— HIT
↓
FUSE —— composed response (~3µs)
Composition with Dependency Graph and CDC
Cache Fusion composes with two other Cachee primitives to create a fully automated invalidation pipeline.
Fragments + Dependency Graph
Fragments can declare dependencies using DEPENDS_ON. A billing fragment might depend on a pricing tier key. When the pricing tier changes, the dependency graph invalidates the billing fragment — and only the billing fragment. The rest of the FUSE composition stays hot. Dependency-aware invalidation at fragment granularity means you get both surgical precision (only the affected piece is evicted) and causal correctness (the graph ensures nothing stale is served).
Fragments + CDC
CDC auto-invalidation watches your database change stream and invalidates the corresponding cache key when a row changes. With fragments, CDC invalidates individual fragments instead of entire responses. The orders table gets a new row. CDC fires. user:123:orders is invalidated. The name, preferences, and billing fragments are untouched. The next FUSE assembles 1 fresh fragment and 3 cached fragments. Zero application code. The database change flows through CDC into the fragment layer and the cache self-heals.
Use Cases for Platform Engineering Teams
- GraphQL APIs: Each resolver maps to a fragment. FUSE assembles the response shape. Different query shapes share the same fragments. Fragment reuse eliminates the combinatorial explosion of cache keys that makes GraphQL caching impossible with monolithic entries.
- SaaS dashboards: Each widget is a fragment. The dashboard is a FUSE. A billing update invalidates the revenue widget. The activity feed, user count, and alert widgets stay cached. Dashboard load time stays under 50ms even during high write volume.
- API gateway response assembly: The gateway aggregates 5 microservice responses. Each is a fragment. When one service deploys and its data changes, only that fragment is invalidated. The other 4 stay hot. FUSE replaces expensive backend fan-out with microsecond composition.
- Personalized content: Base template fragments (layout, product info, pricing) are shared across all users. Personal fragments (name, recommendations, plan badge) are per-user. FUSE composes them. A product description update invalidates one shared fragment; personal fragments are untouched across every user.
Related Reading
- Cache Fusion — Product Page
- Causal Dependency Graphs
- CDC Auto-Invalidation
- Cache Coherence
- Enterprise
Also Read
Cache Fragments. Compose at Read Time. Zero Over-Invalidation.
Fragment composition. Dependency graphs. CDC auto-invalidation. Cross-instance coherence. The cache primitives your API actually needs.
Start Free Trial Schedule Demo