Bluetape4k JaVers Part 2: Exposed, Redis, and Kafka Persistence

Part 1 covered JaVers commits, snapshots, and diffs. Part 2 asks an operational question: where should those snapshots live?
bluetape4k-javers provides Exposed JDBC, Redis, and Kafka repositories. They should not be chosen
as interchangeable storage engines. Exposed is durable SQL history, Redis is a fast snapshot read
path, and Kafka is a write-only event stream.

In prose, the map says this: a JaVers commit is one audit boundary, but the operational destination depends on the role. If people will query history, use a durable store. If screens read latest snapshots often, add a fast read store. If downstream consumers need audit events, emit a stream. Expecting one repository to do all three jobs usually makes the design blurry.
Exposed JDBC
Section titled “Exposed JDBC”ExposedCdoSnapshotRepository stores commit metadata and CDO snapshots through Exposed tables.
It creates CommitTable and CdoSnapshotTable, then stores global id, commit id, version, type,
encoded state, changed properties, and managed type. Set createSchemaOnEnsure = false when an
external migration tool owns schema creation.
val snapshotRepository = ExposedCdoSnapshotRepository(database)snapshotRepository.ensureSchema()
val javers = JaversBuilder.javers() .registerJaversRepository(snapshotRepository) .registerEntity(Order::class.java) .build()This is a good choice when audit history should use the same SQL backup and recovery system.
Repository operations run inside Exposed transaction {} blocks. Application data changes and
JaVers commits do not automatically become one distributed transaction, so the calling layer must
still define the required transaction boundary.
The Redis module provides Lettuce and Redisson implementations. The Lettuce repository stores
snapshots newest-first in Redis LIST keys and maintains global id and sequence indexes in HASH
keys. The Redisson repository uses RListMultimap for snapshots and RMap for commit sequences.
Both default to LZ4 + Fory encoding.
val repo = LettuceCdoSnapshotRepository("order", redisClient)val javers = JaversBuilder.javers() .registerJaversRepository(repo) .build()Redis is a good fit for short history and latest-state lookups. It can be the primary repository, but its AOF/RDB, replication, backup, and recovery objectives must satisfy the audit-retention requirements. Keeping SQL as the source of truth and Redis as an explicit projection or cache often makes operational ownership clearer.
The Lettuce implementation groups snapshot writes, indexes, and commit sequence updates with
MULTI/EXEC. It uses a dedicated write connection and a lock to prevent transaction commands from
interleaving. The Redisson implementation can leave partial data across its data structures, so use
Lettuce when commit-level atomicity inside Redis is required.
KafkaCdoSnapshotRepository is intentionally write-only. saveSnapshot publishes a snapshot event
to an explicitly configured topic or the KafkaTemplate default topic, using the global id as the
record key. Read methods return empty, false, or zero, and the first read-path call logs a warning.
val repo = KafkaCdoSnapshotRepository(kafkaTemplate)val javers = JaversBuilder.javers() .registerJaversRepository(repo) .build()
javers.commit("system", order)That contract matters. Kafka is useful for downstream consumers, projections, and audit pipelines.
It is not the repository that answers “give me the latest snapshot for this object.” Pair it with
Exposed or Redis when query reads are required. Use KafkaCdoSnapshotProjector when Kafka records
must be replayed into a read-capable repository.
Combining Persistence Roles
Section titled “Combining Persistence Roles”Production systems often need several roles. CompositeCdoSnapshotRepository accepts one primary
repository as the read source of truth and an ordered list of secondary write targets. With Exposed
as primary and Kafka as secondary, SQL remains queryable while the same commit is published as an
event. Redis and Kafka can both be secondary targets when the service needs a read projection and
an event stream.

val repository = CompositeCdoSnapshotRepository( primary = exposedRepository, secondaryRepositories = listOf( redisProjectionRepository, kafkaRepository, ), options = CompositeCdoSnapshotRepositoryOptions( writeFailurePolicy = CompositeCdoSnapshotFailurePolicy.FAIL_FAST, ),)Reads delegate to the primary. Writes update the primary first, then invoke secondaries in order.
The default FAIL_FAST policy stops at the first secondary failure. BEST_EFFORT attempts every
secondary and then throws an aggregate exception. It does not ignore failures or provide retries.
This is not a distributed transaction. A primary commit can already be visible when a secondary fails. Retry, duplicate-event handling, alerting, and recovery remain application responsibilities.
Selection Table
Section titled “Selection Table”| Criterion | Exposed JDBC | Redis | Kafka |
|---|---|---|---|
| Main role | Durable snapshot history | Fast snapshot reads and projections | Snapshot event stream |
| Reads | JaVers queries | Fast latest/history reads | Empty/false/zero |
| Writes | Exposed transaction path | LIST/multimap plus indexes | Kafka publish acknowledgement |
| Recovery | Database backup/restore | Redis persistence or rebuild policy | Consumer replay/projection policy |
| Operational work | Database schema and capacity | Key, TTL, and persistence policy | Topics, consumers, and replay |
| Best fit | Audit history with SQL operations | Read-heavy history/projection | Downstream audit/event pipeline |
Do not choose Kafka alone merely because the module sits under persistence. Kafka is a stream
adapter, not a query store. Pair it with a read repository or use KafkaCdoSnapshotProjector to
apply events to one.
Source Links
Section titled “Source Links”- Repository: bluetape4k-javers
- Exposed repository:
ExposedCdoSnapshotRepository.kt - Lettuce repository:
LettuceCdoSnapshotRepository.kt - Redisson repository:
RedissonCdoSnapshotRepository.kt - Kafka repository:
KafkaCdoSnapshotRepository.kt - Composite repository:
CompositeCdoSnapshotRepository.kt - Kafka projector:
KafkaCdoSnapshotProjector.kt
Closing
Section titled “Closing”Persistence choice in bluetape4k-javers is about read, write, and recovery responsibilities.
Exposed keeps durable history near SQL, Redis serves fast snapshot reads, and Kafka streams
snapshot events to other consumers. When a service needs several roles, compose an explicit
primary and ordered secondaries, then define recovery for partial secondary failure.
Comments
Leave a note or reaction with your GitHub account.