Occurrent 0.20.5 is released with the following changes:
- Added
adaptandcomposedecider combinators to thedsl/decidermodule.adaptwidens a decider over a feature’s own command and event subtypes into one over the shared supertypes, ignoring foreign events and treating foreign commands as no-ops, so aDecider<CourseCommand, CourseState, CourseEvent>can run against a service over a commonDomainEvent. It is available as a Java static takingClasstokens and as a Kotlinreifiedextension that readscourseDecider.adapt()at the call site.composecombines several feature deciders into one whose state is the product of the individual states. Each command routes to the decider that recognizes it, each state slice evolves independently, and the composed decider is terminal once every constituent is. The two and three decider overloadsadapteach decider for you and return a typedPairorTriple, so you can writecompose(courseDecider, studentDecider, enrollmentDecider)over the feature deciders directly. The two decider case also has an infix form,courseDecider compose studentDecider. For four or more, a varargcompose(d1, d2, d3, d4, ...)and acompose(list)form both return a positionalCompositeStateand take deciders that already share the command and event type.- Both combinators are pure decider algebra and add no new dependency to the module.
- The decider
executeextensions onApplicationServicenow widen a decider’s event type for you.- A feature decider over its own narrow event type can be passed straight to an
ApplicationServiceover a broader event type, without callingadaptoradaptEventsfirst. This removes a papercut, because an injected application service is typically over the broadest event type, so a feature decider previously always had to be widened by hand at every call site. - Added
adaptEvents, the event-only counterpart toadapt. It widens only the event type and leaves the command type unchanged. Theexecuteextensions use it internally.
- A feature decider over its own narrow event type can be passed straight to an
- The CloudEvent converter can now truncate the CloudEvent time to a configured precision.
Instant.now()andOffsetDateTime.now()carry nanoseconds on modern JVMs, whichTimeRepresentation.DATEcannot store, so an append failed with a “contains micro-/nanoseconds” error. The Jackson CloudEvent converter builder gainstimePrecision(ChronoUnit), and the Spring Boot starter adds theoccurrent.cloud-event-converter.time-precisionproperty (aChronoUnit, for examplemillis).- When that property is unset and the event store
time-representationisDATE, the converter now defaults to truncating toMILLIS, so the common case works with no configuration.RFC_3339_STRINGkeeps full precision.
- The Spring Boot starter’s fallback CloudEvent converter now registers the Jackson modules found on the classpath.
- The default Jackson 3 converter built a bare
ObjectMapper, and Jackson 3, unlike Jackson 2, does not auto-register modules. So the fallback converter could not serialize or deserialize Kotlin data classes orjava.timetypes even when their modules were on the classpath, failing with a “no Creators” error. The fallback now usesJsonMapper.builder().findAndAddModules()to discover and register them. Supplying your ownCloudEventConverterortools.jacksonObjectMapperbean still overrides this.
- The default Jackson 3 converter built a bare
- Fixed a remaining silent event loss in
CatchupSubscriptionModelat the handover from the catch-up phase to the live subscription.- The delta reconciliation sized its read from a count of matching events and then read the newest N of them. An event written in the window between that count and the read shifted the newest-N window forward and pushed the oldest during-catch-up event out of the read. That event sat at or before the live subscription’s resume position, so the live subscription did not redeliver it either, and it was lost. This is the residual case left open by the 0.20.4 fix, which closed the clock-skew variant but not the count-to-read window.
- The reconciliation now re-reads the recent tail until the matching count stops growing, so an event that arrives in the count-to-read window is picked up by a later pass instead of being skipped. Overlapping passes are deduplicated through the handover cache, so at-least-once delivery is preserved without introducing duplicates.
- Upgraded Spring Boot from 4.0.4 to 4.1.0. This pulls in Spring Framework 7.0.8, Spring Data 2026.0.0, Reactor 2025.0.x, the MongoDB driver 5.8.0, Kotlin 2.3.21, and Jackson 2.21.4 / 3.1.4 transitively.
- The explicit Reactor and MongoDB driver version overrides were removed from the root build, so their versions are now governed by the Spring Boot dependency BOM.