Anatomy of a Message
In Axon, all communication between components is done with explicit messages, represented by the Message interface.
A Message consists of a message type, payload, metadata, and an identifier.
Message type
Every message in Axon Framework has a MessageType, which identifies what type of message it is - what structure and content you can expect.
The MessageType consists of two parts:
-
A
QualifiedName- The business or domain name of the message type. -
A
version- The version of the message structure.
This decouples the message type from its Java class representation.
The Java class is not used to identify the type of a message.
Instead, the MessageType identifies what structure the message has, independent of which Java class is used to represent it.
Many messages can have the same MessageType - it’s a type identifier, not an instance identifier.
For example, all "OrderPlaced v1.0" events share the same MessageType, even though each is a distinct message with its own unique identifier.
Benefits of MessageType
This approach provides several advantages:
-
Flexibility in representation: Different services can use different Java classes to represent the same message type.
-
Decoupling: Services don’t need to share concrete message class implementations.
-
Business alignment: Message names can reflect business terminology rather than technical class names.
-
Version management: All message types (commands, events, queries) can be versioned consistently.
Defining message types
The component in charge of deciding on the MessageType, is the MessageTypeResolver. The default MessageTypeResolver checks for message-specific annotation.
These annotations are the @Command, @Event, or @Query annotation, which you can attach on your classes:
@Event(name = "OrderPlaced", version = "1.0")
public class OrderPlacedEvent {
private final String orderId;
private final BigDecimal amount;
// Constructor, getters...
}
The annotation parameters control the MessageType:
-
namespace- The namespace or bounded context (defaults to the package name) -
name- The business name (defaults to the simple class name) -
version- The message version (defaults to0.0.1)
If you don’t specify these annotations, Axon will use the fully qualified class name as the QualifiedName and 0.0.1 as the version, maintaining backward compatibility.
|
Beyond their technical purpose, these annotations serve as documentation in your code. They make it immediately clear that a class represents a command, event, or query, improving code readability and making your domain model’s intent explicit. |
Defining the namespace with @Namespace
You can use the @Namespace annotation as a convenient way to define the namespace of the MessageType for multiple message classes at once.
As such, it acts as a fallback for whenever you are (1) using annotations and (2) have not specified the namespace attribute directly on each @Command, @Event, or @Query annotation.
The @Namespace annotation can be applied at different levels:
-
On a class - Defines the namespace for that specific message class. Technically no different from using the
@Command,@Event, or@Queryannotation. -
On an enclosing class - Defines the namespace for all nested classes within it
-
On a package (via
package-info.java) - Defines the namespace for all classes in that package -
On a module (via
module-info.java) - Defines the namespace for all classes in that module
To automatically set the namespace for several messages inside the same package, we could annotate the package-info.java file as shown below:
@Namespace("orders")
package com.example.orders.api;
import org.axonframework.messaging.core.annotation.Namespace;
The different levels described also represent the search order. When resolving the namespace for a class, the following order is used until a non-empty value is found:
-
The
@Namespaceannotation on the class itself -
The
namespaceattribute on the message annotation (@Command,@Event, or@Query) -
The
@Namespaceannotation on enclosing classes (from innermost to outermost) -
The
@Namespaceannotation on the package (viapackage-info.java) -
The
@Namespaceannotation on the module (viamodule-info.java) -
The package name of the class (default fallback)
This layered approach allows you to set a default namespace at a broad level (package or module) while still overriding it for specific classes when needed.
|
Using |
Payload conversion at handling time
Because message identity is decoupled from Java classes, payloads are converted to the handler’s expected type at handling time.
This means different handlers can receive the same message in different Java representations:
// Handler 1: Receives as domain object (OrderPlacedEvent is @Event annotated)
@EventHandler
public void handle(OrderPlacedEvent event) {
// Payload is converted to OrderPlacedEvent
}
// Handler 2: Receives as JSON (must specify messageType)
@EventHandler(messageType = "com.example.orders.OrderPlaced")
public void handle(JsonNode event) {
// Same message, converted to JsonNode
}
// Handler 3: Receives as Map (must specify messageType)
@EventHandler(messageType = "com.example.orders.OrderPlaced")
public void handle(Map<String, Object> event) {
// Same message, converted to Map
}
Axon uses the message’s MessageType to identify which handlers should receive the message, then converts the payload to whatever type each handler needs.
The message identifier remains the same throughout - it’s still the same message, just represented in different Java types.
|
When the first parameter of your handler is not annotated with |
This approach reduces the need for upcasters in many scenarios. Instead of creating upcasters to convert old message formats to new Java classes during deserialization, you can often just handle messages in different formats and let Axon perform the conversion.
Message type resolution
The MessageTypeResolver determines the MessageType for each message class in your application.
By default, it uses the annotations and class information to derive the qualified name and version.
Specifying message types
Use the message-specific annotations to define the name, namespace, and version of your messages.
For events:
package com.example.events;
// Namespace defaults to the package name: "com.example.events"
// Full qualified name will be: "com.example.events.UserRegistered"
@Event(name = "UserRegistered", version = "1.0")
public class UserRegisteredEvent {
// fields, constructors, etc.
}
// Explicitly specify a different namespace
// Full qualified name will be: "com.mycompany.user.UserRegistered"
@Event(namespace = "com.mycompany.user", name = "UserRegistered", version = "1.0")
public class UserRegisteredEvent {
// fields, constructors, etc.
}
For commands:
package com.example.commands;
@Command(name = "RegisterUser", version = "1.0")
public class RegisterUserCommand {
// fields, constructors, etc.
}
For queries:
package com.example.queries;
@Query(name = "FindUserById", version = "1.0")
public class FindUserByIdQuery {
// fields, constructors, etc.
}
Default values:
-
If you don’t specify a
namespace, Axon searches for the@Namespaceannotation (as described here) to use its value as thenamespace. The fallback for when that is not present too is the package name of the class. -
If you don’t specify a
name, Axon uses the simple class name. -
If you don’t specify a
version, Axon uses0.0.1as the version. -
The fully qualified name is always:
namespace + "." + name.
Custom message type resolution
If you need custom logic to determine message types, you can implement the MessageTypeResolver interface:
import org.axonframework.messaging.core.MessageTypeResolver;
import org.axonframework.messaging.core.MessageType;
public class CustomMessageTypeResolver implements MessageTypeResolver {
@Override
public Optional<MessageType> resolve(@Nonnull Class<?> payloadType); {
// Custom logic to determine the MessageType
return /* ... */;
}
}
Then register it with your configuration:
ApplicationConfigurer applicationConfigurer = /* The configurer that contains this Application's configuration */
MessagingConfigurer.enhance(applicationConfigurer)
.registerMessageTypeResolver(config -> new CustomMessageTypeResolver());
Payload
The payload contains the information about what happened - the specific data related to the message. While the message type describes what the message means (for example, "OrderPlaced"), the payload contains the details about that specific occurrence (the order ID, amount, customer details, etc.).
Accessing the payload
You can access the payload in different ways:
public void retrievePayload(Message message) {
// Get the payload as-is
Object payload = message.payload();
// Get the payload converted to a specific type
OrderPlacedEvent event = message.payloadAs(OrderPlacedEvent.class);
// Get the payload type
Class<?> type = message.payloadType();
}
The payloadAs(Class<T>) method attempts to convert the message payload into the required type as you access it.
In cases where a message originates outside the JVM (Axon Server, Database, Store), Axon attaches an applicable converter to the message before passing it on for handling. This applies for CommandMessages, CommandResultMessages, EventMessages, QueryMessages and QueryResponseMessages and makes those messages conversion aware, so handler code usually does not need to reference a converter directly when using a generic message type for handling:
@EventHandler
public void on(EventMessage event) {
// Converter is already attached — no need to pass it explicitly
OrderPlacedEvent order = event.payloadAs(OrderPlacedEvent.class);
}
If you need explicit control over conversion or if you are working with a message that was not produced by Axon’s infrastructure, you can still supply a converter directly:
public void retrieveConvertedPayload(Message message, Converter converter) {
OrderPlacedEvent payload = message.payloadAs(OrderPlacedEvent.class, converter);
}
When building custom infrastructure that produces Message instances (for example GenericMessage, GenericCommandMessage, GenericEventMessage, GenericQueryMessage, etc.), you can call withConverter(Converter) to attach the converter so downstream handlers benefit from the same automatic conversion:
GenericEventMessage messageWithConverter = rawMessage.withConverter(eventConverter);
Converting message payloads
If you need to convert a message’s payload for downstream processing, use withConvertedPayload:
public void convertEventPayload(EventMessage event, Converter converter) {
// Create a new message with JSON payload
EventMessage jsonMessage = event.withConvertedPayload(
JsonNode.class,
converter
);
}
This creates a new Message instance with the converted payload, leaving the original message unchanged.
Metadata
Metadata describes the context in which a message was generated. For example, metadata might contain information about the message that caused this message to be generated, or user information about who triggered the action.
In Axon, metadata is represented as a Map<String, String>.
Both keys and values must be Strings.
This simplifies serialization and aligns with how most systems and frameworks handle metadata.
Metadata is immutable - mutating methods create and return a new instance instead of modifying an existing one.
|
When more complex metadata value are required, you can use Axon’s |
Creating metadata
Metadata metadata = Metadata.with("userId", "user-123") (1)
.and("traceId", "trace-456"); (2)
| 1 | Creates a Metadata instance with a single key-value pair |
| 2 | Adds another entry, returning a new instance with both entries |
Adding metadata to messages
Messages in Axon are immutable. To add metadata, you create a new message instance with the additional metadata:
EventMessage event = new GenericEventMessage(
messageType,
new OrderPlacedEvent(orderId, amount)
);
// Add metadata - creates a new message instance
EventMessage eventWithMetadata = event.withMetadata(
Metadata.with("userId", "user-123")
);
// Add to existing metadata - merges with any existing entries
EventMessage mergedMessage = eventWithMetadata.andMetadata(
Metadata.with("correlationId", "corr-789")
);
The withMetadata method replaces all metadata with the given map.
The andMetadata method merges the given metadata with existing entries, with new entries overwriting existing ones with the same key.
|
Because metadata values must be Strings, you need to convert any non-String values before adding them to metadata:
Know that you can always use Axon’s |
Message-specific data
Different message types provide additional information beyond payload and metadata.
For example:
-
An
EventMessageprovides atimestamp()representing when the event occurred. -
A
CommandMessageprovides aroutingKey()used to route the command to the correct handler instance. Especially important in distributed environments. -
A
CommandMessageandQueryMessageprovide apriority()used to signal the importance of the command/query with the handling system.
EventMessage event = // ...
Instant occurredAt = event.timestamp();
CommandMessage command = // ...
Optional<String> routingKey = command.routingKey();
Message identifier
Each message has a unique identifier that identifies that specific message instance. The identifier remains constant even if the message is represented in different Java classes or if metadata is added.
Two messages with the same identifier represent the same conceptual message, even if:
-
Their payloads are represented using different Java classes (for example, domain object vs JSON).
-
They have different metadata.
-
They exist in different services or contexts.
String id = message.identifier();
The identifier is automatically generated when you create a message, or you can provide a specific identifier through the message constructor.
Together, the MessageType and identifier provide complete message identification:
-
The
MessageTypetells you what type of message it is (the structure) -
The
identifiertells you which specific instance of that message type it is
Creating messages
While Axon typically creates messages for you based on method return values and parameters, you can create messages explicitly when needed:
public EventMessage constructEvent(String orderId,
double amount) {
// Create an event message
MessageType orderPlacedType = new MessageType(
new QualifiedName("com.example.orders", "OrderPlaced"),
"1.0"
);
return new GenericEventMessage(
orderPlacedType,
new OrderPlacedEvent(orderId, amount),
Metadata.with("userId", "user-123")
);
}
|
Axon’s infrastructure components that construct |