Skip to content

Spring Boot starter

Spring Boot · Added in 0.14.0 · Modules: occurrent-mongodb-spring-boot-starter

Use the “Spring Boot Starter” project to bootstrap Occurrent quickly if using Spring Boot 4. Add the following to your build script:

<dependency>
<groupId>org.occurrent</groupId>
<artifactId>occurrent-mongodb-spring-boot-starter</artifactId>
<version>0.34.0</version>
</dependency>

Next create a Spring Boot application annotated with @SpringBootApplication as you would normally do, and also add the @EnableOccurrent annotation (located in package org.occurrent.springboot.mongo.blocking). Occurrent will then configure the following components automatically:

  • A Spring MongoDB Event Store instance (EventStore)

  • A Spring CheckpointStorage instance

  • A durable Spring MongoDB competing consumer subscription model (SubscriptionModel)

  • A Jackson-based CloudEventConverter. From 0.20.1, the starter autoconfigures the Jackson 3 lane by default. Existing applications can still use the Jackson 2 compatibility lane during migration, but in that case you need to define your own CloudEventConverter bean explicitly. It uses a reflection based cloud event type mapper that uses the fully-qualified class name as cloud event type (you should absolutely override this bean for production use cases). You can do this, for example, by doing:

    @Bean
    public CloudEventTypeMapper<GameEvent> cloudEventTypeMapper() {
    return ReflectionCloudEventTypeMapper.simple(GameEvent.class);
    }

    This will use the “simple name” (via reflection) of a domain event as the cloud event type. But since the package name is now lost from the cloud event type property, the ReflectionCloudEventTypeMapper will append the package name of GameEvent when converting back into a domain event. This only works if all your domain events are located in the exact same package as GameEvent. If this is not the case you need to implement a more advanced CloudEventTypeMapper such as

    class CustomTypeMapper : CloudEventTypeMapper<GameEvent> {
    override fun getCloudEventType(type: Class<out GameEvent>): String = type.simpleName
    override fun <E : GameEvent> getDomainEventType(cloudEventType: String): Class<E> = when (cloudEventType) {
    GameStarted::class.simpleName -> GameStarted::class
    GamePlayed::class.simpleName -> GamePlayed::class
    // Add all other events here!!
    ...
    else -> throw IllegalStateException("Event type $cloudEventType is unknown")
    }.java as Class<E>
    }

    or implement your own custom CloudEventConverter.

  • A GenericApplicationService instance (ApplicationService)

  • A subscription dsl instance (Subscriptions)

  • A query dsl instance (DomainEventQueries)

  • When the DCB capability is enabled, the DCB counterparts too: a DCB application service (DcbApplicationService), a DCB subscription dsl (DcbSubscriptions), and a DCB query dsl (DcbDomainEventQueries)

  • Support for annotations

For most new Spring Boot applications, the recommended setup is:

  • Spring Boot 4
  • occurrent-mongodb-spring-boot-starter
  • occurrent-cloudevent-converter-jackson3 if you configure the converter explicitly

If you are upgrading an existing application that still depends on the Jackson 2 converter API, you can continue to use that compatibility lane while migrating incrementally. The Spring Boot starter will not autoconfigure the Jackson 2 converter for you, so you must register a CloudEventConverter bean yourself, for example:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import org.occurrent.application.converter.CloudEventConverter;
import org.occurrent.application.converter.jackson.JacksonCloudEventConverter;
import org.occurrent.application.converter.typemapper.CloudEventTypeMapper;
@Bean
CloudEventConverter<GameEvent> cloudEventConverter(ObjectMapper objectMapper, CloudEventTypeMapper<GameEvent> cloudEventTypeMapper) {
return new JacksonCloudEventConverter.Builder<GameEvent>(objectMapper, URI.create("urn:example:game"))
.typeMapper(cloudEventTypeMapper)
.build();
}

Any user-defined CloudEventConverter bean takes precedence over the starter’s built-in fallback converter, regardless of whether your custom converter uses Jackson 2 or Jackson 3.

You can of course override other beans as well to tailor them to your needs. See the source code of org.occurrent.springboot.mongo.blocking.OccurrentMongoAutoConfiguration if you want to know exactly what gets configured automatically.

It’s also possible to configure certain aspects from the application.yaml file under the “occurrent” namespace. For example:

occurrent:
application-service:
enable-default-retry-strategy: false

You can code-complete the available properties in Intellij or have a look at org.occurrent.springboot.common.OccurrentProperties to find which configuration properties that are supported.

The settings that only apply to a MongoDB event store or a MongoDB subscription model have mongodb in their key:

KeyDefaultDeprecated key it replaces
occurrent.event-store.mongodb.collectioneventsoccurrent.event-store.collection
occurrent.event-store.mongodb.time-representationDATEoccurrent.event-store.time-representation
occurrent.subscription.mongodb.collectionsubscriptionsoccurrent.subscription.collection
occurrent.subscription.mongodb.restart-on-change-stream-history-losttrueoccurrent.subscription.restart-on-change-stream-history-lost

