Record Class Message

java.lang.Object
java.lang.Record
dev.agenor.core.Message
Record Components:
id - unique message identifier, auto-generated if null
topic - the topic for publish-subscribe routing, may be null
senderId - the identifier of the sending agent, may be null
receiverId - the identifier of the receiving agent for point-to-point, may be null
correlationId - identifier linking this message to a previous message (for replies), may be null
content - the message payload, may be any serializable object or null
headers - custom metadata as key-value pairs, never null (empty map if not provided)
timestamp - when the message was created, auto-generated if null

public record Message(String id, String topic, String senderId, String receiverId, String correlationId, Object content, Map<String,String> headers, Instant timestamp) extends Record
Immutable message object for agent communication in the Agenor framework.

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:

Message core components and properties
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 senderId for traceability
  • Use meaningful topics for pub-sub patterns
  • Set receiverId for point-to-point messages
  • Use correlationId for 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:
  • 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 id is auto-generated as UUID
      • Missing timestamp is set to current time
      • headers map 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 null
      topic - topic for routing
      senderId - sender agent identifier
      receiverId - receiver agent identifier
      correlationId - correlation identifier for request-response
      content - message payload
      headers - metadata map, defensively copied
      timestamp - creation timestamp, set to now if null
  • Method Details

    • getContent

      public <T> T getContent(Class<T> type)
      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: content is declared Object and 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 a LinkedHashMap rebuilt 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 — returns null

      Null Handling: Returns null if the content is null. Always check for null before using the returned value, or use Optional.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. Check content-class in 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 of type nor convertible to it
      See Also:
    • convertContent

      public static <T> T convertContent(Object content, Class<T> type)
      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 call getContent(Class); this overload exists for callers holding a payload without a Message around it.

      Type Parameters:
      T - the expected type of the content
      Parameters:
      content - the payload to convert; may be null
      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 of type nor convertible to it
      Since:
      0.26.0
    • builder

      public static Message.MessageBuilder 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

      public Message.MessageBuilder reply(Object content)
      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:

      1. Receive a request message
      2. Process the request
      3. Use reply() to create response
      4. 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:

      • correlationId set to this message's id
      • receiverId set to this message's senderId
      • content set 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

      public final String 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.
      Specified by:
      toString in class Record
      Returns:
      a string representation of this object
    • 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.
      Specified by:
      hashCode in class Record
      Returns:
      a hash code value for this object
    • equals

      public final boolean equals(Object o)
      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 with Objects::equals(Object,Object).
      Specified by:
      equals in class Record
      Parameters:
      o - the object with which to compare
      Returns:
      true if this object is the same as the o argument; false otherwise.
    • id

      public String id()
      Returns the value of the id record component.
      Returns:
      the value of the id record component
    • topic

      public String topic()
      Returns the value of the topic record component.
      Returns:
      the value of the topic record component
    • senderId

      public String senderId()
      Returns the value of the senderId record component.
      Returns:
      the value of the senderId record component
    • receiverId

      public String receiverId()
      Returns the value of the receiverId record component.
      Returns:
      the value of the receiverId record component
    • correlationId

      public String correlationId()
      Returns the value of the correlationId record component.
      Returns:
      the value of the correlationId record component
    • content

      public Object content()
      Returns the value of the content record component.
      Returns:
      the value of the content record component
    • headers

      public Map<String,String> headers()
      Returns the value of the headers record component.
      Returns:
      the value of the headers record component
    • timestamp

      public Instant timestamp()
      Returns the value of the timestamp record component.
      Returns:
      the value of the timestamp record component