the/experts. Blog

Cover image for Taking advantage of Metadata in Axon Framework
Mitchell Herrijgers
Mitchell Herrijgers

Posted on • Originally published at blog.codecentric.de

Taking advantage of Metadata in Axon Framework

Welcome to Axon Framework 102, where we will be deep diving into many interesting challenges you might encounter when working with Axon Framework. This blog post will dive into using a great feature of Axon Framework to your advantage: Metadata!

About the Axon Framework series

I have been working with Axon Framework for two years now. I have written my thesis about strangling a remaining monolith at the Port of Rotterdam Authority and I am currently doing exactly that with Axon Framework. We have seen interesting challenges, such as a great number of events, long replays and privacy regulations. I believe those challenges to be relevant to all people using Axon Framework, or maybe even all people using Event-Sourcing! This is why I’m sharing these challenges and possible solutions with you; so you can be inspired to make a solution just as good, or even better.

This blog series will require some base experience with Axon Framework. If you have no experience with it yet, it is a very cool framework and you will probably enjoy learning and using it. So get your hands dirty and come back in a bit. This blog series will be waiting for you.

Metadata in Axon Framework

All commands, queries, and events in Axon Framework are messages that can have metadata attached to them. It is a map structure where you can add additional data about the event that is not part of your domain model. Authentication and audibility can be one of them; it’s great to know which user executed an action on your system, but it might be irrelevant for your domain. You could add the username to the command and the event, but this pollutes your domain model. In addition, it is not very DRY since you will have to repeat this for every command and event.

Metadata can help you achieve these goals while not repeating yourself or polluting your domain model. When dispatching a command, or applying an event, you can provide a map of the metadata you want to pass along with it. This is done in the next example, where my profile is created by a user named “hansklok”.

val metaData = MetaData.from(mapOf("username" to "hansklok")) 
val command = CreateProfileCommand("morlack", "Mitchell Herrijgers") 
commandGateway.sendAndWait<Any>(GenericCommandMessage(command, metaData))
Enter fullscreen mode Exit fullscreen mode

This data can later be referenced in all command- and event handlers (both in and outside of the aggregate) by defining MetaData as a parameter. Axon will automatically revolve the Metadata for you when you require it in your handlers. The following event handler logs the username present in the MetaData:

@EventHandler
fun handle(event: ProfileCreatedEvent, metadata: MetaData) {
    logger.info("Got event by {}: {}", metadata["username"], objectMapper.writeValueAsString(event))
}
Enter fullscreen mode Exit fullscreen mode

This example, however basic, demonstrates how useful metadata can be, especially when you have certain data which should be added to all events.

When to use

MetaData should be used for data that is not important to your domain model. I recommend using MetaData only for types of data that should be present for all events. For example, this can be the users’ name for auditing reasons, or the command name that was the precursor to the event to make debugging easier. On my current project for the Rotterdam Port Authority I have used metadata for the following data:

  • Storing the username
  • Storing the command name
  • Storing the command payload
  • Some identifying information of the entity in the aggregate

Storing this data along with the event has made troubleshooting and debugging the application much easier. However, we want to make this as low-maintenance as possible! We don’t want other developers to constantly think about all the MetaData they have to attach to their commands and events, right? This is why we can write Correlation Data Providers, which add the MetaData automatically and validate the needed properties.

Correlation Data Providers

Correlation Data Providers will provide MetaData properties based on the command message when applying events and are managed by Axon to run for for you. By letting Axon run it for you with every command there is no need to programmatically add MetaData to every event, which in turn reduces the chance of making a mistake greatly! Take a look at the following CorrelationDataProvider:

/**
 * Adds metadata about the triggering command to the metadata. This metadata is later passed to each event when applying it.
 * There is a small blacklist to prevent the eventstore from blowing up too quickly
 */
class CommandInfoCorrelationDataProvider : CorrelationDataProvider {
    // The following commands are too large to serialize them to the metadata. 
    val payloadBlacklist = listOf(
        ImportShipVisitDeclarationFromLegacyCommand::class,
        UpdateShipVisitFromLegacyCommand::class,
        DeclareShipVisitCommand::class,
        CreateShipVisitCommand::class,
    )
    override fun correlationDataFor(message: Message<*>): MutableMap<String, *> {
        val metadataMap = mutableMapOf<String, Any>()
        metadataMap["commandName"] = message.payload::class.simpleName as String
        if (!message.metaData.containsKey("username")) {
            throw IllegalStateException("You should supply username in your command!")
        }
        metadataMap["username"] = message.metaData["username"] // copy over username from comand
        if (message.payload !is ShipVisitCommand) {
            return metadataMap
        }

        if (!payloadBlacklist.contains(message.payload::class)) {
            metadataMap["commandPayload"] = message.payload
        }
        return metadataMap
    }
}
Enter fullscreen mode Exit fullscreen mode

