Getting started
Four steps take a Spring Boot service from no cache to a preloaded reference table and a cached transactional row. Each step ends with a check you can see in the log, and each one leaves the application working exactly as before if the cache is off.
You need a Spring Boot service, a database repository, and a Redis or Valkey connection. For a local run this is enough:
docker run -d -p 6379:6379 valkey/valkey:8.0
Step 1 — Add the dependency
<dependency>
<groupId>com.juliusbaer.dac</groupId>
<artifactId>cache-starter</artifactId>
<version>${revision}</version>
</dependency>
Applications inside the client-notification-handler repository use
${revision} — the dac modules ride the same version train as the rest of the build.
An application outside the repository pins the released version instead.
The starter brings cache-core (the cache plane and the data plane), cache-binding-spring
(Spring wiring, write bindings, @Stitch) and cache-masterdata-starter
(preloading of static tables, refresh endpoints). Add one build-time dependency so the cache
records are generated from your entities. It is provided and sits on the compiler's
processor path, the same way Lombok does:
<dependency>
<groupId>com.juliusbaer.dac</groupId>
<artifactId>cache-snapshot-processor</artifactId>
<version>${revision}</version>
<scope>provided</scope>
</dependency>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></path>
<path><groupId>com.juliusbaer.dac</groupId><artifactId>cache-snapshot-processor</artifactId><version>${revision}</version></path>
</annotationProcessorPaths>
</configuration>
</plugin>
Step 2 — Point at Redis
spring:
application:
name: product-service # becomes the Redis key root "product-service:cache"
data:
redis:
host: localhost
connect-timeout: 250ms # fit these to your request SLA
timeout: 500ms
Cache manager started successfully with 0 region(s). The engine is running; nothing is
cached yet. If Redis is unreachable at startup the application still starts and reads from its
source. The timeouts also cap how long an L1 miss waits if Redis fails later.Step 3 — Cache static data
Pick a small reference table that changes rarely. One annotation on the JPA entity is the whole
cache declaration. The build generates ProductCategorySnapshot — the record that is
stored in the cache — together with a BINDING constant that describes the region:
This step is the minimal
path. How versioned snapshots work, the startup modes (attach /
reload-if-stale), scheduled refresh and the index rules are all on
Static data.
@Entity
@CacheSnapshot(region = "productCategories",
indexes = @SnapshotIndex(name = "byName", property = "name"))
public class ProductCategory {
@Id private Long id;
private String name;
private String description;
// getters
}
Then give the region a typed entry point — one small class, no region bean. Because the entity is JPA, the master-data starter finds its repository, preloads the whole table at startup and keeps it versioned; the facade only reads:
@Component
public class ProductCategoryCache extends AbstractCache<Long, ProductCategorySnapshot> {
private final ProductCategoryRepository repository;
public ProductCategoryCache(CacheSupport support, ProductCategoryRepository repository) {
super(ProductCategorySnapshot.BINDING, support);
this.repository = repository;
}
public Optional<ProductCategorySnapshot> byName(String name) {
return byIndex("byName", name)
.or(() -> repository.findByNameIgnoreCase(name).map(ProductCategorySnapshot::from));
}
}
Read it anywhere:
productCategoryCache.byName("Bonds");
Region 'productCategories' loaded snapshot v… (3 entries). Call the read twice — the
database is not queried at all; both answers come from the cache.The .or(repository…) branch is the deliberate safety path. It runs only when the region is off or not loaded yet, so a service that is still warming up, or a deployment without Redis, keeps answering from the database. Static data explains when the fallback is reached.
Step 4 — Cache transactional data
Do not copy a large transaction table into the cache. Mark the entity transactional, give the region a TTL, and tell the facade how to load one missing row. The TTL is declared in code as the safe default and can be overridden per environment.
TTL and negative caching, eviction after commit through the Hibernate write binding, refresh events, the local fallback and caching an external application's data are all on Transactional data.
@Entity
@CacheSnapshot(region = "transactions", type = RegionKind.TRANSACTIONAL, ttl = "PT5M")
public class Transaction {
@Id private String id;
private String clientId;
private BigDecimal amount;
private String status;
// getters and setters
}
@Component
public class TransactionCache extends AbstractCache<String, TransactionSnapshot> {
private final TransactionRepository repository;
public TransactionCache(CacheSupport support, TransactionRepository repository) {
super(TransactionSnapshot.BINDING, support);
this.repository = repository;
}
@Override
protected Optional<CacheLoader<String, TransactionSnapshot>> loader() {
return Optional.of(id -> repository.findById(id).map(TransactionSnapshot::from));
}
@Override
protected Optional<TransactionSnapshot> loadFromSource(String id) {
return repository.findById(id).map(TransactionSnapshot::from);
}
}
Keep the cache fresh after writes with one Hibernate write binding. Ordinary JPA inserts, updates and deletes then evict the key after a successful commit; a rollback leaves the cached value untouched. Nothing else in the service needs a cache call:
@Bean
CacheWriteBinding<Transaction> transactionWrites() {
return CacheWriteBinding.evict("transactions", Transaction.class, Transaction::getId);
}
dac:
cache:
regions:
transactions:
l2-ttl: ${TRANSACTION_CACHE_TTL:30s}
Cache miss in region 'transactions' reached the backing loader; the second is answered
from memory in well under a millisecond. Wait for the TTL and read again — one fresh database load.Where to go next
AbstractCache, and how RegionBinding describes a region.
Static dataStep 3 in depth: versioned snapshots, startup modes, scheduled refresh, indexes.
Transactional dataStep 4 in depth: TTLs, eviction after commit, refresh events, external lookups.
Live initializerRun every behaviour above on the demo with a live log.