Skip to content

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

3D workbench illustration of robotic engineers sorting JaVers snapshots into Exposed, Redis, and Kafka stations
The word persistence is shared. The read, write, and recovery roles are not.

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.

Persistence role map from a JaVers commit to Exposed, Redis, and Kafka
Choose by repository role, not by module name.

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.

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.

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.

Composite repository delegating reads to the primary and forwarding writes to the primary and ordered secondaries
Reads stay on the primary, while writes reach the primary before ordered secondary fan-out.
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.

CriterionExposed JDBCRedisKafka
Main roleDurable snapshot historyFast snapshot reads and projectionsSnapshot event stream
ReadsJaVers queriesFast latest/history readsEmpty/false/zero
WritesExposed transaction pathLIST/multimap plus indexesKafka publish acknowledgement
RecoveryDatabase backup/restoreRedis persistence or rebuild policyConsumer replay/projection policy
Operational workDatabase schema and capacityKey, TTL, and persistence policyTopics, consumers, and replay
Best fitAudit history with SQL operationsRead-heavy history/projectionDownstream 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.

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.