JaVers + Exposed DDD order flow
Latest stable Based on Javers release 0.2.1
This released example follows one order through a command-side Exposed table, a JaVers audit repository, Kafka domain events, and a Redis read model. Its value is not the amount of code. It makes the boundaries between source-of-truth state, audit history, event delivery, and query projection visible enough to test.
The problem the example solves
Section titled “The problem the example solves”PlaceOrderCommand and MarkOrderPaidCommand change an Order aggregate. A useful implementation must answer four different questions:
- What is the current order state?
- Who changed it, and what did JaVers record?
- Which domain event left the command side?
- What can the query side read without loading the aggregate?
The example assigns those jobs to different stores. Exposed persists the current Order; ExposedCdoSnapshotRepository persists JaVers commits and snapshots; Kafka carries OrderPlaced and OrderMarkedPaid; Redis stores OrderSummary. Redis is a projection, not the source of truth, and the JaVers tables are an audit store, not the command-side order table.
Released fixture boundary
Section titled “Released fixture boundary”Run the example from the repository root with JDK 21 and a Docker-compatible container runtime.
./gradlew :javers-exposed-ddd:testThe released tests do not start a PostgreSQL server. OrderCommandHandlerTest and OrderProjectionFlowTest use an in-memory H2 database with MODE=PostgreSQL. The projection test starts Kafka and Redis with the Projects Testcontainers launchers. Therefore the test boundary is:
| Responsibility | Released fixture | What it proves |
|---|---|---|
| order and JaVers tables | H2, PostgreSQL compatibility mode | command, snapshot, and history behavior in the example schema |
| domain-event transport | Kafka Testcontainer | encoded events can be published and consumed by order key |
| query projection | Redis Testcontainer | consumed events produce a readable OrderSummary |
Use a real PostgreSQL Testcontainer before adopting the schema or transaction assumptions in production. H2 compatibility mode does not prove PostgreSQL DDL, locking, isolation, query-plan, or driver behavior. The Exposed transaction-ownership guide explains application repository boundaries, while the Projects manual covers PostgreSQL, Kafka, Redis, and Testcontainers facilities.
Follow the command path
Section titled “Follow the command path”The smallest useful reading sequence is:
Order.ktdefines the aggregate invariant andPLACEDtoPAIDtransition.OrderCommand.ktseparates request intent from stored state.OrderCommandHandler.ktcreates the aggregate or loads and changes it.OrderRepository.ktstoresexample_orderthrough Exposed, then delegates the JaVers commit and event publication toAggregateRepository.OrderCommandHandlerTest.ktstates the expected command-side result.
For PlaceOrderCommand(order-1) with two 12.50 items, the test expects:
- current status
PLACEDand total25.00in the source-of-truth order; - one JaVers snapshot whose
domainEventTypeisOrderPlaced; - one published
OrderPlacedwith customer, total, aggregate ID, and fixed occurrence time.
After MarkOrderPaidCommand, another test expects current status PAID, two audit snapshots, and the publication order OrderPlaced, then OrderMarkedPaid.
Run only these command-side tests when changing aggregate or audit behavior:
./gradlew :javers-exposed-ddd:test --tests '*OrderCommandHandlerTest*'Follow the projection path
Section titled “Follow the projection path”Continue from the domain event to the query model:
OrderEvents.ktdefines audit attributes and event payload fields.OrderDomainEventJsonCodec.ktfixes the example-local JSON shape.OrderKafkaEventPublisher.ktsends synchronously and uses the order ID as the record key.OrderProjectionEventConsumer.ktpolls records and applies them in poll order.RedisOrderSummaryProjection.ktwrites one JSON document per order.OrderQueryService.ktreads only Redis.
Run the released end-to-end projection test:
./gradlew :javers-exposed-ddd:test --tests '*OrderProjectionFlowTest*'The test places two 15.00 items and polls Kafka until Redis contains an OrderSummary with customer customer-projection-1, total 30.00, and status PLACED. It then marks the order paid and waits for the same Redis key to show status PAID and the fixed update timestamp. The polling loop is an assertion aid, not an operational consumer loop.
Learn the stack in layers
Section titled “Learn the stack in layers”Use the example as a sequence rather than reading every class at once:
- javers-core: commit an object and query snapshots without persistence integration.
- javers-exposed: replace the in-memory JaVers repository with Exposed-backed commit and snapshot tables.
- javers-ddd: add aggregate persistence, audit metadata, and a synchronous
DomainEventPublisherafter each transition. - Kafka event: serialize
OrderPlacedorOrderMarkedPaidand publish with the stable order ID key. - Redis projection: consume the event into
OrderSummary, then query Redis independently from the command-side tables.
This order keeps the responsibility of each layer visible. It also makes failures easier to inject and reconcile. See DDD and CQRS, failure contracts, and the benchmark interpretation.
Production gaps and failure boundaries
Section titled “Production gaps and failure boundaries”The release example deliberately leaves production coordination to the application:
OrderRepository.persist, the JaVers commit, and Kafka publication are three sequential operations, not one transaction.- A JaVers failure can leave the order row committed without a matching audit snapshot.
- A Kafka failure can leave both order state and audit history committed without an event.
- There is no transactional outbox, inbox, retry queue, dead-letter policy, event ID, deduplication store, or replay checkpoint.
- Kafka ordering is expected only for records routed to the same partition by order ID. Topic and producer configuration still matter.
OrderProjectionEventConsumerdoes not manage offsets, retries, poison records, rebalances, or graceful shutdown for a service.OrderMarkedPaidrequires an existing Redis summary. A missing, duplicated, or out-of-order event can fail or regress the projection.- The JSON codec is example-local and has no schema version or compatibility policy.
- Redis stores the latest view only. It is not a substitute for the order table or JaVers history.
- The tests do not cover real PostgreSQL, process restarts, broker outages, concurrent commands, or projection rebuilds.
For production, introduce an explicit outbox and consumer idempotency policy, version event schemas, persist replay progress, and provide a projection rebuild path from a durable event or audit source. Instrument the three command-side steps separately.
Exercises that expose the boundaries
Section titled “Exercises that expose the boundaries”- Replace H2 with a PostgreSQL Testcontainer and verify schema creation, timestamps, and concurrent updates.
- Fail the JaVers repository after
example_ordercommits; record how reconciliation finds the missing audit version. - Fail Kafka publication, add an outbox row in the same database transaction as the order, and publish it later.
- Add an event ID and make Redis projection updates idempotent under duplicate delivery.
- Deliver
OrderMarkedPaidbeforeOrderPlaced; decide whether to retry, park, or rebuild the summary. - Delete the Redis key and rebuild it without reading the command-side table directly.
- Version the event JSON and keep old payloads readable in a codec compatibility test.
The full released dependency set is pinned in build.gradle.kts. Keep the example’s claims at this release boundary when comparing it with later repository code.