Record Class Message
- Record Components:
id- unique message identifier, auto-generated if nulltopic- the topic for publish-subscribe routing, may be nullsenderId- the identifier of the sending agent, may be nullreceiverId- the identifier of the receiving agent for point-to-point, may be nullcorrelationId- identifier linking this message to a previous message (for replies), may be nullcontent- the message payload, may be any serializable object or nullheaders- custom metadata as key-value pairs, never null (empty map if not provided)timestamp- when the message was created, auto-generated if null
Messages are the fundamental unit of communication between agents. They
encapsulate content along with routing, correlation, and metadata information.
The Message record provides a type-safe, immutable container for
inter-agent communication with support for both publish-subscribe and
point-to-point messaging patterns.
Key Features:
- Immutability - Thread-safe, can be safely shared across agents
- Builder Pattern - Fluent API for message construction
- JSON Serialization - Native Jackson support for serialization
- Correlation Support - Built-in request-response pattern
- Flexible Content - Type-safe content retrieval
- Headers - Extensible metadata for custom information
Message Components:
| Component | Purpose | Required |
|---|---|---|
| id | Unique message identifier | Auto-generated if not provided |
| topic | Publish-subscribe routing | Optional for direct messages |
| senderId | Source agent identification | Optional but recommended |
| receiverId | Point-to-point routing | Optional for broadcast |
| correlationId | Request-response correlation | Optional, for replies |
| content | Message payload | Optional |
| headers | Custom metadata | Optional |
| timestamp | Creation time | Auto-generated if not provided |
Messaging Patterns Supported:
1. Publish-Subscribe (Topic-Based): Multiple agents subscribe to topics and receive all messages published to those topics.
// Publisher
Message announcement = Message.builder()
.topic("system.alerts")
.content("System maintenance scheduled")
.build();
messageService.send(announcement);
// Subscribers
messageService.subscribe("system.alerts", message -> {
log.info("Received alert: {}", message.content());
return CompletableFuture.completedFuture(null);
});
2. Point-to-Point (Direct Messaging): Messages sent directly to a specific agent by ID.
// Send to specific agent
Message direct = Message.builder()
.senderId("order-processor")
.receiverId("payment-service")
.content(orderData)
.build();
messageService.send(direct);
// Receiver subscribes to their ID
messageService.subscribeToReceiver("payment-service", message -> {
processPayment(message.getContent(OrderData.class));
return CompletableFuture.completedFuture(null);
});
3. Request-Response (Correlation-Based): Synchronous-style communication using correlation IDs to match requests and responses.
// Requester
Message request = Message.builder()
.senderId("client")
.receiverId("server")
.content("query data")
.build();
CompletableFuture<Message> responseFuture =
messageService.sendAndWait(request, 5000);
responseFuture.thenAccept(response -> {
log.info("Response: {}", response.content());
});
// Responder
messageService.subscribeToReceiver("server", request -> {
Object result = processQuery(request.content());
// Reply using correlation
Message response = request.reply(result)
.senderId("server")
.build();
return messageService.send(response);
});
Headers Usage: Headers provide a flexible mechanism for metadata without modifying the content:
Message message = Message.builder()
.topic("data.events")
.content(data)
.header("priority", "HIGH")
.header("source", "sensor-01")
.header("timestamp-utc", Instant.now().toString())
.header("retry-count", "0")
.build();
// Filter by headers
messageService.subscribe(
msg -> "HIGH".equals(msg.headers().get("priority")),
this::handleHighPriority
);
Content Type Safety:
The getContent(Class) method provides type-safe content retrieval:
// Sending typed content
OrderData order = new OrderData(...);
Message message = Message.builder()
.content(order)
.build();
// Receiving with type safety
messageService.subscribe("orders", msg -> {
OrderData receivedOrder = msg.getContent(OrderData.class);
processOrder(receivedOrder);
return CompletableFuture.completedFuture(null);
});
Thread Safety: Messages are immutable and thread-safe. They can be safely:
- Shared across multiple agent threads
- Passed through message queues
- Stored for audit or replay
- Serialized and transmitted over network
Serialization: Messages are fully JSON-serializable using Jackson annotations. This enables:
- Persistence to databases or files
- Network transmission in distributed systems
- Logging and debugging
- Message replay and testing
Best Practices:
- Always set
senderIdfor traceability - Use meaningful topics for pub-sub patterns
- Set
receiverIdfor point-to-point messages - Use
correlationIdfor request-response patterns - Keep content serializable for distributed scenarios
- Use headers for cross-cutting concerns (priority, tracing, etc.)
- Don't store large objects in content; use references instead
- Since:
- 0.1.0
- See Also:
-
Nested Class Summary
Nested Classes -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionstatic Message.MessageBuilderbuilder()Creates a new builder for constructing messages fluently.content()Returns the value of thecontentrecord component.static <T> TconvertContent(Object content, Class<T> type) Converts an arbitrary message payload to the requested type.Returns the value of thecorrelationIdrecord component.final booleanIndicates whether some other object is "equal to" this one.<T> TgetContent(Class<T> type) Retrieves the message content as the requested type, converting it if necessary.final inthashCode()Returns a hash code value for this object.headers()Returns the value of theheadersrecord component.id()Returns the value of theidrecord component.Returns the value of thereceiverIdrecord component.Creates a reply message builder with correlation ID set automatically.senderId()Returns the value of thesenderIdrecord component.Returns the value of thetimestamprecord component.topic()Returns the value of thetopicrecord component.final StringtoString()Returns a string representation of this record class.
-
Constructor Details
-
Message
public Message(String id, String topic, String senderId, String receiverId, String correlationId, Object content, Map<String, String> headers, Instant timestamp) Canonical constructor with default value generation and defensive copying.This constructor ensures that:
- Missing
idis auto-generated as UUID - Missing
timestampis set to current time headersmap is defensively copied for immutability- Empty headers map is created if null
Note: Jackson primarily uses this constructor during deserialization. For message creation, use the
builder()pattern instead.- Parameters:
id- message identifier, generated if nulltopic- topic for routingsenderId- sender agent identifierreceiverId- receiver agent identifiercorrelationId- correlation identifier for request-responsecontent- message payloadheaders- metadata map, defensively copiedtimestamp- creation timestamp, set to now if null
- Missing
-
-
Method Details
-
getContent
Retrieves the message content as the requested type, converting it if necessary.This is the accessor to use in a message handler. Casting
content()directly works only as long as the message never crosses a serialising transport, which is a property of the deployment, not of the code — see below.Why this is not a cast:
contentis declaredObjectand carries no type information on the wire (ADR-005). The in-memory dispatcher therefore hands the receiver the sender's original object, while a serialising transport such as Redis (ADR-021) hands it aLinkedHashMaprebuilt from JSON. This method absorbs that difference:- content already of the requested type — returned as the same reference, no allocation, no Jackson involved
- content deserialised into a
Map/List— converted to the requested type - content
null— returnsnull
Null Handling: Returns
nullif the content isnull. Always check for null before using the returned value, or useOptional.ofNullable().Examples:
Simple content types:
String text = message.getContent(String.class); Integer number = message.getContent(Integer.class); Boolean flag = message.getContent(Boolean.class);Complex domain objects — works on every transport:
OrderData order = message.getContent(OrderData.class); PaymentRequest payment = message.getContent(PaymentRequest.class);Collections (requires care with generics — element types are not converted):
@SuppressWarnings("unchecked") List<String> items = message.getContent(List.class); @SuppressWarnings("unchecked") Map<String, Object> data = message.getContent(Map.class);Safe usage with null check:
OrderData order = message.getContent(OrderData.class); if (order != null) { processOrder(order); } else { log.warn("Message {} has null content", message.id()); }Limitation — this removes the
ClassCastException, not the need to ask for the right type. Conversion is lenient about unknown properties, so that a receiver still reads a payload whose sender has added a field. The cost is that reading a post-transport payload as an unrelated type succeeds, yielding an object with null or default fields, instead of failing. Only an incompatible shape (a scalar read as a record, say) throws. Checkcontent-classin the headers when the sender's claim matters.Cost: conversion runs on every call, so a handler reading the same payload more than once should hold the result in a local variable.
- Type Parameters:
T- the expected type of the content- Parameters:
type- the class of the expected type; must not be null- Returns:
- the content as type T, or null if content is null
- Throws:
IllegalArgumentException- if the content is neither an instance oftypenor convertible to it- See Also:
-
convertContent
Converts an arbitrary message payload to the requested type.The shared implementation behind
getContent(Class)and the dialogue layer's equivalent accessor, exposed so that both read the same semantics from one place rather than from two implementations that can drift apart. Handler code should callgetContent(Class); this overload exists for callers holding a payload without aMessagearound it.- Type Parameters:
T- the expected type of the content- Parameters:
content- the payload to convert; may be nulltype- the class of the expected type; must not be null- Returns:
- the content as type T, or null if content is null
- Throws:
IllegalArgumentException- if the content is neither an instance oftypenor convertible to it- Since:
- 0.26.0
-
builder
Creates a new builder for constructing messages fluently.The builder pattern is the recommended way to create messages. It provides a fluent, readable API that makes message construction clear and maintainable.
Examples:
Minimal message:
Message simple = Message.builder() .content("Hello") .build();Topic-based broadcast:
Message broadcast = Message.builder() .topic("system.events") .senderId("monitor-agent") .content(event) .build();Direct message:
Message direct = Message.builder() .senderId("client") .receiverId("server") .content(request) .build();With headers:
Message enriched = Message.builder() .topic("orders") .content(order) .header("priority", "HIGH") .header("region", "US-WEST") .build();- Returns:
- a new MessageBuilder instance
- See Also:
-
reply
Creates a reply message builder with correlation ID set automatically.This convenience method simplifies the request-response pattern by automatically setting the correlation ID to this message's ID and setting the receiver to this message's sender.
Usage Pattern:
- Receive a request message
- Process the request
- Use
reply()to create response - Send the response
Examples:
Simple reply:
messageService.subscribeToReceiver("server", request -> { String result = processRequest(request.content()); Message response = request.reply(result) .senderId("server") .build(); return messageService.send(response); });Reply with additional metadata:
Message response = request.reply(result) .senderId("processor") .topic("responses") .header("processing-time-ms", String.valueOf(elapsed)) .header("status", "SUCCESS") .build();Error reply:
Message errorResponse = request.reply(null) .senderId("validator") .header("error", "VALIDATION_FAILED") .header("error-message", errorMessage) .build();Automatic Fields: The returned builder has:
correlationIdset to this message'sidreceiverIdset to this message'ssenderIdcontentset to the provided reply content
Example:
// Responder — inside a subscribeRecipient handler dispatcher.subscribeRecipient("server", req -> { Message reply = req.reply(processQuery(req.content())) .senderId("server") .build(); return dispatcher.sendTo(reply); });- Parameters:
content- the reply content (payload)- Returns:
- a MessageBuilder with correlationId and receiverId pre-set
- See Also:
-
toString
Returns a string representation of this record class. The representation contains the name of the class, followed by the name and value of each of the record components. -
hashCode
public final int hashCode()Returns a hash code value for this object. The value is derived from the hash code of each of the record components. -
equals
Indicates whether some other object is "equal to" this one. The objects are equal if the other object is of the same class and if all the record components are equal. All components in this record class are compared withObjects::equals(Object,Object). -
id
Returns the value of theidrecord component.- Returns:
- the value of the
idrecord component
-
topic
Returns the value of thetopicrecord component.- Returns:
- the value of the
topicrecord component
-
senderId
Returns the value of thesenderIdrecord component.- Returns:
- the value of the
senderIdrecord component
-
receiverId
Returns the value of thereceiverIdrecord component.- Returns:
- the value of the
receiverIdrecord component
-
correlationId
Returns the value of thecorrelationIdrecord component.- Returns:
- the value of the
correlationIdrecord component
-
content
Returns the value of thecontentrecord component.- Returns:
- the value of the
contentrecord component
-
headers
Returns the value of theheadersrecord component.- Returns:
- the value of the
headersrecord component
-
timestamp
Returns the value of thetimestamprecord component.- Returns:
- the value of the
timestamprecord component
-