Try it live

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.

Choose an exampleUse the menu to jump directly to the behaviour you want to test.
Run the stepsEach panel explains which buttons to use and what result to expect.
Watch the resultCompare source and cache tables, then open the live log when you need more detail.

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.

cache: checking… keys: — transactions TTL: —

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:

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.

2

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.

3

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

  1. Step 1: the log shows MISS → loaded from the database; the row above turns cached with a 30s countdown.
  2. Step 2: same read, now answered in microseconds from this instance's memory — the response shows servedFrom and the milliseconds.
  3. Do nothing for 30s: the countdown reaches 0 and the entry is gone — not cached. That is the TTL doing automatic eviction.
  4. 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.

1

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.

2

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.

3

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 ←

Show the per-region counters