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. |
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 uses 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(Type) method converts the payload to the required type at the moment you need it.
In most cases, you don’t need to pass a Converter explicitly - if the message has passed through converter-aware components (which is typical in Axon), the conversion happens automatically.
If you need explicit control over conversion, you can provide a converter:
public void retrieveConvertedPayload(Message message, Converter converter) {
OrderPlacedEvent payload = message.payloadAs(OrderPlacedEvent.class, converter);
}
This is useful when you need the same message in different formats at different points in your processing logic.
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 |