Interface MessageHandler
- Functional Interface:
- This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
A MessageHandler is a callback that processes incoming messages.
Handlers are registered with the MessageDispatcher to receive messages
matching specific criteria (topics, receivers, or predicates).
Functional Interface:
Being a @FunctionalInterface, handlers can be created using:
- Lambda expressions
- Method references
- Anonymous classes
Asynchronous by Default:
The handle(Message) method returns a CompletableFuture,
enabling non-blocking, asynchronous message processing. This is crucial for:
- Avoiding blocking the thread that delivers the message
- Processing multiple messages concurrently
- Chaining message processing operations
- Error handling and recovery
Example - Lambda Handler:
// Simple lambda handler
MessageHandler handler = message -> {
log.info("Received: {}", message.content());
return CompletableFuture.completedFuture(null);
};
// Subscribe
topicSubscriber.subscribeTopic("notifications", handler);
Example - Method Reference:
public class OrderProcessor {
public CompletableFuture<Void> handleOrder(Message message) {
OrderData order = message.getContent(OrderData.class);
return processOrderAsync(order);
}
public void setupSubscription(TopicSubscriber subscriber) {
// Method reference as handler
subscriber.subscribeTopic("orders", this::handleOrder);
}
}
Example - Async Processing:
MessageHandler asyncHandler = message -> {
return CompletableFuture.supplyAsync(() -> {
// Long-running operation in separate thread
Data data = message.getContent(Data.class);
Result result = expensiveOperation(data);
// Send response
Message response = message.reply(result)
.senderId("processor")
.build();
messageService.send(response);
return null;
});
};
Example - Synchronous Wrapper:
Use sync(SyncMessageHandler) for simple synchronous handlers:
// Synchronous handler wrapped in async
MessageHandler handler = MessageHandler.sync(message -> {
log.info("Processing: {}", message.content());
processSync(message);
// No need to return CompletableFuture
});
Error Handling: An unhandled exception completes the returned future exceptionally, and that outcome is the message's outcome: the mailbox does not acknowledge it, so on a transport with redelivery it is delivered again and eventually dead-lettered (ADR-033). Other handlers for the same message still run; the first failure is rethrown with the rest attached as suppressed exceptions.
A failure is therefore not contained by default. To contain one, catch it in the handler, where the decision is visible — and make handlers idempotent, because a redelivered message runs them again.
MessageHandler resilientHandler = message -> {
return CompletableFuture.supplyAsync(() -> {
try {
processMessage(message);
return null;
} catch (ValidationException e) {
log.warn("Invalid message: {}", e.getMessage());
sendErrorReply(message, e);
return null;
} catch (Exception e) {
log.error("Processing failed", e);
// Rethrow to mark future as failed
throw new CompletionException(e);
}
});
};
Best Practices:
- Keep handlers focused on a single responsibility
- Use async processing for I/O or long-running operations
- Handle errors explicitly; don't let exceptions propagate silently
- Log at appropriate levels for debugging and monitoring
- Consider idempotency for retry scenarios
- Don't block; return futures for all long operations
- Since:
- 0.1.0
- See Also:
-
Nested Class Summary
Nested ClassesModifier and TypeInterfaceDescriptionstatic interfaceFunctional interface for synchronous message handling. -
Method Summary
Modifier and TypeMethodDescriptionHandles a received message asynchronously.static MessageHandlersync(MessageHandler.SyncMessageHandler syncHandler) Creates an async handler wrapper for synchronous message handlers.
-
Method Details
-
handle
Handles a received message asynchronously.This method is invoked when a message matching the subscription criteria is delivered. The handler should process the message and return a
CompletableFuturethat completes when processing is done.Asynchronous Processing: The method returns immediately with a future. Actual processing can be:
- Immediate - Use
CompletableFuture.completedFuture(null) - Async - Use
CompletableFuture.supplyAsync(...) - Chained - Return an existing future from async operations
Thread Safety: This method may be called concurrently from multiple threads for different messages. Implementations must be thread-safe if they access shared state.
Error Handling:
- Handle expected errors within the handler
- Let unexpected errors complete the future exceptionally
- Don't silently swallow exceptions
Performance:
- Don't block the calling thread
- Use
CompletableFuture.runAsync()for heavy processing - Consider thread pool sizing for concurrent messages
Examples:
Immediate completion:
public CompletableFuture<Void> handle(Message message) { log.info("Received: {}", message.content()); return CompletableFuture.completedFuture(null); }Async processing:
public CompletableFuture<Void> handle(Message message) { return CompletableFuture.runAsync(() -> { expensiveOperation(message.content()); }); }Chained operations:
public CompletableFuture<Void> handle(Message message) { return fetchDataAsync() .thenCompose(data -> processAsync(data)) .thenCompose(result -> sendReply(message, result)) .exceptionally(ex -> { log.error("Handler failed", ex); return null; }); }- Parameters:
message- the received message, never null- Returns:
- a
CompletableFuturethat completes when processing is finished, or completes exceptionally if processing fails - Throws:
RuntimeException- if processing fails (will complete future exceptionally)- See Also:
- Immediate - Use
-
sync
Creates an async handler wrapper for synchronous message handlers.This factory method simplifies creating handlers for simple synchronous operations. It wraps a
MessageHandler.SyncMessageHandlerthat processes messages synchronously and returns void, converting it to the asyncMessageHandlerinterface.When to Use:
- Quick, in-memory processing
- Simple logging or routing logic
- Operations that complete instantly
When NOT to Use:
- I/O operations (database, network, files)
- CPU-intensive computations
- Operations that could block
Error Handling: An exception thrown by the sync handler is caught and returned as a failed future. That does not contain it: the failed future is how the failure reaches the mailbox, which then does not acknowledge the message (ADR-033). To contain a failure, catch it inside the handler body.
Examples:
Simple logging:
MessageHandler handler = MessageHandler.sync(message -> { log.info("Received message: {}", message.content()); });Quick processing:
MessageHandler handler = MessageHandler.sync(message -> { String data = message.getContent(String.class); if (data != null) { cache.put(message.id(), data); } });With error handling:
MessageHandler handler = MessageHandler.sync(message -> { try { quickProcess(message); } catch (Exception e) { log.error("Processing failed", e); throw e; // Becomes failed future } });- Parameters:
syncHandler- the synchronous message handler- Returns:
- an async MessageHandler wrapping the synchronous handler
- Throws:
NullPointerException- if syncHandler is null- See Also:
-