The deprecated keys still work, and will be removed two releases after 0.34.0.

You can set both the old and the new key as long as they have the same value. That way an old key you have not found yet, in an environment variable for example, does not break a configuration you have already migrated. If the two values differ, the application fails at startup with an error that names both keys.

The org.occurrent.UpgradeToOccurrent_0_34 OpenRewrite recipe renames the keys in your .properties and .yaml files, as described in section 4 of the upgrade guide. It can’t rename environment variables such as OCCURRENT_EVENT_STORE_COLLECTION, so you have to find those in your deployment configuration and rename them yourself.

Deferring Subscription Startup (Added in 0.32.0)

Section titled “Deferring Subscription Startup”

By default the starter creates every subscription and starts it during context refresh, before your application is ready to receive requests. occurrent.subscription.mode controls whether that happens at all:

occurrent.subscription.mode=auto
ModeWhat it means
autoThe default. Every subscription is created and started during context refresh.
manualEvery subscription is registered, but none of them runs until you start it yourself.
disabledNo subscription beans are created at all.

manual registers a subscription rather than starting it. Nothing is subscribed, no competing-consumer lease is taken, and no change stream or catch-up replay begins until you start it yourself, by calling resumeSubscription(id) on the injected SubscriptionModel. This is the same mechanism the subscription life cycle uses to hold subscriptions back between tests, made available for production startup too, for an application that only wants its subscriptions running once, say, it has won a leader election.

Withholding is not the same as never having registered. A subscription that resolves to the model’s own default start position has that position fixed the moment it is registered, not the moment it is started, so a subscription that has never run before does not silently skip whatever is written while it waits. A subscription registered with an explicit start position of its own, a replay or an explicit checkpoint, writes nothing at registration and keeps doing what it asked for. Once a subscription has run at least once, it resumes from its own stored checkpoint regardless of how long it was withheld this time around.

manual also withholds sagas and push projections, not only the @Subscription, @StreamSubscription, and @DcbSubscription annotations. A @Saga’s timer poller does not fire until its saga’s subscription is started. A withheld @Projection(source = Source.PUSH) bound to a DomainEventFeed is not registered on the feed until you start it, so it does not take part in the feed’s catchUpAll(). If a projection is started later than the rest of the feed, for example a manual subscription resumed after the feed has already caught up, catchUpAll() will not replay it, since it only re-runs projections that were already live. Replay it on its own with catchUp(String), passing that projection’s id.

Boot no longer validates subscription wiring under manual. A bad filter or an unsupported start position used to fail during context refresh. Under manual it instead fails the first time the subscription is started, which for a leader-election deployment can be well after the application has already started serving traffic.

Outside Spring, ManualStartSubscriptionModel in occurrent-subscription-api-blocking gives you the same thing, minus the registration-fixes-the-position guarantee above. ManualStartSubscriptionModel.stoppedByDefault(subscriptionModel) wraps any subscription model so that subscriptions register withheld and start only when you say so, but a first run starts from the wrapped model’s own default at the moment it’s started, so events written between registration and start do not reach it. Pass a GlobalCheckpointSource and a CheckpointStorage to the three-argument stoppedByDefault overload to get that guarantee outside Spring too. It is also what the JUnit extension’s stopped-by-default testing is built on.

occurrent.subscription.mode replaces the deprecated occurrent.subscription.enabled (true maps to auto, false to disabled). The deprecated property still works during the deprecation window, and setting both is fine as long as they agree, so a leftover environment variable does not break an otherwise-migrated configuration. Setting both to values that disagree fails startup, naming both values in the error. See the upgrade guide for the OpenRewrite recipe that renames the property for you.

If your application is reactive (Spring WebFlux with reactive MongoDB), use the reactive starter (org.occurrent:occurrent-mongodb-reactive-spring-boot-starter) and annotate your application with @EnableOccurrentReactive (package org.occurrent.springboot.mongo.reactor) instead of @EnableOccurrent. It auto-configures the reactive counterparts of everything the blocking starter sets up: a reactive EventStore, a reactive transaction manager, a reactive application service (both the stream and the DCB application service), the query DSLs, a reactive subscription model backed by CheckpointStorage, and the reactive StreamSubscriptions and DcbSubscriptions DSLs. @EnableOccurrent and @EnableOccurrentReactive can now sit on one classpath together, since the beans each contributes are disjoint, so an application that genuinely needs both a blocking and a reactive stack can annotate with both instead of picking one.

Deferring subscription startup with occurrent.subscription.mode=manual works the same way here, with one difference that follows from the reactive SubscriptionModel having no competing-consumer layer. There is nothing to wrap. The starter simply hands back a model that is already stopped, and you resume each subscription with resumeSubscription(id). A @Projection(source = Source.PUSH) fed by a reactive DomainEventFeed is withheld by a registry rather than by the model itself, so starting it runs the work that registering it would otherwise have done immediately. The reactive DomainEventFeed has the same per-id catchUp(String) as the blocking one, for a projection started after the rest of the feed has already caught up.