Skip to content

Sending messages

Send a message to ask your application to do work or provide information. The sender names the action; Fluxzero finds the handler, so the sender does not need its process address.

IntentMessage kind
Ask for a change, such as creating a userCommand
Ask for information, such as a user’s profileQuery
Announce something that happenedEvent
Perform work laterSchedule

The simplest way to send messages is via static methods on the Fluxzero class:

Fluxzero.sendCommand(new CreateUser("Alice")); // Async command

Here CreateUser is your application’s command payload, for example record CreateUser(String name) {}. Its command handler decides what creating a user means. sendCommand returns a future for the result; the variants below let you choose whether and how to wait.

Messages can include metadata:

Fluxzero.sendCommand(new CreateUser("Bob"),
Metadata.of("source", "admin-ui"));

Commands trigger domain behavior and optionally return a result.

Fire-and-forget:

Fluxzero.sendAndForgetCommand(new CreateUser("Alice"));

Send and wait:

UserId id = Fluxzero.sendCommandAndWait(new CreateUser("Charlie"));

Async:

CompletableFuture<UserId> future =
Fluxzero.sendCommand(new CreateUser("Bob"));

Queries retrieve state from read models or projections.

Blocking:

UserProfile profile =
Fluxzero.queryAndWait(new GetUserProfile("user456"));

Async:

CompletableFuture<UserProfile> result =
Fluxzero.query(new GetUserProfile("user123"));

Events can be published via:

Fluxzero.publishEvent(new UserLoggedIn("user789"));

By default:

  • ✅ Events are persisted in the event log for downstream processing.
  • ⚠️ If a local handler exists, the event will not be forwarded unless @LocalHandler(logMessage = true).

Schedule messages for future delivery:

Fluxzero.schedule(new ReminderFired(), Duration.ofMinutes(5));

Schedule periodic tasks:

Fluxzero.schedulePeriodic(new PollExternalApi());

Send outbound HTTP calls through the Fluxzero Runtime:

WebRequest request = WebRequest
.get("https://api.example.com/data").build();
WebResponse response = Fluxzero.get()
.webRequestGateway().sendAndWait(request);

Send custom metric messages:

Fluxzero.publishMetrics(
new SystemLoadMetric(cpu, memory));

A DispatchInterceptor lets you hook into the message pipeline before it’s published to Fluxzero or handled locally. Interceptors are a powerful way to enrich, validate, log, or even block messages at dispatch time.

public class LoggingInterceptor implements DispatchInterceptor {
@Override
public Message interceptDispatch(Message message, MessageType type,
String topic) {
log.info("Dispatching: {} to topic {}", type, topic);
return message;
}
}

What you can do with an interceptor:

  • interceptDispatch(...) — inspect, modify, or block a message before it’s dispatched
  • modifySerializedMessage(...) — adjust the serialized form of a message before it’s sent across the wire
  • monitorDispatch(...) — observe or log the final message as it leaves the system

To block dispatch, simply return null from the interceptDispatch method.

Registering an interceptor globally Assuming you are configuring a FluxzeroBuilder builder:

builder.addDispatchInterceptor(new LoggingInterceptor(), MessageType.COMMAND, MessageType.EVENT);

See Configuring Fluxzero for details on registration.


Fluxzero processes the message through the following pipeline:

All configured DispatchInterceptors run first. They may:

  • Inject or modify metadata
  • Validate or mutate the message
  • Block or suppress delivery

If a local handler matches the message type/topic, it’s invoked immediately. Otherwise, the message is forwarded.

The message is serialized (typically via Jackson), tagged with metadata, and versioned for transport.

A second pass of interceptors may adjust or enrich the serialized form before sending.

Messages not handled locally are published to the Fluxzero Runtime. From there, delivery guarantees, retries, rate limits, and remote handler routing apply.

The diagram below illustrates the full dispatch pipeline. At each step, processing may end early if the message is blocked or suppressed.

graph TD
    START[Message dispatched] --> PRE[Pre-serialization dispatch interceptors]
    PRE --> PRE_BLOCK{Dispatch blocked?}
    PRE_BLOCK -->|No| HANDLERS{Local handler available?}
    HANDLERS -->|Yes| LOCAL[Invoke local handler]
    HANDLERS -->|No| SERIALIZE[Serialize message with metadata]
    LOCAL --> PASSIVE{Passive?}
    PASSIVE -->|Yes| SERIALIZE
    PASSIVE -->|No| LOG_MESSAGE{Log message?}
    LOG_MESSAGE -->|Yes| SERIALIZE
    LOG_MESSAGE --> RETURN_LOCAL_RESULT[Return local result]
    SERIALIZE --> POST[Post-serialization interceptors]
    POST --> POST_BLOCK{Dispatch blocked?}
    POST_BLOCK -->|No| FORWARD[Forward to Runtime]

Sometimes you need all messages with the same identifier to be processed in order, while still allowing parallelism across unrelated entities. Fluxzero supports this through the @RoutingKey annotation.

Apply @RoutingKey to a field in your message payload (or metadata) to ensure that all messages sharing the same key are handled sequentially. This is especially useful for per-customer, per-order, or per-entity consistency.

Routing keys are converted into a hash using consistent hashing at dispatch time. This ensures even distribution across segments while maintaining order for messages that share the same key.

Handlers may also override the routing key for finer control. See Custom routing keys for details.

You can place the annotation directly on a field, or point it to a nested property path:

public record ShipOrder(@RoutingKey OrderId orderId) {
}
@RoutingKey("customer/id")
public record OrderPlaced(Customer customer) {
}

Apply @Timeout to a payload class (command or query) to enforce maximum wait time for blocking calls:

@Timeout(value = 3, timeUnit = TimeUnit.SECONDS)
public record CalculatePremium(UserProfile profile)
implements Request<BigDecimal> {}

When sent using queryAndWait(...), this timeout is respected:

BigDecimal result = Fluxzero
.queryAndWait(new CalculatePremium(user));

If no response arrives in time, a TimeoutException is thrown.


A handler’s response and a storage acknowledgement answer different questions. The response tells you the result of the command or query. A storage acknowledgement tells you that the Runtime stored the outgoing message; it does not mean a downstream handler has finished.

For ordinary outgoing messages, SDK 2.x uses Guarantee.STORED by default. You can override fluxzero.publishing.defaultGuarantee (FLUXZERO_PUBLISHING_DEFAULT_GUARANTEE), or select a guarantee for one call:

Fluxzero.sendCommand(new CreateUser("Alice"), Metadata.empty(), Guarantee.STORED);

NONE does not await a transport acknowledgement, SENT waits for sending, and STORED waits for storage. These choices do not change request/reply futures into delivery receipts or force local messages through the Runtime. They are independent of fluxzero.defaults.version.

Convenience methods such as publishEvent do not individually wait for remote storage. When you need to observe acknowledgement outside a handler, use the gateway overload that returns a future and await that future at the application boundary. During tracked handling, the default consumer waits for registered outgoing writes before advancing its position. Processing remains at least once: a crash can cause a message to be handled again, so external side effects should be idempotent. See Consumer settings.

A Message holds the payload, metadata, ID and timestamp. Sending a received message again creates a new dispatch; the SDK assigns the transport information needed for that request. Use application metadata for correlation you want to carry forward, and never treat a caller-supplied message ID or transport source as authenticated identity.

For exact overloads and completion contracts, see GenericGateway and Guarantee . Message replacement APIs are documented in DeserializingMessage .


© 2026 Fluxzero