Upgrading to Axon 4 from 3.x

Jan-Hendrik Kuperus
6 min readMay 17, 2019

--

Photo by Martin from Pexels.com

Today I set out to upgrade one of my apps to Axon 4. We had been diligently tracking the 3.4-branch, but now it was high time to put in the effort of upgrading.

Update July 22nd: Since the upgrade, we’ve sporadically found “mystery” events. These events seemed to be copies of earlier events and appeared on apparently random times. These events even appeared on entities that had been marked archived in our domain. None of these could receive commands through any conventional way.

After digging deeper, we noticed all of our Saga’s were replayed once the application was restarted for whatever reason. This was a bit puzzling, until I reread my own comment on the fact that Saga’s are now handled by TrackingEventProcessors instead of SubscribingEventProcessors. We needed an explicit TokenStory to replace the default InMemoryTokenStore. Details below in the section on Event Processing.

Update June 7th: After upgrading, I found my TrackingEventProcessor instances would no longer process events. Turns out, the restructuring of packages carries through to TrackingToken implementations. Axon 4 therefore could not deserialise the old tokens and kept looping on this. See the section Event Handling section below for a way to fix this.

Since I could not find a lot of information on what I would run into during this upgrade, I decided to keep notes and make sure anyone following me on this path would have one more article with useful tips to read.

To give you a little hope: the upgrade seemed like a lot of work, but in the end I was actually surprised to see the changes contained mostly to updating imports and some configuration.

(Small disclaimer: my app is written in Scala, so the code samples are in Scala, but the same changes obviously apply to Java/Kotlin code)

So what changed?

Well, quite a bit actually. Most notably is the name and package-structure. The folks at AxonIQ have decided to include their Axon Server in the ecosystem surrounding their framework. This is the main reason they’ve changed the name “Axon Framework” to just “Axon”.

Such a change in its own would not have had a lot of impact on existing applications or the use of their name in the wild, but they timed it with a major release ensuring the name-change coincides with a bunch of new features. So remember, no more Axon Framework, just Axon.

New Module Structure

The changes that will take you some effort to work through are all related to the restructuring of the packages. Previously we had to import axon-core and that was pretty much it. This artefact has grown considerably in the past few years and its apparent modularity was not obvious since we had to include the whole thing in any project that even just brushed up against Axon-concepts.

Instead of axon-core, you can now import the parts you need for your specific project:

  • axon-configuration — Contains all of the configuration classes for Axon
  • axon-modelling — Contains everything you need to build Aggregates and Sagas
  • axon-eventsourcing — Contains infrastructure components to handle Command and Query models and load Aggregates from a stream of events
  • axon-messaging — Contains all components to support command, query and event messaging. If you’re building a service that only responds to events, this is all you need, very good for building small services to maintain a read model
  • axon-spring — Contains helper classes to integrate with Spring
  • And a few more…

Streamlined Configurer / Configuration / Builder API

One of the problems with Axon 3.x was the way the configuration was built. If you didn’t use the Spring Boot auto configuration, you had to wrestle several different styles of creating infrastructure components. In some cases you even had to extend Axon native classes to attempt to override a default.

In Axon 4 the configuration API has been streamlined. Wherever you see a xxxConfiguration, there is now also an xxxConfigurer and it is required to create the former. Additionally, most components now support the Builder pattern, setting sensible defaults and allowing full customisability beyond those defaults.

Alright, enough talk…

Let’s get to the things I actually had to change to go from Axon 3.x to Axon 4. Please be aware that this is in no way an exhaustive guid to upgrade. This is a list of the things I had to change to get the application working again. If you used to configure things in custom ways, expect more things you may need to rework.

Configuration / Modules:

  • Instead of axon-core, our command handling application now has these Maven dependencies:
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-eventsourcing</artifactId>
<version>${axon.version}</version>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-modelling</artifactId>
<version>${axon.version}</version>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-configuration</artifactId>
<version>${axon.version}</version>
</dependency>
  • It is now considerably easier to provide a JacksonSerializer with just a custom ObjectMapper, thanks to the builder pattern:
JacksonSerializer.builder()
.objectMapper(localObjectMapper)
.build()
  • The same goes for classes like JdbcEventStorageEngine and JdbcSagaStore
  • A lot of imports will break because of the restructuring. Luckily some of these can be handled with regexes and sed. Run this command on a prompt inside your project: (works just as well on *.java files)
