Message handling
A handler is the application code that responds to a message. Its annotation selects the kind of message; its parameter type selects the payload. A return value becomes the answer to a command or query.
Here is a query and its complete handler:
public record GetGreeting(String name) implements Request<String> {}
class GreetingHandler { @HandleQuery String handle(GetGreeting query) { return "Hello, " + query.name() + "!"; }}data class GetGreeting(val name: String) : Request<String>
class GreetingHandler { @HandleQuery fun handle(query: GetGreeting) = "Hello, ${query.name}!"}Register GreetingHandler with your application, or make it a Spring @Component for discovery.
Then ask the query through Fluxzero:
String greeting = Fluxzero.queryAndWait(new GetGreeting("Alice")); // "Hello, Alice!"val greeting = Fluxzero.queryAndWait(GetGreeting("Alice")) // "Hello, Alice!"Request<String> declares the answer’s type. You can
test this handler locally without starting a Runtime.
Reacting to an event
Section titled “Reacting to an event”An event tells you something happened. A handler can start another action in response. For example, given your
application’s CreateUser and SendWelcomeEmail payloads:
class UserEventHandler { @HandleEvent void handle(CreateUser event) { Fluxzero.sendCommand(new SendWelcomeEmail(event.getUserProfile())); }}class UserEventHandler { @HandleEvent fun handle(event: CreateUser) { Fluxzero.sendCommand(SendWelcomeEmail(event.userProfile)) }}The email command has its own handler. This keeps reacting to user creation separate from sending the email.
Inside handlers, static Fluxzero calls use the current application instance.
Handlers on the payload
Section titled “Handlers on the payload”Reusable operations can keep their handler on the command/query payload itself. This is also the default for an
external API interaction: the local @HandleCommand or @HandleQuery method makes the WebRequestGateway call,
and other handlers dispatch its payload instead of injecting an API-service bean. No @TrackSelf or @Consumer is
required merely for an external call. See Local handling and the complete
outbound integration example.
Returning futures
Section titled “Returning futures”Handler methods may also return a CompletableFuture<T> instead of a direct value. Fluxzero will publish the result when the future completes:
class AsyncUserQueryHandler { @HandleQuery CompletableFuture<UserProfile> handle(GetUserProfile query) { return userService.fetchAsync(query.getUserId()); }}class AsyncUserQueryHandler { @HandleQuery fun handle(query: GetUserProfile): java.util.concurrent.CompletableFuture<UserProfile> { return userService.fetchAsync(query.userId) }}Handler matching & passive handlers
Section titled “Handler matching & passive handlers”Fluxzero resolves which handler(s) should run based on message type and specificity.
Most specific handler wins
Section titled “Most specific handler wins”If multiple methods in the same class match a message, only the most specific one is invoked.
class SpecificityExample { @HandleEvent void handle(Object event) { /* generic fallback */ }
@HandleEvent void handle(CreateUser event) { /* specific handler */ }}class SpecificityExample { @HandleEvent fun handle(event: Any) { /* generic fallback */ }
@HandleEvent fun handle(event: CreateUser) { /* specific handler */ }}Multiple classes are all invoked
Section titled “Multiple classes are all invoked”When different classes handle the same payload, each eligible handler runs independently.
class BusinessHandler { @HandleEvent void handle(CreateUser event) { /* business logic */ }}
class LoggingHandler { @HandleEvent void log(Object event) { /* audit/metrics */ }}class BusinessHandler { @HandleEvent fun handle(event: CreateUser) { /* business logic */ }}
class LoggingHandler { @HandleEvent fun log(event: Any) { /* audit/metrics */ }}Requests prefer a single active handler
Section titled “Requests prefer a single active handler”For commands, queries, and web requests, a single non-passive handler should produce the response. Additional passive handlers can observe for metrics/auditing.
class UserHandler { @HandleQuery UserAccount handle(GetUser query) { return userRepository.find(query.getUserId()); }}
class QueryMetricsHandler { @HandleQuery(passive = true) void record(Object query) { metrics.increment("queries." + query.getClass().getSimpleName()); }}class UserHandler { @HandleQuery fun handle(query: GetUser): UserAccount { return userRepository.find(query.userId) }}
class QueryMetricsHandler { @HandleQuery(passive = true) fun record(query: Any) { metrics.increment("queries.${query::class.simpleName}") }}Parameter resolvers
Section titled “Parameter resolvers”Fluxzero allows fine-grained control over handler method parameters using the ParameterResolver interface. This lets you inject any value into annotated handler methods — beyond just the payload or Message metadata.
When a message is dispatched to a handler (e.g. via @HandleEvent, @HandleCommand, etc.), the framework inspects the method’s parameters and resolves each one using the configured ParameterResolvers.
By default, Fluxzero supports injection of the following:
- The message payload (automatically matched by parameter type)
- The full
Message,Schedule, orWebRequest - The raw
DeserializingMessage(for low-level access) - The message
Metadata - The currently authenticated
User(if available) - The associated
Entitywrapper or the entity value itself - The triggering message (annotated with
@Trigger) - Any Spring bean (when Spring integration is enabled)
Other contextual values like message ID or timestamp can be obtained from the Message:
@HandleEventvoid handle(CreateUser event, Message message) { log.info("User created at {}", message.getTimestamp());}@HandleEventfun handle(event: CreateUser, message: Message) { log.info("User created at {}", message.timestamp)}Writing your own resolver
Section titled “Writing your own resolver”You can create a custom resolver to inject arbitrary values, such as headers, timestamps, or contextual objects:
public class TimestampParameterResolver implements ParameterResolver<DeserializingMessage> { @Override public Function<DeserializingMessage, Object> resolve(Parameter parameter, Annotation methodAnnotation) { if (parameter.getType().equals(Instant.class)) { return DeserializingMessage::getTimestamp; } return null; }}class TimestampParameterResolver : ParameterResolver<DeserializingMessage> { override fun resolve(parameter: Parameter, methodAnnotation: Annotation): Function<DeserializingMessage, Any>? { return if (parameter.type == Instant::class.java) { Function { msg -> msg.timestamp } } else null }}Then register it via your builder:
Assuming you are configuring a FluxzeroBuilder builder:
builder .addParameterResolver(new TimestampParameterResolver()) .build(); builder .addParameterResolver(TimestampParameterResolver()) .build()And use it in your handler:
@HandleCommandvoid handle(CreateOrder command, Instant timestamp) { log.info("Command received at {}", timestamp);}@HandleCommandfun handle(command: CreateOrder, timestamp: Instant) { log.info("Command received at {}", timestamp)}Use cases
Section titled “Use cases”- Injecting request-specific context (e.g. tracing info)
- Supporting custom annotations (e.g.
@FromHeader) - Enabling access to correlated data (e.g. parent entity)
- Binding to environment or system values
Custom parameter injection is a powerful tool for modular, contextual logic. It works seamlessly with all handler annotations (@HandleEvent, @HandleCommand, @HandleQuery, @HandleError, etc.) and helps avoid boilerplate argument passing.
Handler interceptors
Section titled “Handler interceptors”A HandlerInterceptor lets you wrap or intercept the execution of handler methods. This gives you a central place to add cross‑cutting behavior such as:
- Authorization & access control — block or allow messages depending on the current user
- Auditing & logging — capture details about incoming messages, handler calls, or results
- Validation — enforce extra business rules before or after handler execution
- Blocking — stop the handler from being invoked entirely
- Result transformation — adjust or enrich results before they are published
- Thread context propagation — set up thread‑local state (e.g. correlation IDs, security principals)
public class AuthorizationInterceptor implements HandlerInterceptor { @Override public Function<DeserializingMessage, Object> interceptHandling( Function<DeserializingMessage, Object> next, HandlerInvoker invoker) { return message -> { if (!isAuthorized(message)) { throw new UnauthorizedException(); } return next.apply(message); }; }}class AuthorizationInterceptor : HandlerInterceptor { override fun interceptHandling( next: Function<DeserializingMessage, Any>, invoker: HandlerInvoker ): Function<DeserializingMessage, Any> { return Function { message -> if (!isAuthorized(message)) { throw UnauthorizedException() } next.apply(message) } }}You can register handler interceptors either per class (via @Consumer annotation) or globally across the application.
See Configuring Fluxzero for details.
Handler invocation flow
Section titled “Handler invocation flow”The diagram below shows how Fluxzero selects and invokes handlers for an incoming message:
graph TD
MSG[Incoming message] --> MATCH[Match handlers by signature & parameters]
MATCH --> FOUND_MATCH{Handler found?}
FOUND_MATCH -->|Yes| INTERCEPT[Apply handler interceptors]
INTERCEPT --> PRE_BLOCK{Handling blocked?}
PRE_BLOCK -->|No| INVOKE[Provide method parameters]
INVOKE --> DESERIALIZE{Payload already deserialized?}
DESERIALIZE -->|No| UPCAST[Upcast payload if needed]
UPCAST --> MAP[Map to payload object]
MAP --> HANDLED[Invoke handler method]
DESERIALIZE -->|Yes| HANDLED
- Handlers are selected based on method signatures and parameter types.
- Interceptors can wrap the call or block invocation altogether.
- On invocation, the message payload is deserialized if needed, which may include upcasting followed by mapping to the target object type.
© 2026 Fluxzero