Skip to content

Bluetape4k JaVers Part 1: Audit and Diff Overview

3D workbench illustration of small robotic auditors inspecting JaVers snapshots and diff cards
Auditing stores changes so someone can read them later.

This is Part 1 of the bluetape4k-javers series. Part 1 sets up the audit and diff model, Part 2 compares Exposed, Redis, and Kafka persistence roles, and Part 3 maps the workshop and DDD/CQRS examples onto a practical command flow.

The easiest way to add audit history is to create one history table per entity. That works for a while. Then fields grow, nested objects appear, and readers start asking who changed what and why. At that point the audit code tends to repeat itself across every entity.

bluetape4k-javers solves that problem with JaVers’ object audit model. JaVers commits an object, stores snapshots of its state, and computes diffs between objects or versions. This repository adds Kotlin/JVM-friendly helpers, codecs, persistence adapters, and DDD examples around that model.

It helps to understand JaVers itself before looking at the bluetape4k adapters. JaVers is not tied to JPA revision tables in the way Hibernate Envers is. It is an object auditing and diff library: the application commits a domain object, JaVers inspects the object graph, records changed properties, and stores versioned snapshots. That makes it useful for Kotlin/JVM services that want to explain how a domain object changed, not merely which database row was updated.

FeatureWhat it gives you
Object auditingCommit object state as versioned snapshots
Object diffCompute property-level differences between objects or versions
JQL queryQuery history by instance, class, commit, or version range

This is also different from CDC. Debezium-style CDC turns database log changes into a stream. JaVers starts from the application object and commit metadata. Use CDC when every database change must be streamed outward. Use JaVers when readers need object history and human-readable diffs.

Flow from domain object to JaVers commit, snapshot repository, and query or diff reader
The application commits aggregates, and readers query snapshot history or object diffs.
ConceptMeaning
CommitA change unit with author, commit id, and commit properties
CDO SnapshotThe state of one object at one version
GlobalIdJaVers identity such as Order/1
DiffProperty-level changes between objects or snapshots
ShadowAn object reconstructed from snapshot history

A JaVers commit is more than a log line. It carries object state, changed properties, and commit metadata together.

val product = Product(1L, "Widget", BigDecimal("9.99"), "Tools")
javers.commit("alice", product)
val snapshots = javers.findSnapshots(
QueryBuilder.byInstanceId(1L, Product::class.java).build()
)

That gives the application one audit boundary instead of a separate hand-written history path for every entity.

bluetape4k-javers does not rewrite JaVers. It keeps the JaVers model and adds the pieces that Kotlin/JVM services in the bluetape4k stack keep needing.

ModuleResponsibility
javers-coreExtensions, codecs, and repository base classes
javers-exposedExposed JDBC CDO snapshot repository
javers-persistence-redisLettuce and Redisson Redis snapshot repositories
javers-persistence-kafkaWrite-only Kafka snapshot stream repository
javers-dddAggregate, domain event, repository, and publisher helpers
examples/javers-exposed-dddExposed + JaVers + event projection example

The shared base is AbstractCdoSnapshotRepository. It handles snapshot encode/decode, commit head state, sequence handling, and query filtering. Concrete repositories implement storage-specific operations such as saveSnapshot, loadSnapshots, and getKeys.

abstract class AbstractCdoSnapshotRepository<T : Any>(
protected val codec: JaversCodec<T>,
) : CdoSnapshotRepository {
override fun persist(commit: Commit?) {
// commit snapshots -> repository-specific saveSnapshot(...)
}
}

The workshop ProductAuditService shows the smallest useful shape. It commits the product to JaVers and upserts the current row through Exposed.

Sequence diagram showing ProductAuditService committing to JaVers, persisting snapshots, and updating ProductTable
Even the small example is easier to read when current state and audit history have separate responsibilities.
fun save(author: String, product: Product) {
javers.commit(author, product)
transaction {
ProductTable.upsert {
it[id] = product.id
it[name] = product.name
it[price] = product.price
it[category] = product.category
}
}
}

Diffs can be computed without persisting either side:

val diff = javers.compare(
oldProduct,
oldProduct.copy(price = BigDecimal("7.50")),
)

The tests check INITIAL, UPDATE, and TERMINAL snapshots, latest snapshot lookup, and property-level value changes.

JaVers fits best when object-level history matters. If a business flow needs to show changed properties, author metadata, latest snapshots, and reconstructed object state, JaVers gives that model directly. If all the service needs is a simple append-only event row, a small audit table may still be enough.

Audit history is useful only when it can be read and explained later. bluetape4k-javers brings JaVers commits, snapshots, and diffs into the bluetape4k stack so Kotlin services can avoid rebuilding the same audit path per entity.

Next, we choose where those snapshots should live.

Comments

Leave a note or reaction with your GitHub account.