Skip to content
Javers docs0.2

javers-ddd

Latest stable Based on Javers release 0.2.1

javers-ddd connects source-of-truth aggregate persistence, a JaVers commit, and a domain-event publisher call. It fixes the order in which those contracts are invoked; the adapter still decides when delivery happens and whether the caller observes a delivery failure.

dependencies {
implementation("io.github.bluetape4k.javers:javers-ddd")
}

Choose this module when aggregates have stable IDs, JaVers history should carry domain-event metadata, and the service can explicitly own recovery between persistence, audit, and publication. Use javers-core directly if the service does not need the aggregate repository and publisher contracts.

The central APIs are AggregateRoot, AggregateRepository, DomainEvent, and DomainEventPublisher.

Runnable quick start: aggregate commit metadata

Section titled “Runnable quick start: aggregate commit metadata”
import io.bluetape4k.javers.ddd.*
import org.javers.core.Javers
import org.javers.core.JaversBuilder
import org.javers.core.metamodel.annotation.Id
import java.time.Instant
data class Order(
@Id override val id: Long,
var status: String,
) : AggregateRoot<Long>
data class OrderPlaced(
override val aggregateId: Long,
override val occurredOn: Instant = Instant.now(),
override val attributes: Map<String, String> = mapOf("channel" to "web"),
) : DomainEvent
class OrderRepository(javers: Javers) :
AggregateRepository<Order, Long>(Order::class.java, javers) {
private val rows = mutableMapOf<Long, Order>()
override fun persist(aggregate: Order) = aggregate.copy().also { rows[it.id] = it }
override fun findById(id: Long) = rows[id]?.copy()
}
val javers = JaversBuilder.javers().registerEntity(Order::class.java).build()
val repository = OrderRepository(javers)
repository.save(Order(1, "PLACED"), "order-service", OrderPlaced(1))
val snapshot = repository.loadHistory(1).single()
check(snapshot.commitMetadata.properties["domainEventType"] == OrderPlaced::class.qualifiedName)
check(snapshot.commitMetadata.properties["event.channel"] == "web")

For one event, toJaversProperties() records domainEventType, aggregateId, occurredOn, and namespaced event.* attributes. For multiple events it records only domainEventCount and comma-separated domainEventTypes; per-event attributes are not retained. This mapping is defined in DomainEvent.kt.

save calls subclass persist, then javers.commit, then eventPublisher.publishAll. publishAll invokes each publisher in collection order, but actual delivery semantics depend on the implementation. The no-op, function, and composite publishers run immediately and propagate an exception from publish. The Spring application-event and Kafka adapters use publishAfterCommit, so an active Spring transaction can defer their work until afterCommit. The Kafka adapter starts KafkaTemplate.send() without waiting for the returned future, so a broker-side send failure is not necessarily reported through save.

If domain persistence fails, no audit or publisher call follows. If the JaVers commit fails, domain state may already exist. An immediate publisher failure can leave domain state and audit in place while later publishers or events have not run. Deferred or non-blocking adapters require their own delivery-failure monitoring and recovery.

load checks the source-of-truth repository first and falls back to the latest JaVers shadow. loadHistory returns snapshots for the aggregate ID. A shadow is useful for audit reconstruction, but it should not silently replace a required operational database.

The provided no-op, function, and composite publishers are immediate adapters. Spring application events, Spring Kafka, and NATS surfaces require their matching runtime dependencies and have adapter-specific timing. None is a transactional outbox.

Record a command/event ID when retries or deduplication matter. Monitor failures separately for domain persistence, JaVers commit, and event publication. Reconciliation should compare aggregate ID, audit version, and external event state rather than repeating the whole command blindly.

Terminal window
./gradlew :javers-ddd:test

The release test AggregateRepositoryTest.kt proves saved state, commit metadata, event publication, history, and shadow fallback. Add failure injection between all three save steps.

  • It does not supply aggregate persistence.
  • It does not provide an outbox, retry queue, or deduplication store.
  • It does not guarantee exactly-once event delivery.
  • It does not make domain, audit, and messaging resources atomic.

Continue with DDD and CQRS, failure contracts, and the released Javers Exposed DDD example.