Cache
The Cache pattern stores the result of an expensive computation and serves it on subsequent requests with the same key. When the key matches, the route returns the cached body and skips the computation. When the key does not match, the route runs the on_miss sub-pipeline, stores the result, and returns it.
- cache:
key: "${header.cacheKey}"
ttl: "5s"
on_miss:
- set_body: "computed-fresh-data"
- log: "Cache MISS — body computed and stored"
- log: "After cache body is: ${body}"
The cache step evaluates a key expression against each exchange. If the repository holds a live entry for that key, the step replaces the body with the cached bytes and returns Completed. If the key is absent or expired, the step runs the on_miss sub-pipeline, writes the resulting body to the repository with the given ttl, and returns the body to the pipeline. A key expression that evaluates to None bypasses the cache. The route runs the on_miss sub-pipeline without a lookup or a write-back.
With coalesce_misses: true, concurrent misses on the same key run the on_miss sub-pipeline once. The first miss becomes the leader. The other misses wait and share the leader's body and error. This prevents a stampede when many exchanges miss the same key at once.
The cache_invalidate step removes a key from the repository. Use it when an upstream event makes a cached entry stale. Set key to remove one entry. Set key_prefix to remove every entry whose key starts with that prefix, a namespace purge. Exactly one of the two is required. On success the step sets the CamelCacheInvalidatedCount exchange property. The value is 1 for an exact key and the removed count for a prefix. A backend without key iteration fails closed on key_prefix. The memory backend has no key iteration.
The cache_peek_stale step reads a cached entry and ignores its in-band expiry. This serves a post-expiry entry as a fallback when the source is unavailable.
cache_peek_stale on_miss policy
The cache_peek_stale step accepts an on_miss policy. The value "stop" is the default. On a miss the step sets PipelineOutcome::Stopped and the branch stops. The value "continue" passes the exchange through unchanged. Both values set the CamelCachePeekHit=false and CamelCachePeekStale=false exchange properties on a miss.
On a hit the step sets CamelCachePeekHit=true. It sets CamelCachePeekStale=true when the served entry is past its expires_at. A fresh entry sets CamelCachePeekStale=false. The properties enable a stale-while-revalidate (SWR) route: peek with on_miss: continue, branch on CamelCachePeekHit, fetch and cache on a miss, and serve the peeked body otherwise.
- cache_peek_stale:
key: "${header.cacheKey}"
on_miss: continue
- choice:
when:
- simple: "${exchangeProperty.CamelCachePeekHit} == false"
steps:
- set_body: "fresh-data"
- cache:
key: "${header.cacheKey}"
ttl: "5s"
on_miss:
- log: "SWR miss — fresh body stored"
otherwise:
- log: "Serving cached body: ${body}"
cache_clear and cache_stats
The cache_clear step removes every entry from the repository. It takes an optional repository name and defaults to "memory". The body passes through unchanged.
- cache_clear: {}
- cache_clear:
repository: "persistent"
The cache_stats step replaces the body with a JSON snapshot of the repository statistics. It takes an optional repository name and defaults to "memory".
- cache_stats: {}
The snapshot has these fields: repository, hits, misses, evictions, entries, peek_stale_served, invalidations, and bytes. The bytes field is the total stored payload size when the backend reports it. The redb backend reports a size. The memory backend reports null.
Stale-on-error with a circuit breaker
Compose cache_peek_stale with a route-level circuit_breaker to serve a stale entry when the downstream service fails. The fallback list holds a sub-pipeline. The breaker runs the fallback only while the circuit is open.
circuit_breaker:
failure_threshold: 1
open_duration_ms: 60000
fallback:
- cache_peek_stale:
key: "user-profile-42"
The route body wraps the upstream fetch in a cache step that stores the result under a static key. When the fetch fails failure_threshold times in a row, the circuit opens. While open, the fallback runs cache_peek_stale against the same static key and serves the last cached entry, even when that entry is past its TTL. On a miss (no entry), the default on_miss: stop policy stops the fallback cleanly. The exchange completes without a CircuitOpen error.
The fallback runs on routes with and without an error_handler. A failing fallback step follows the route's error handling. A route with an error_handler routes the failure through the handler. A route without one surfaces the raw error. See Circuit Breaker for the breaker states and Route structure for the fallback field.
The control plane also accepts the five cache steps. A CanonicalRouteSpec sent through RuntimeCommand::RegisterRoute supports cache, cache_invalidate, cache_clear, cache_stats, and cache_peek_stale in the route body and in circuit_breaker.fallback. An unknown step still fails with an error that names the step. See Control Bus and ADR-0016.
Use the Cache pattern when a route computes the same result more than once. API responses, database lookups, and transform-heavy pipelines benefit from caching.
The default repository is "memory" (moka-backed, size-eviction only). A persistent "persistent" repository (redb-backed) is available when [default.cache_repo] backend = "redb" is set. The redb backend survives process restarts. Its sweep task reclaims entries whose expires_at + stale_retention has passed. The memory backend does not run a sweep. Expired entries stay in memory until size pressure evicts them. cache_size is required for the redb backend (for example, cache_size = "256MiB"), bounding the redb page cache; sweep_interval is optional (default 1h).
A shared "redis" repository (redis-backed) is available when [default.cache_repo] backend = "redis" is set. Every process that connects to the same Redis shares one cache, so cached entries work across processes. Redis expires each key at expires_at + stale_retention. The redis backend runs no background sweep task. cache_size and sweep_interval do not apply to it. See ADR-0063 for the design.
[default.cache_repo]
backend = "redis"
url = "redis://127.0.0.1:6379"
stale_retention = "30m"
The Cache differs from the Claim Check and the Idempotent Consumer. All three use a repository trait. The Cache stores the full computed body with a TTL. The Claim Check stores the original payload without a TTL. The Idempotent Consumer stores only the deduplication key. A route that needs all three can chain them.
Per ADR-0056, the CacheRepository trait lives in camel-api, with memory and redb backends in camel-core and a redis backend in camel-redis-repo (ADR-0063). The trait stores CacheEntry { bytes, content_type, expires_at } with in-band expiry. The memory and redb backends do size-eviction only. The expires_at field drives get() misses and peek_stale() reads. Per ADR-0001, each cache step compiles into a Service<Exchange> step in the Tower middleware pipeline. The processor contract is documented in camel-processor/CONTEXT.md.
The example source is at examples/cache-example.