CoCache
wow-cocache connects Wow query/event contracts to CoCache's Cache SPI. Use it to cache an aggregate read model by ID and either evict on domain events or update on state events. It does not create a cache backend or register a business cache automatically.
Features
QueryGatewayCacheSourceloads from a localSnapshotQueryGateway.QueryApiCacheSourceloads from a remoteReactiveSnapshotQueryApiand converts 404 to empty.EvictStateCacheRefresherconsumes domain events and evicts.SetStateCacheRefresherconsumes state events and sets, or evicts deleted state.
The application owns cache keys, DTO conversion, TTL, backend, capacity, consistency target, and bean registration. The CoCache backend owns eviction, TTL, and storage semantics.
Installation
implementation("me.ahoo.wow:wow-cocache")Starter has no cocache-support capability or wow.cocache.* auto-configuration. Minimum runtime wiring is an existing Cache<K,D>, a source when miss loading is required, and an explicitly registered refresher when event-driven refresh is required.
Getting Started
Choose the aggregate, key type, cache DTO, and refresh strategy first, then register only the components needed.
1. Configure the CoCache Cache
Create Cache<String, OrderView> through the application's existing CoCache configuration. wow-cocache does not choose Redis, a local map, or another backend, and does not duplicate backend connection or serialization validation.
2. Register a Cache Source (cache miss loader)
@Bean
fun orderCacheSource(queryGateway: SnapshotQueryGateway<OrderState>) =
QueryGatewayCacheSource(
snapshotQueryGateway = queryGateway,
stateToCacheDataConverter = { snapshot -> OrderView(snapshot.state) },
)Default LoadCacheSourceConfiguration uses timeout=10s plus CoCache default TTL/amplitude. loadCacheValue is a synchronous CoCache SPI and waits for the Reactor source. Timeout or query errors propagate. Do not call the blocking miss loader directly from a Reactor event loop.
3. Register a Cache Refresher (event-driven refresh)
@Bean
fun orderCacheRefresher(
cache: Cache<String, OrderView>,
) = EvictStateCacheRefresher<String, OrderState, OrderView>(
namedAggregate = ORDER,
cache = cache,
)A refresher is a MessageFunction and must enter the application's event-processor discovery/registration flow. Constructing it does not subscribe to a bus.
Cache Refresh Strategies
Eviction is simpler and reloads on the next read. Active set reduces misses but depends on the state-event bus, converter, and TTL. Both are eventually consistent cache refresh, not the same transaction as EventStore.
Evict Strategy
EvictStateCacheRefresher has functionKind=EVENT, converts aggregateId.id to the key by default, and calls cache.evict. Provide keyConvert for non-String keys instead of relying on the unchecked default cast.
Set Strategy
SetStateCacheRefresher has functionKind=STATE_EVENT. Non-deleted state passes through StateToCacheDataConverter and is stored as DefaultCacheValue.ttlAt; deleted state is evicted. TTL defaults come from CoCache and can be overridden in the constructor.
Cache Sources
A source only loads a miss and converts a DTO. Authorization, tenant/space scope, and not-found semantics must come from the called Gateway/API contract.
QueryGatewayCacheSource
It creates a single query by aggregateId. Empty results return a null cache value. Query errors and timeouts do not masquerade as cache misses.
QueryApiCacheSource
It combines ReactiveSnapshotQueryApi with StateCacheSource. getById not-found becomes empty; other HTTP and decode failures propagate. Do not cache remote unavailability as “not found.”
Verified failures and boundaries: an empty source writes no value; deleted state evicts; miss-loader timeout/errors propagate; cache operation failures fail the refresher and enter event-processing recovery.
Focused check:
./gradlew :wow-cocache:checkNext, read Query and Projection, then verify consistency with real TTL/eviction behavior from the selected cache backend.