DAC Cache live initializer
Run cache reads, refreshes, database-write scenarios and failure cases against this initializer. The tables show the source and cached values; the log drawer shows what each action did.
Live initializer
Use the buttons below to try each behaviour. The tables compare the source data with the cache, and the live log shows what each request did. Use clear caches whenever you want to start again.
docker run -p 6379:6379 valkey/valkey:8.0) and restart the initializer to try all panels.
This initializer uses an in-memory store, so Docker is not required. To test several service
instances with a shared L2 cache, set dac.initializer.embedded-backbone=false and
VALKEY_HOST, then run it against Valkey.
The initializer data
All panels below use the same example data. This initializer behaves like a small trading service with:
- Product categories — a reference table, cached as the static region
productCategories(declared by one annotation, whole table preloaded, versioned, no expiry). - Products — the sellable catalog, cached as the static region
products(declared as one bean, indexed by code and by category). - Client transactions — data that changes while the service is running. A bounded useful set can be loaded first, a miss loads one row from the database, and a business event can populate any additional rows it needs. Each row expires after 30 seconds in this initializer.
The tables below are live: source of truth (the "database") on the left of each row, what the cache holds right now on the right — including the remaining TTL. They refresh every 2 seconds; every action in the panels below changes them.
Product catalog — source vs. cached snapshot
Client transactions — database vs. cache (watch "expires in")
Inside Redis
This is what the cache actually writes. Every application gets its own keyspace root
({application name}:cache — here dac-initializer:cache), and under it every
region follows one naming scheme:
{root}:{region}:current which snapshot version is live (static regions only)
{root}:{region}:contract shape fingerprint — boot fails when two apps disagree
{root}:{region}[:vN]:id:{key} one cached value (static keys carry the version)
{root}:{region}[:vN]:idx:{index}:{value} index entry pointing at an id
{root}:{region}:stream write-behind queue (a Redis stream)
{root}:channel:refresh refresh-event stream
{root}:lock:{role} leader locks and per-key refresh locks
Static values have no expiry — a refresh writes a whole new version and flips the pointer. Transactional values carry their TTL, which you can watch tick down here. Negative markers ("this row does not exist") and index entries are visible too. The initializer runs on the embedded in-memory store; against a real Valkey the keys look exactly the same.
Show the live keys
Refreshes every 2 seconds while open. Read a transaction or run any example, then look here — values, indexes, negative markers and locks appear as the engine writes them.
Static: the product catalog
This example shows a small reference table loaded as one cache snapshot. Read the static data guide ←
Try it: look products up from the cache
Data first
Then the example — look products up from the cache
Each click also writes one line into the live log below — no database call appears, because the snapshot answers.
Refresh & partial refresh
Change the source, then refresh either the whole snapshot or one region. Read the refresh guide ←
Try it: insert a product, then refresh the region
Data first
Then the example — insert a product, watch it stay out of the cache, refresh
Step 1 makes the table above show the new row with not in snapshot;
step 3 flips it to in snapshot and bumps the region version. Partial
refresh of just the categories region: POST /api/admin/master-data-cache/reload/productCategories.
Event-driven refresh
Publish an event after a committed change to repair the affected cached data. Read the event guide ←
Try it: rename a category, repair the cache with one event
Data first
Then the example — rename a category at the source, repair the cache with one event
After step 1 the categories table shows stale — the cache
still serves the old name. Step 2 repairs exactly that one row in place: no endpoint, no full
reload, the region version does not move. The same works for transactions:
POST /initializer/refresh-events?region=transactions&key=TX-1002. An event without a
key refreshes the whole region.
Transactional data loading
Follow these three steps in order: warm the bounded working set, keep one-row database fallback for an unexpected miss, then use business events only to extend the warmed set when needed.
Warm the working set
Required default. Load a bounded set such as the last 24 hours before accepting traffic. This improves the hit rate without copying the whole transaction table.
Keep the miss loader
Correctness fallback. If a row was not warmed, the cache loader reads that one row and stores it for the next request. This should be measured as an exception, not the normal path.
Extend after a business event
Add only when needed. Let an application event select an additional bounded set, then place those rows in the cache.
Success check: never preload an unbounded transaction table. Measure the hit rate and source-load count. Adjust the window until ordinary client traffic is served from the warmed cache and source reads are exceptional.
Try it: warm the default 24-hour window
The initializer database contains three rows from the last 24 hours and one older row. Warming selects only the recent rows. The read then shows a cache hit instead of a database load.
Try it: handle a cache miss by reading one database row
Watch the row move from the database into the cache
- Step 1: the log shows
MISS → loaded from the database; the row above turns cached with a 30s countdown. - Step 2: same read, now answered in microseconds from this instance's memory — the response
shows
servedFromand the milliseconds. - Do nothing for 30s: the countdown reaches 0 and the entry is gone — not cached. That is the TTL doing automatic eviction.
- Read once more: a fresh database load, and the cycle starts again.
Try it: populate rows after a business event
TX-1003 is older than the 24-hour default window, but it belongs to CLIENT-200. This initializer event selects the rows for CLIENT-200. A real application can use any event and query that fits its own domain. The following read is a cache hit.
Transactional data refresh after database changes
Change the database first. After the commit succeeds, update or remove the cached copy. Do not wait for the TTL to repair a normal insert, update, or delete.
Insert
Commit the new row. Push the committed value to the cache when it will be read immediately; otherwise let the first read load it.
Update
After commit, replace the cached value when you already have it. Eviction is the simpler safe option when the writer cannot build the full snapshot.
Delete
Delete the database row, then evict the key. This prevents any instance from serving the deleted value from memory or Redis.
Try it: insert, update, and delete one row
Run the buttons from left to right. The live table shows the database and cache after each step.
Caching "not found"
A short negative-cache entry prevents repeated database reads for the same missing key. Read the not-found guide ←
Try it: read a transaction that does not exist
Data first
Then the example — read a transaction that does not exist
Watch dbLoaderCalls under the table: it rises on step 1 and stays flat
on step 2, until the 15s negative entry expires.
Staleness & eviction
This example shows why a committed database change must refresh or evict the cached copy. Read the staleness guide ←
Try it: make it stale, see it, fix it
Data first — watch the "stale" badge
Then the example — make it stale, see it, fix it
Normal JPA entity writes use CacheWriteBinding and need no manual
eviction call. This panel deliberately acts like a batch job or another writer that bypasses
Hibernate, so it calls regionEvictions.evict("transactions", txId). Step 2+4 in one
go: direct-db?…&evict=true. The event-driven alternative to step 4 is publishing
{region: transactions, key: …} on the refresh channel.
Batched writes (write-behind)
Write-behind combines repeated changes to the same key before writing the newest value to the database. Read the write policy guide ←
Try it: a burst of 50 status changes becomes one database write
Data first — watch dbWriteBatches under the table
Then the example — a burst of 50 status changes
Right after step 1 the table shows the cache ahead of the database
(db behind for ~2s); after the flush, dbWriteBatches
went up by one and dbRowsWritten by one row — not fifty.
Leader election
Only one service instance runs each scheduled cache job at a time. Read the leader guide ←
Try it: which work does this instance currently own?
Configuration
Check the settings the application is actually using for every region. Read the settings guide ←
Show the effective settings per region (all TTLs visible here)
Statistics
Use these counters to see where reads are served and how often the database is used. Read the statistics guide ←