Occurrent 0.20.0 is released with two major themes:

  • Spring Boot 4.0.4 and Jackson 3 as the new main path for Spring-based Occurrent applications
  • StreamReadFilter, ExecuteOptions, and related ApplicationService/Kotlin improvements for more explicit and efficient command handling

These changes work well together: Boot 4 + Jackson 3 is now the preferred stack for new applications, while StreamReadFilter and ExecuteOptions make it easier to express filtered reads and synchronous side effects explicitly at the ApplicationService boundary.

Spring Boot 4 and Jackson 3

Occurrent 0.20.0 upgrades the Spring Boot line to 4.0.4 and introduces a new Jackson 3-native converter lane.

Highlights:

  • Added org.occurrent:cloudevent-converter-jackson3 as the new Jackson 3-native CloudEvent converter artifact.
  • The existing org.occurrent:cloudevent-converter-jackson artifact remains available as a compatibility lane for incremental migration.
  • The Spring Boot starter now supports both lanes while keeping the same Spring-facing API, including @EnableOccurrent and the occurrent.* property namespace.
  • Examples and starter-oriented setup are now updated to the Spring Boot 4 / Jackson 3 path.
  • Remaining example usage of Jackson default typing has been replaced with explicit CloudEvent converter configuration.

Upgrade guidance:

  • New applications should use Spring Boot 4 together with Jackson 3.
  • Existing applications that already depend on the Jackson 2 converter API can continue to use that lane while migrating gradually.

StreamReadFilter and ExecuteOptions

Occurrent 0.20.0 also introduces a set of related API improvements around filtered stream reads and clearer ApplicationService execution options.

Why this matters:

  • StreamReadFilter lets supported event stores read only the subset of events that a command or use case actually depends on. This can reduce unnecessary IO and deserialization work when a stream contains many event types but a particular operation only needs a few of them.
  • ExecuteOptions makes it possible to combine filtered reads and synchronous side effects in a single, explicit ApplicationService API instead of relying on many overloaded methods.
  • Kotlin users now also get clearer collection-oriented names such as executeSequence(...) and executeList(...), plus direct-import helpers like sideEffect(...), filter(...), and namespaced typed filters under ExecuteFilters.

Highlights:

  • Added org.occurrent.eventstore.api.StreamReadFilter.
  • Added optional capability interface org.occurrent.eventstore.api.blocking.ReadEventStreamWithFilter.
  • Added optional capability interface org.occurrent.eventstore.api.reactor.ReadEventStreamWithFilter.
  • Added filtered stream-read support in:
    • InMemoryEventStore
    • MongoEventStore
    • SpringMongoEventStore
    • ReactorMongoEventStore
  • Added org.occurrent.application.service.blocking.ExecuteOptions.
  • Added ApplicationService.execute(streamId, executeOptions, domainFunction) for blocking application services.
  • Added Kotlin helpers:
    • executeSequence(...)
    • executeList(...)
    • options()
    • filter(...)
    • sideEffect(...)
    • sideEffectOnSequence(...)
    • sideEffectOnList(...)
  • Kotlin collection-oriented application-service usage now centers on executeSequence(...) and executeList(...).
  • Added namespaced Kotlin typed filter helpers under ExecuteFilters, for example ExecuteFilters.type<NameDefined>() and ExecuteFilters.excludeTypes(NameDefined::class, NameWasChanged::class).

For synchronous side effects, the preferred API is now ExecuteOptions.sideEffect(...). The older executePolicy(...) and executePolicies(...) helpers still exist, but they are no longer the primary recommended approach for new code.

A simple Java example:

WriteResult result = applicationService.execute(
        streamId,
        ExecuteOptions.<DomainEvent>options()
                .filter(StreamReadFilter.type("com.acme.NameDefined"))
                .sideEffect(newEvents -> newEvents.forEach(this::publish)),
        domainFn
);

A simple Kotlin example:

applicationService.executeSequence(
    streamId,
    options().filter(ExecuteFilters.type<NameDefined>()).sideEffect(
        { event: NameDefined -> publish(event) }
    )
) { events ->
    decide(events)
}

Read the updated documentation for details, examples, and guidance on when filtered reads are safe to use.