Upgrading to Axon 4 from 3.x
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 Axonaxon-modelling
— Contains everything you need to buildAggregate
s andSaga
saxon-eventsourcing
— Contains infrastructure components to handleCommand
andQuery
models and loadAggregates
from a stream of eventsaxon-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 modelaxon-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
andJdbcSagaStore
- 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,
Saga
s had to have aSagaConfiguration
which you had to register as a module. This is no longer the case, they can be directly registered on theEventProcessing
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 sameUnitOfWork
as theCommand
that triggers the Saga’s event. A big gotcha here is that they now need aTokenStore
. If your application already usedTrackingEventProcessor
, 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 anEventListener
, but anEventMessageHandler
- The
EventHandlingConfiguration
andEventProcessingConfiguration
have been combined into theEventProcessingModule
. As far as I can see, you can simply move all of the configuration performed on these two classes as-is to theEventProcessingModule
and register that - If you’re using
GenericDomainMessage
orDomainEventMessage
, 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 correspondingexpectResultMessagePayload
-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)
ParameterResolver
s are now required to match a dispatched Message through theircanHandle
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 injectnull
, 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 thewhen
-method to fail with aNoHandlerForCommandException
. It would not make it to theexpectException
-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