$ find . -name "*.scala" | xargs sed -i.bkup -e "s/import org.axonframework.commandhandling.model/import org.axonframework.modelling.command/g" -e "s/import org.axonframework.eventhandling.saga/import org.axonframework.modelling.saga/g"

Sagas:

  • Previously, Sagas had to have a SagaConfiguration which you had to register as a module. This is no longer the case, they can be directly registered on the EventProcessing component:
configurer.eventProcessing().registerSaga(classOf[MySaga])
  • Saga’s are now by default handled by a TrackingEventProcessor. This means they are no longer handled in the same UnitOfWork as the Command that triggers the Saga’s event. A big gotcha here is that they now need a TokenStore. If your application already used TrackingEventProcessor, you probably have this configured already. If not, just add one:
val tokenStore = JdbcTokenStore
.builder()
.connectionProvider(connectionProvider)
.serializer(tokenSerializer)
.build()
tokenStore.createSchema(tokenTableFactory)configurer.registerComponent(classOf[TokenStore], _ => tokenStore)
  • Since handling of Sagas is now no longer guaranteed to happen in the same UnitOfWork as the Command/Event that started it, this makes it harder for integration tests to reliably check the number of events that were emitted. I fixed this by waiting a few milliseconds to see if my expected number of events had already been emitted:
def testCreation(): Unit = {
// Mock MVC code to call creation endpoint
awaitEvents(4) // Assertions
}
private def awaitEvents(expectedNrOfEvents: Int, maxWaitMillis: Long = 1000L): Unit = {
val startTime = System.currentTimeMillis()
while(publishedEvents.size() < expectedNrOfEvents &&
(System.currentTimeMillis() - startTime) < maxWaitMillis) {
Try(Thread.sleep(200))
}
}

Event Handling:

  • If you are using a ListenerInvocationErrorHandler, please note the contract has changed. Its third argument is no longer an EventListener, but an EventMessageHandler
  • The EventHandlingConfiguration and EventProcessingConfiguration have been combined into the EventProcessingModule. As far as I can see, you can simply move all of the configuration performed on these two classes as-is to the EventProcessingModule and register that
  • If you’re using GenericDomainMessage or DomainEventMessage, be aware these imports have also changed. You can run this to fix most of them:
$ find . -name "*.scala" | xargs sed -i.bkup -e "s/eventsourcing.Domain/eventhandling.Domain/g" -e "s/eventsourcing.Generic/eventhandling.Generic/g"
  • Update June 7th: Because packages have been restructured, your old TokenEntry records will no longer deserialise in Axon 4. You need to update the tokenType field on the existing tokens if you want to continue using them. For example, I had to run these SQL commands to get the processors to run again:
UPDATE TokenEntry SET tokenType = 'org.axonframework.eventhandling.GapAwareTrackingToken'

Tests:

  • Validating handler return values is no longer supported. These are now wrapped in CommandResultMessage objects. You can use the corresponding expectResultMessagePayload -functions now. A little bit of regex will help you:
$ find . -name "*.scala" | xargs sed -i.bkup -e "s/expectReturnValueMatching/expectResultMessagePayloadMatching/g"  -e "s/expectReturnValue/expectResultMessagePayload/g"
  • Difference in Handling : (custom) ParameterResolvers are now required to match a dispatched Message through their canHandle method. If they don’t, you will receive an exception saying “No Handler for Command”. A bit confusing, but it is an improvement, because previously your ParameterResolver would simply inject null, but you may be puzzled why some tests suddenly fail
  • The TestFixture has had a few updates as well. One noticeable change that broke a test was the fact that a missing handler would cause the when -method to fail with a NoHandlerForCommandException. It would not make it to the expectException-validator method. It does now, so my intercepts have been replaced with this:
//    intercept[NoHandlerForCommandException] {
labelManagementFixture
.givenNoPriorActivity()
.when(SomeUnknownCommand("1234"))
.expectException(classOf[NoHandlerForCommandException])

Concluding…

That wasn’t so bad, was it? Now we can start playing with the more exciting new features introduced in the fourth major version of Axon, like programmatic replay.

If you’re looking for the official news on what’s new in Axon 4, take a look at the release notes: https://axoniq.io/docs/axon-4

Cheers!

— JH

--

--

Jan-Hendrik Kuperus

Hi! I’m the Founder and Director of Yoink. I love writing code, tweaking it, beautifying it. I'm an all-round coder and a Professional Amateur Baker 😁🎂