This Correlation Data Provider adds the Command’s name and payload to the event’s MetaData. It also copies over the username Now, if we want to see the cause of an event, all we have to do is look in the Metadata column of our event store. We will never have to think about adding those metadata properties ever again since it is automatically done for us. Axon has some other great examples on this, and even provides some auto-configured out-of-the-box!

It can always be easier

As developers we always want our lives to be easier, and most of us want this to as easily readable as possible. So, knowing that, which of the following event handlers do you prefer?

@EventHandler
fun handle(event: ProfileCreatedEvent, metadata: MetaData) {
    logger.info("Got event by {}: {}", metadata["username"], objectMapper.writeValueAsString(event))
}
@EventHandler
fun handle(event: ProfileCreatedEvent, @MetaDataValue("username") username: String) {
    logger.info("Got event by {}: {}", username, objectMapper.writeValueAsString(event))
}
@EventHandler
fun handle(event: ProfileCreatedEvent, @Username username: String) {
    logger.info("Got event by {}: {}", username, objectMapper.writeValueAsString(event))
}
Enter fullscreen mode Exit fullscreen mode

They all function the exact same way with the same result but use a different method. The first injects the entire MetaData object, of which you can then get anything. The second one uses the MetaDataValue annotation to let Axon Framework fetch us a specific key out of the MetaData. The last one uses a custom annotation and ParameterResolver to resolve a username.

Personally, I prefer the last one with the specific annotation. It’s immediately clear that we are using the username while handling this event, instead of the entire MetaData. The event handler also does not need to know the String constant “username” and we can easily find the places where we use the annotation, and thus the username. By default, Axon Framework supplies multiple annotations to handle events with ease. I have created a little overview of them for you:

Annotation Description
@Timestamp Injects the timestamp of the event message as an Instant

Writing your own ParameterResolver

So how do we write a ParameterResolver so we can use the annotation? First, we need to define an annotation that it preserved at runtime, which looks like this in Kotlin:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Username
Enter fullscreen mode Exit fullscreen mode

Then, we write a ParameterResolver to resolve it out of the message. In our case it looks like this:

In the ParameterResolver we can use any attribute of the message of application we want. Say you have a file in your classpath and want to inject that value into your event handlers, you can! It’s up to you whether you should.
Last but not least we need to create a ParameterResolverFactory and register it with Axon Framework. The job of this Factory is to return an instance of our ParameterResolver when a matching method parameter is encountered. In our case we want it to match with an annotation, like this:


class HamisParameterResolverFactory : ParameterResolverFactory {
    private val usernameMetadataResolver = UsernameMetadataResolver()

    override fun createInstance(executable: Executable?, parameters: Array<out Parameter>, parameterIndex: Int) = when {
        parameters[parameterIndex].isAnnotationPresent(Username::class.java) -> {
            usernameMetadataResolver
        }
        else -> null
    }
}
Enter fullscreen mode Exit fullscreen mode

We can of course expand this when statement when we want additional annotations in our application. We can also match the parameter on its’ class. Note that Axon Framework allows only one custom factory, in which you can match as many parameters as you want.

Now we are ready to register the resolver with Axon Framework. This is done through a META-INF file containing the FQDN of the class you want to register. This file should be present in the META-INF/services classpath directory with the name “org.axonframework.messaging.annotation.ParameterResolverFactory”. Axon Framework scans this file and registers our factory on boot. Now we are ready to use our custom annotation.

Metadata validation

It’s great we can use this MetaData, but to be really useful we want it to be always present. So let’s validate that! We can write a dispatch or handler interceptor to validate the MetaData. I prefer the dispatch interceptor since it is also under my control and it has not invoked the aggregate yet when validating, presenting the least strain on my application. Take a look at the Interceptor:

@Component
class MetadataCommandInterceptor<T : Message<*>>(
    private val commandBus: CommandBus
) : MessageDispatchInterceptor<T> {
    @PostConstruct
    fun register() {
        commandBus.registerDispatchInterceptor(this as MessageDispatchInterceptor<in CommandMessage<*>>)
    }

    override fun handle(messages: MutableList<out T>?): BiFunction<Int, T, T> {
        return BiFunction { index, message ->
            require(message.metaData.containsKey("company")) { "Every command should have company defined in its metadata!" }

            message
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

After Spring created the interceptor it registers itself to the command bus. For every outgoing command message, it calls the handle function. You can also modify the messages in the process. In our case, we want to throw an exception when the company of the invoker is not present in the MetaData. Usually, this should not happen in production since you have always through of adding it, but for testing and validation purposes during testing this is a great approach.

Demo

You can see a demo of this validation by starting the demo application here. It also contains all code included earlier in this blog post to give you a great reference to start from. You can always ask questions either here, on github, or using an old-school e-mail.

Conclusion

Metadata is a great way to add data to your events that is related to non-functional requirements, such as keeping the invokers’ username for auditing reasons. We can make our by registering DataCorrelationProviders, custom annotations to inject the MetaData where we need it and we can validate that the MetaData is always present. This can be of great help during our Axon Framework journey!

Discussion (0)