https://github.com/fluxzero-io/fluxzero-sdk-java
Client libraries to interface with Fluxzero Runtime
https://github.com/fluxzero-io/fluxzero-sdk-java
Last synced: 11 days ago
JSON representation
Client libraries to interface with Fluxzero Runtime
- Host: GitHub
- URL: https://github.com/fluxzero-io/fluxzero-sdk-java
- Owner: fluxzero-io
- License: apache-2.0
- Created: 2025-09-05T09:41:44.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2026-06-29T06:07:47.000Z (15 days ago)
- Last Synced: 2026-06-29T08:24:57.832Z (15 days ago)
- Language: Java
- Homepage:
- Size: 497 MB
- Stars: 18
- Watchers: 0
- Forks: 2
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
- awesome-java - Fluxzero
README
# Fluxzero Java SDK
[](https://github.com/fluxzero-io/fluxzero-sdk-java/actions)
[](https://central.sonatype.com/artifact/io.fluxzero/fluxzero-sdk-java?smo=true)
[](https://fluxzero-io.github.io/fluxzero-sdk-java/javadoc/apidocs/)
[](https://raw.githubusercontent.com/fluxzero-io/fluxzero-sdk-java/refs/heads/main/docs/cheatsheet.pdf)
[](https://www.apache.org/licenses/LICENSE-2.0)
This repository contains the official Java SDK for [Fluxzero](https://fluxzero.io). For a short overview of
functionalities, check out this [cheatsheet](docs/cheatsheet.pdf).
---
## Installation
### Maven Users
Import the [Fluxzero BOM](https://mvnrepository.com/artifact/io.fluxzero/fluxzero-bom) in your
`dependencyManagement` section to centralize version management:
```xml
io.fluxzero
fluxzero-bom
${fluxzero.version}
pom
import
```
Then declare only the dependencies you actually need (no version required):
```xml
io.fluxzero
java-client
io.fluxzero
java-client
tests
test
io.fluxzero
test-server
test
io.fluxzero
proxy
test
```
> 💡 **Note:** The `test-server` and `proxy` modules are **optional**, and can be included if you want to test your
> application locally against an **in-memory Flux server**.
---
### Gradle Users
Use [platform BOM support](https://docs.gradle.org/current/userguide/platforms.html) to align dependency versions
automatically:
Kotlin DSL (build.gradle.kts)
```kotlin
dependencies {
implementation(platform("io.fluxzero:fluxzero-bom:${fluxzeroVersion}"))
implementation("io.fluxzero:java-client")
testImplementation("io.fluxzero:java-client", classifier = "tests")
testImplementation("io.fluxzero:test-server")
testImplementation("io.fluxzero:proxy")
}
```
Groovy DSL (build.gradle)
```groovy
dependencies {
implementation platform("io.fluxzero:fluxzero-bom:${fluxzeroVersion}")
implementation 'io.fluxzero:java-client'
testImplementation('io.fluxzero:java-client') {
classifier = 'tests'
}
testImplementation 'io.fluxzero:test-server'
testImplementation 'io.fluxzero:proxy'
}
```
---
> 🔖 **Tip:** You only need to update the version number **once** in the BOM reference. All included modules will
> automatically align
> with it.
---
## Basic example
Create a new project and add an event class:
```java
public record HelloWorld() {
}
```
Create a handler for the event:
```java
public class HelloWorldEventHandler {
@HandleEvent
void handle(HelloWorld event) {
System.out.println("Hello World!");
}
}
```
Publish the event:
```java
public class ExampleMain {
public static void main(final String[] args) {
var fluxzero = DefaultFluxzero.builder()
.build(LocalClient.newInstance());
fluxzero.registerHandlers(new HelloWorldEventHandler());
fluxzero.eventGateway().publish(new HelloWorld());
}
}
```
Output:
```
Hello World!
```
### With Spring
Fluxzero integrates seamlessly with Spring. Here’s how the above example looks with Spring Boot:
```java
@SpringBootApplication
public class ExampleMain {
public static void main(String... args) {
SpringApplication.run(ExampleMain.class, args);
Fluxzero.publishEvent(new HelloWorld());
}
}
```
And annotate your handler with `@Component`:
```java
@Component
public class HelloWorldEventHandler {
@HandleEvent
void handle(HelloWorld event) {
System.out.println("Hello World!");
}
}
```
> ⚠️ Using Spring non-Boot? Add `@Import(FluxzeroSpringConfig.class)` to register Fluxzero and related beans.
### Testing your handler
Fluxzero includes a powerful TestFixture utility for testing your handlers without needing a full application
or infrastructure setup.
Here’s how to test our HelloWorldEventHandler from earlier:
```java
class HelloWorldEventHandlerTest {
@Test
void testHelloWorldHandler() {
TestFixture.create(new HelloWorldEventHandler())
.whenEvent(new HelloWorld())
.expectThat(fc -> System.out.println(
"Event handled successfully!"));
}
}
```
This will invoke your handler exactly like Fluxzero would in production, but entirely in memory and synchronously
by default.
> ✅ We’ll explore more powerful testing patterns — including assertions on results, published commands, exceptions,
> and full event flows — later in this guide.
---
# Features
The java client supports all features of Fluxzero but also offers plenty of additional functionality.
What follows is a summary of the most important features.
## Table of Contents
### Messaging
- [Message Handling](#message-handling)
- [Tracking Messages](#tracking-messages)
- [Message Replays](#message-replays)
- [Dynamic Dead Letter Queue (DLQ)](#dynamic-dead-letter-queue-dlq)
- [Fluxzero Error Codes](#fluxzero-error-codes)
- [Local Handlers](#local-handlers)
- [Payload Validation](#payload-validation)
- [User and Role-Based Access Control](#user-and-role-based-access-control)
- [Scheduling](#scheduling)
- [User-defined message logs](#user-defined-message-logs)
- [Dispatching Messages](#dispatching-messages)
- [Testing your Handlers](#testing-your-handlers)
- [Handling Web Requests](#handling-web-requests)
- [Serving Static Files](#serving-static-files)
- [Handling WebSocket Messages](#handling-websocket-messages)
- [Testing Web Endpoint Behavior](#testing-web-endpoint-behavior)
- [Outbound Web Requests](#outbound-web-requests)
- [Metrics Messages](#metrics-messages)
### Domain Modeling, Persistence, and Search
- [Domain Modeling](#domain-modeling)
- [Applying Updates to Entities](#applying-updates-to-entities)
- [Nested Entities](#nested-entities)
- [Model Persistence](#model-persistence)
- [Stateful Handlers](#stateful-handlers)
- [Document Indexing and Search](#document-indexing-and-search)
- [Tracking and Updating Documents](#tracking-and-updating-documents)
### Data Handling and Serialization
- [Protecting Sensitive Data](#protecting-sensitive-data)
- [Serialization, Upcasting, and Downcasting](#serialization-upcasting-and-downcasting)
- [Filtering Object Content](#filtering-object-content)
- [Kotlin Support](#kotlin-support)
### Configuration and Extensibility
- [Configuring Application Properties](#configuring-application-properties)
- [Parameter Injection with Custom Resolvers](#parameter-injection-with-custom-resolvers)
- [Interceptors: Dispatching, Handling, and Batching](#interceptors-dispatching-handling-and-batching)
- [Configuring Fluxzero](#configuring-fluxzero)
- [WebSocketClient: Connect to the Fluxzero Runtime](#websocketclient-connect-to-the-fluxzero-runtime)
- [Compatibility and Dependencies](#compatibility-and-dependencies)
---
## Message Handling
Fluxzero is centered around sending and receiving messages — such as __commands__, __events__, __queries__, and
__web requests__. These messages can originate from your own application or any other client connected to the same
Fluxzero Runtime.
Handlers are simply methods annotated with `@HandleCommand`, `@HandleEvent`, `@HandleQuery`, etc. Here’s a basic example
of an event handler that dispatches a command to send a welcome email when a user is created:
```java
class UserEventHandler {
@HandleEvent
void handle(CreateUser event) {
Fluxzero.sendCommand(
new SendWelcomeEmail(event.getUserProfile()));
}
}
```
This handler uses the static `sendCommand` method on Fluxzero, which works because the client is automatically
injected into the thread-local context before message handling begins. This approach eliminates the need to inject
`Fluxzero` into every handler.
To receive that command, you would define a corresponding command handler:
```java
class EmailCommandHandler {
@HandleCommand
void handle(SendWelcomeEmail command) {
//send welcome email to user
}
}
```
Handlers can return a result (e.g. from queries or commands), which will automatically be published as a __Result__
message and sent back to the originating client:
```java
class UserQueryHandler {
@HandleQuery
UserProfile handle(GetUserProfile query) {
//return the user profile
}
}
```
To perform a query and wait for its result synchronously, you can use:
```java
class UserEventHandler {
@HandleEvent
void handle(ResetPassword event) {
UserProfile userProfile = Fluxzero
.queryAndWait(new GetUserProfile(event.getUserId()));
// Perform reset using userProfile
}
}
```
### Returning Futures
Handler methods may also return a `CompletableFuture` instead of a direct value. In that case, Fluxzero will
publish the result to the result log once the future completes:
```java
@HandleQuery
CompletableFuture handle(GetUserProfile query) {
return userService.fetchAsync(query.getUserId());
}
```
This can be useful when calling asynchronous services (e.g. via HTTP or database drivers).
> ⚠️ **Caution:** While supported, returning a future means Flux will consider the message *handled* as soon as the
> handler returns the future—not when the future completes. This can be problematic if:
>
> - The operation must be guaranteed to complete (e.g. business-critical updates),
> - You rely on message acknowledgment for progress tracking,
> - Or you need back-pressure to avoid overloading the system.
>
> For most handlers, **synchronous return types are recommended**, or use `.join()` to explicitly block when necessary.
This pattern works best for non-critical side effects or purely read-oriented queries that can tolerate eventual
completion.
### Handler Matching and Passive Handlers
Fluxzero dynamically resolves which handler method(s) should respond to a message based on **handler specificity**
and **message type**. This resolution behavior is consistent across all message types — including **commands**,
**queries**, **events**, **errors**, **metrics**, and more.
#### Most Specific Handler Wins (Per Class)
If multiple handler methods in the *same class* can handle a message (e.g. a `CreateUser` event), **only the most
specific method** is invoked. This allows you to define fallback methods at a more general level if no exact match is
found.
```java
@HandleEvent
void handle(Object event) {
log.info("Generic fallback");
}
@HandleEvent
void handle(CreateUser event) {
log.info("Handling specific CreateUser event");
}
```
➡️ In this example, only the `handle(CreateUser)` method runs when a `CreateUser` event is dispatched.
#### Multiple Handler Classes Are Invoked
When the same message is handled by **different classes**, all eligible handlers are invoked independently.
```java
public class BusinessHandler {
@HandleEvent
void handle(CreateUser event) {
// perform business logic
}
}
public class LoggingHandler {
@HandleEvent
void logEvent(Object event) {
log.info("Observed event {}", event);
}
}
```
➡️ Here, both handlers are invoked when a `CreateUser` event is dispatched.
#### Requests Prefer a Single Active Handler
For **request-like messages** — such as commands, queries and web requests — only **one handler** class is
expected to produce a response. If multiple are eligible, only the ones marked as **non-passive** will be considered for
producing the result.
Additional handlers may still be registered using `passive = true`, e.g., for logging, auditing, metrics, or outbox
patterns.
```java
public class UserHandler {
@HandleQuery
UserAccount handle(GetUser query) {
return userRepository.find(query.getUserId());
}
}
public class QueryMetricsHandler {
@HandleQuery(passive = true)
void record(Object query) {
metrics.increment("queries." + query.getClass().getSimpleName());
}
}
```
➡️ In this case:
- The `handle(...)` method produces the result.
- The `record(...)` method logs the query,
By combining **handler specificity**, **class-level isolation**, and the `passive` flag, Fluxzero gives you
precise control over how messages are processed — even across mixed concerns like logging, read models, business logic,
and cross-cutting concerns.
### Metadata Support
Messages can include **metadata**, which are contextual key-value pairs (typically of a technical nature). These are
useful for passing user context, correlation IDs, request info, etc.
For example, sending a command with metadata:
[//]: # (@formatter:off)
```java
Fluxzero.sendCommand(
new CreateUser(...),
Metadata.of("userAgent", userAgent)
);
```
[//]: # (@formatter:on)
Reading metadata in a handler is just as easy:
```java
class UserCommandHandler {
@HandleCommand
void handle(CreateUser command, Metadata metadata) {
String userAgent = metadata.get("userAgent");
...
}
}
```
## Tracking Messages
Fluxzero handles message dispatch asynchronously by default. When a message such as a command is published:
1. It is sent to the Fluxzero Runtime.
2. The Runtime logs the message and notifies all subscribed consumers.
3. Consumers stream these messages to the relevant handler methods.
### Default Consumer Behavior
Handlers without an explicit `@Consumer` or matching custom `ConsumerConfiguration` are assigned according to the
configured unconfigured-handler consumer mode. If no mode is configured explicitly, `fluxzero.defaults.version`
selects the default behavior.
For defaults versions `2026.05.20` and newer, the default behavior is `perHandler`:
```properties
fluxzero.defaults.version=2026.05.20
# equivalent explicit setting:
fluxzero.tracking.unconfiguredHandlerConsumerMode=perHandler
```
With this defaults version, Fluxzero creates an isolated default consumer per handler class. The generated consumer uses
the default consumer configuration for the message type as its template and is named after the application and handler
class, for example `my-app_MyHandler`.
Existing applications without `fluxzero.defaults.version`, or with an older defaults version, keep the compatibility
default: unconfigured handlers join the shared application default consumer for the message type. You can choose either
behavior explicitly:
```properties
fluxzero.tracking.unconfiguredHandlerConsumerMode=perHandler
# or
fluxzero.tracking.unconfiguredHandlerConsumerMode=defaultAppConsumer
```
For example:
```java
class MyHandler {
@HandleCommand
void handle(SomeCommand command) {
...
}
}
```
With `perHandler`, this handler gets its own generated command consumer. With `defaultAppConsumer`, it joins the shared
default command consumer. A matching custom `ConsumerConfiguration` or explicit `@Consumer` remains more specific and
takes precedence.
The practical difference is that `perHandler` gives each unconfigured handler its own tracking position and error
isolation. With `defaultAppConsumer`, unconfigured handlers for the same message type move together through one shared
consumer, which preserves the original compatibility behavior for existing applications.
### Custom Consumers with @Consumer
You can override the default behavior using the @Consumer annotation:
```java
@Consumer(name = "MyConsumer")
class MyHandler {
@HandleCommand
void handle(SomeCommand command) {
...
}
}
```
To apply this to an entire package (and its subpackages), add a package-info.java file:
```java
@Consumer(name = "MyConsumer")
package com.example.handlers;
```
### Customizing Consumer Configuration
You can tune the behavior using additional attributes on the @Consumer annotation:
```java
@Consumer(name = "MyConsumer", threads = 2, maxFetchSize = 100, maxFetchBytes = 104_857_600)
class MyHandler {
@HandleCommand
void handle(SomeCommand command) {
...
}
}
```
- threads = 2: Two threads per application instance will fetch commands.
- maxFetchSize = 100: Up to 100 messages fetched per request, helping apply backpressure.
- maxFetchBytes = 104_857_600: Up to 100 MiB of serialized payload bytes fetched per request. Set `0` to disable the
byte limit. Omit it, or set `-1`, to inherit the global default.
Each thread runs a **tracker**. If you deploy the app multiple times, Flux automatically load-balances messages across
all available trackers.
You can set the global default with:
```properties
fluxzero.tracking.maxFetchBytes=104857600
```
### Default Consumer Settings
| Setting | Default Value |
|---------------|---------------------------|
| threads | 1 |
| maxFetchSize | 1024 |
| maxFetchBytes | -1 (inherits 104857600 bytes / 100 MiB) |
These defaults are sufficient for most scenarios. You can always override them for improved performance or control.
---
## Message Replays
Fluxzero allows you to **replay past messages** by tracking from an earlier index in the message log.
This is useful for:
- Rebuilding projections or read models
- Repairing missed or failed messages
- Retrospectively introducing new functionality
Since message logs — including **commands**, **events**, **errors**, etc. — are durably stored (with **events** retained
indefinitely by default), replays are usually available.
#### Replaying from the Past
The most common way to initiate a replay is to define a **new consumer** using the `@Consumer` annotation with a
`minIndex`.
```java
@Consumer(name = "auditReplay", minIndex = 111677748019200000L)
public class AuditReplayHandler {
@HandleEvent
void on(CreateUser event) {
...
}
}
```
In this example, the consumer `auditReplay` will process all events starting from **January 1st, 2024**.
> ℹ️ The `minIndex` value specifies the *inclusive* starting point for the message log.
> Index values are based on time and can be derived using `IndexUtils`:
```java
long index = IndexUtils.indexFromTimestamp(
Instant.parse("2024-01-01T00:00:00Z"));
// returns: 111677748019200000L
```
This approach is perfect for:
- Starting **fresh consumers** for replays
- Bootstrapping projections without interfering with live handlers
- Keeping logic encapsulated and isolated
#### Resetting Existing Consumers
If you want to reset an existing consumer to an earlier point in the log:
[//]: # (@formatter:off)
```java
long replayIndex = 111677748019200000L;
Fluxzero.client()
.getTrackingClient(MessageType.EVENT)
.resetPosition("myConsumer", replayIndex, Guarantee.STORED);
```
[//]: # (@formatter:on)
This restarts the consumer from the given index and causes old messages to be redelivered and reprocessed.
> ⚠️ Replaying over existing handlers may require extra caution (e.g. to avoid duplicating side effects).
#### 🔁 Parallel Replays with `exclusive = false`
Sometimes you want to **re-use the same handler class** for both:
- **Live processing** (e.g. default consumer)
- **Replaying past messages** (e.g. rebuilding projections)
To achieve this, annotate the handler with `exclusive = false`. This tells Flux that the handler may participate in
**multiple consumers simultaneously**:
```java
@Consumer(name = "live", exclusive = false)
public class OrderProcessor {
@HandleCommand
void handle(SendOrder command) {
//submit an order
}
}
```
Now, register a **second consumer** at runtime for the **same handler** using a different tracking position:
[//]: # (@formatter:off)
```java
fluxzeroBuilder.addConsumerConfiguration(
ConsumerConfiguration.builder()
.name("replay") // A new consumer
.handlerFilter(handler -> handler instanceof OrderProcessor) // same class
.minIndex(111677748019200000L) // Start of replay window
.maxIndex(111853279641600000L) // End of replay window
.build(),
MessageType.COMMAND
);
```
[//]: # (@formatter:on)
This will spin up a **parallel tracker** that replays all commands since **2024-01-01T00:00:00Z**
until **2024-02-01T00:00:00Z**, while the original `live` consumer continues uninterrupted. In this example, orders
sent during the month of January 2024 will be resubmitted.
> ✅ This is ideal for **bootstrapping read models** or **running repair jobs** without affecting production flow
> or duplicating a handler class.
---
### Handling Failures by Replaying from the Error Log
Fluxzero automatically logs **all message handling errors** to a dedicated **error log**. This includes failures
for:
- Commands
- Queries
- WebRequests
- Schedule messages
- Events (if a handler throws)
- And more
Each error message includes detailed **stack traces**, **metadata**, and most importantly, a **reference to the original
message** that caused the failure.
### Error Handling with `@HandleError`
To react to failures programmatically, define a handler method with the `@HandleError` annotation:
```java
@HandleError
void onError(Throwable error) {
log.warn("Something went wrong!", error);
}
```
You can inject the **failed message** using the `@Trigger` annotation:
```java
@HandleError
void onError(Throwable error, @Trigger SomeCommand failedCommand) {
log.warn("Failed to process {}: {}", failedCommand, error.getMessage());
}
```
> ℹ️ The trigger can be the payload, or a full `Message` or `DeserializingMessage`.
You can also **filter** error handlers by trigger type, message type, or originating consumer:
```java
@HandleError
@Trigger(messageType = MessageType.COMMAND, consumer = "my-app")
void retryFailedCommand(MyCommand failed) {
Fluxzero.sendCommand(failed);
}
```
---
## Dynamic Dead Letter Queue (DLQ)
The **error log is durable** and **replayable**, which means you can treat it as a **powerful, dynamic DLQ**.
Here’s how:
1. **Deploy a special consumer** that tracks the error log.
2. Use `@Trigger` to access and inspect failed messages.
3. Filter and replay failures based on time, payload type, or originating app.
### Example: Retrying Failed Commands from the Past
Let’s assume a bug caused command processing to fail in January 2024. The following setup reprocesses those failed
commands:
```java
@Consumer(name = "command-dlq",
minIndex = 111677748019200000L, maxIndexExclusive = 111853279641600000L) // 2024-01-01 to 2024-02-01
class CommandReplayHandler {
@HandleError
@Trigger(messageType = MessageType.COMMAND)
void retry(MyCommand failed) {
Fluxzero.sendCommand(failed);
}
}
```
> ✅ The original `MyCommand` payload is restored and retried transparently.
> 🧠 You can combine this with logic that deduplicates, transforms, or **selectively suppresses** retries.
### When to Use the Error Log
| Use Case | How the Error Log Helps |
|--------------------------------|--------------------------------------|
| 🛠 Fix a bug retroactively | Replay failed commands from the past |
| 🚧 Validate new handler logic | Test it against real-world errors |
| 🔁 Retry transient failures | Re-issue requests with retry logic |
| 🧹 Clean up or suppress errors | Filter out known false-positives |
The error log acts as a **time-travel debugger** — it gives you full control over how and when to address failures, now
or in the future.
---
### Fluxzero Error Codes
Some SDK exceptions include a stable Fluxzero error code in their message and, where the exception type supports it,
via `getErrorCode()` and `getDocumentationUrl()`. The explanatory text can improve over time, but the code should
remain stable so logs, support notes, and docs can all point at the same problem.
Once published, error codes are treated as part of the SDK support contract: new categories get new codes, existing
codes are not reused for a different meaning.
To keep repeated failures from flooding logs, the SDK renders the full multi-line explanation for the first 100
occurrences of a code in a JVM. Later occurrences use a short message with the same error code and documentation URL.
| Code | Meaning | Typical fix |
|---------------|----------------------------------------------|-------------|
| `FZ-SDK-0001` | No Fluxzero instance is available | Create a Fluxzero instance and run the code inside `fluxzero.apply(...)`, or set `Fluxzero.applicationInstance`. |
| `FZ-SDK-0002` | Request timed out while waiting for a result | Register/start the matching handler, check namespace/topic/routing, or use fire-and-forget for commands that do not return a result. |
| `FZ-SDK-0003` | Handler invocation failed | Inspect the cause and stack trace; use `FunctionalException` for expected business rejections. |
| `FZ-SDK-0004` | Response dispatch failed | Check response serialization, dispatch interceptors, and result/web-response log connectivity. |
| `FZ-SDK-0005` | Thread interrupted while waiting | Check caller shutdown, cancellation, or timeout behavior and decide whether to retry. |
| `FZ-SDK-0006` | Message dispatch failed | Check dispatch interceptors, serialization, topic/namespace configuration, and client connectivity. |
| `FZ-SDK-0007` | No `UserProvider` is configured | Configure `Fluxzero#userProvider`, set `UserProvider.defaultUserProvider`, or remove `Message.addUser(...)`. |
| `FZ-SDK-0008` | Tracking consumer configuration is invalid | Make consumer names unique, register handlers before tracking starts, or add a matching `@Consumer`/`ConsumerConfiguration`. |
| `FZ-SDK-0009` | Periodic schedule configuration is invalid | Add a cron expression or positive delay to `@Periodic`, or disable it explicitly with `Periodic.DISABLED`. |
| `FZ-SDK-0010` | Tracking failed during runtime | Check the original cause, client/message-store connectivity, and whether shutdown or interruption was expected. |
Each code links to `https://fluxzero.io/docs/errors#` in formatted SDK messages.
---
### Routing with `@RoutingKey`
In Fluxzero, routing is used to assign messages to **segments** using consistent hashing. This ensures that
messages about the same entity — for example, all events for a given `OrderId` — are always handled by the **same
consumer**, in **the correct order**.
This is critical when you're handling messages **in parallel**, but still want to ensure **per-entity consistency**.
#### Declaring the Routing Key
By default, the routing key is derived from the message ID. But you can override this by annotating a field, getter, or
method in your **payload class** with `@RoutingKey`.
```java
public record ShipOrder(@RoutingKey OrderId orderId) {
}
```
Or explicitly reference a nested property:
```java
@RoutingKey("customer/id")
public record OrderPlaced(Customer customer) {
}
```
This instructs Flux to extract `customer.id` and use it as the routing key when publishing or consuming the message.
#### Handler-Level Routing Keys
In more advanced cases, you may want to **override routing at the handler level**, regardless of how the message was
published. You can place `@RoutingKey(...)` on the handler method itself:
```java
@HandleEvent
@RoutingKey("organisationId")
void handle(OrganisationUpdate event) {
// Will route based on organisationId in metadata or payload
}
```
> ⚠️ When doing this, be sure to declare your consumer with `ignoreSegment = true`. Otherwise, this routing override
> may cause certain messages to be silently skipped.
```java
@Consumer(ignoreSegment = true)
public class OrganisationHandler {
...
}
```
#### Metadata-Based Routing
Routing keys can also be extracted from **message metadata**. For example:
```java
@RoutingKey("userId")
public class AuditLogEntry { ...
}
```
This will first try to extract `userId` from metadata, and fall back to the payload if not present.
#### Summary
| Placement | Meaning |
|----------------|-----------------------------------------------------------------------|
| Field/getter | Use the property's value as routing key |
| Class-level | Use the named property in metadata or payload |
| Handler method | Overrides routing key used during handling (requires `ignoreSegment`) |
---
## Local handlers
Fluxzero supports both asynchronous and local (synchronous) message handling. **Local handlers** process messages
in the same thread that published them, bypassing the message dispatch infrastructure entirely. This typically results
in faster response times and is ideal for simple or time-sensitive use cases.
To define a local handler, annotate the handler method, class, or its enclosing package with `@LocalHandler`:
```java
@LocalHandler(logMetrics = true)
public class SomeLocalHandler {
@HandleEvent
void handle(ApplicationStarted event) {
//do something
}
}
```
> 💡 Use logMetrics = true to track performance metrics even for local handlers.
### Self-handling messages
Instead of defining message handlers externally, you can embed handler logic directly in the message payload. This is
often useful for queries or simple commands.
```java
public class GetUserProfile {
String userId;
@HandleQuery
UserProfile handle() {
//fetch the user profile and return
}
}
```
By default, such handlers are treated as local. To process them asynchronously (i.e., as part of a consumer),
annotate the class with `@TrackSelf`:
```java
@TrackSelf
@Consumer(name = "user-management")
public class GetUserProfile {
String userId;
@HandleQuery
UserProfile handle() {
// Async handler
}
}
```
When component-scanned (e.g., via Spring), `@TrackSelf` classes will be automatically discovered and registered.
This works even if the annotation is placed on an interface rather than the concrete class—allowing for reusable handler
patterns.
For example, a generic command handler interface can be tracked and reused:
```java
@TrackSelf
public interface UserUpdate {
@HandleCommand
default void handle() {
//default behavior
}
}
```
Implementations of this interface will then be handled asynchronously, using the configured consumer (or the default
one if unspecified).
### Response typing with Request
Fluxzero allows you to formalize the expected return type of commands and queries by implementing the `Request`
interface. This lets the framework:
- Infer the response type at runtime and during testing.
- Validate handler methods at compile-time (e.g., `@HandleQuery` must return an R).
- Simplify `queryAndWait(...)` or `sendCommand(...)` invocations.
Here’s an example for a query:
```java
@Value
public class GetUserProfile implements Request {
String userId;
@HandleQuery
UserProfile handle() {
return loadProfile(userId);
}
}
```
Now, when calling this query, the return type is automatically known:
```java
UserProfile profile = Fluxzero.queryAndWait(new GetUserProfile("123"));
```
When implementing Request, the expected result type (R) is automatically inferred during testing and execution. This
enables type-safe tests like:
[//]: # (@formatter:off)
```java
testFixture.whenQuery(new GetUserProfile("123"))
.expectResult(profile -> profile.getUserId().equals("123"));
```
[//]: # (@formatter:on)
If a class implements `Request`, Flux will use its declared generic type (R) to check that:
- A compatible handler exists
- The handler returns the correct result type
- The correct response is expected during testing
> This pattern is highly recommended for queries, as it reduces boilerplate and improves correctness.
---
## Payload Validation
Fluxzero automatically validates incoming request payloads using a built-in
[Jakarta Validation](https://jakarta.ee/specifications/bean-validation/3.1/) implementation. Hibernate Validator is
not required for normal SDK payload validation.
This includes support for:
- standard Jakarta constraints such as `@NotNull`, `@NotBlank`, `@Size`, `@Pattern`, numeric constraints and temporal
constraints
- Fluxzero convenience constraints such as `@Length`, `@Range`, `@URL`, `@UUID`, `@CreditCardNumber`, and
`@UniqueElements`
- `@Valid` on nested objects and container/type-use validation for arrays, `List`, `Map`, `Optional`, and registered
custom value extractors
- validation groups, group sequences, group conversion, and custom constraint validators
- executable parameter and return-value validation
- contextual method constraints such as `@AssertTrue` methods that inject parameters via Fluxzero's configured
`ParameterResolver`s
- Constraint violations in command/query/webrequest payloads
The supported profile is aimed at SDK usage, not at being a drop-in provider for every Jakarta Validation TCK edge
case. XML mappings, `validation.xml`, CDI lifecycle integration, TraversableResolver reachability rules, and full
Expression Language message evaluation are intentionally not supported.
If a constraint is violated, the handler method is **never called**. Instead, a `ValidationException`,
is thrown before the handler is invoked.
```java
public record CreateUser(@NotBlank String userId,
@NotNull @Valid UserProfile profile) {
}
```
Constraint methods on a payload may also request contextual parameters that the SDK's default validator can inject while
a message is being handled. It uses the same resolver set as handler method injection, so values such as `User`,
`Message`, `DeserializingMessage`, `Metadata`, and custom resolver values can be used without reaching for thread-local
helpers.
```java
public record CreateUser(@NotBlank String userId) {
@AssertTrue(message = "Only admins may create admin users")
boolean allowedBy(User user, Message message) {
return !userId.startsWith("admin-") || user != null && user.hasRole("admin");
}
}
```
If a constrained method declares parameters that cannot be resolved in the current validation context, Fluxzero skips
that method for that validation run. Keep required structural checks on fields or no-argument constraint methods when a
rule must also apply outside message handling.
You can disable this validation entirely by calling:
[//]: # (@formatter:off)
```java
DefaultFluxzero.builder().disablePayloadValidation();
```
[//]: # (@formatter:on)
Of course, it is also easy to provide your own validation if desired. Use `replaceValidator(...)` on the
`FluxzeroBuilder` to replace the configured validator. Convenience methods on `ValidationUtils` delegate to the
validator of the current `Fluxzero` instance when one is bound, and otherwise fall back to
`ValidationUtils.defaultValidator`.
> 💡 **Tip**: Fluxzero automatically correlates errors with the triggering message (e.g.: command or event).
>
> This means you don’t need to log extra context like the message payload or user ID — that information is already
> available in the audit trail in Fluxzero Runtime. This also encourages using **clear, user-facing error messages**
> without leaking internal details.
---
## User and Role-Based Access Control
Fluxzero allows you to restrict message handling based on the authenticated user's roles. This access control
happens **before** the message reaches the handler — similar to how payload validation is enforced.
There are several annotations for declaring user and role requirements:
### `@RequiresAnyRole`
Use this annotation to ensure that a handler is only invoked if the user has **at least one** of the
specified roles.
```java
@HandleCommand
@RequiresAnyRole({"admin", "editor"})
void handle(UpdateArticle command) { ...}
```
```java
@RequiresAnyRole("admin")
public record DeleteAccount(String userId) {
}
```
### `@ForbidsAnyRole`
This annotation works the other way around — it **prevents** message handling if the user has any of the specified
roles.
```java
@ForbidsAnyRole("guest")
@HandleCommand
void handle(SensitiveOperation command) { ...}
```
### `@RequiresUser`
Ensures that a message can only be handled if an **authenticated user** is present. If no user is found, the message is
rejected with an `UnauthenticatedException`.
This is useful for requiring login in scenarios like user account updates, sensitive commands, or personal data access.
```java
@RequiresUser
@HandleCommand
void handle(UpdateProfile command) { ...}
```
### `@NoUserRequired`
Allows a message to be processed even if **no authenticated user** is present — ideal for public APIs, or health checks.
```java
@NoUserRequired
@HandleCommand
void handle(SignUpUser command) { ...}
```
### `@ForbidsUser`
Prevents message handling if an **authenticated user is present**. This is useful for restricting certain flows to
unauthenticated users — such as registration endpoints, or guest-only operations.
```java
@ForbidsUser
@HandleCommand
void handle(SignUpAsGuest command) { ... }
```
---
### Controlling Behavior on Unauthorized Access
All authorization annotations include an optional `throwIfUnauthorized()` property (default: `true`) that controls what
happens when access is denied:
- If `throwIfUnauthorized = true`:
- If a user is required but **not present**, an `UnauthenticatedException` is thrown.
- If a user is present but **lacks the required roles**, an `UnauthorizedException` is thrown.
- If `throwIfUnauthorized = false`:
- The handler or message is silently **skipped**, allowing delegation to other eligible handlers (if any).
This mechanism allows fine-grained control over how authentication and authorization failures are handled across your
system.
---
### Role annotations support nesting and overrides
Flux evaluates these annotations hierarchically. For example:
- If `@RequiresAnyRole("admin")` is placed on a **package**, it applies to all handlers and payloads in that package by
default.
- You can override that requirement on a specific method or class using `@RequiresAnyRole(...)`, `@ForbidsAnyRole(...)`,
or `@NoUserRequired`.
```java
// package-info.java
@RequiresUser
package com.myapp.handlers;
```
```java
@NoUserRequired
@HandleCommand
void handle(PublicPing ping) { ...} // Overrides the package-level requirement
```
This allows you to apply coarse-grained defaults and override them where needed.
---
### Enum-based role annotations (meta-annotations)
For more structure, you can define **custom annotations** using enums or strongly typed roles. For example:
```java
public enum Role {
ADMIN, EDITOR, USER
}
```
```java
@RequiresAnyRole
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RequiresRole {
Role[] value();
}
```
You can now annotate your handlers like this:
```java
@HandleCommand
@RequiresRole(Role.ADMIN)
void handle(DeleteAccount command) { ...}
```
Flux will interpret the enum-based annotation through the underlying `@RequiresAnyRole`.
---
### Best Practices
- Use role annotations on **payload classes** to guarantee strict access checks in all environments.
- Use them on **handlers** to control fallback behavior or define role-specific processing.
- Set security defaults on **packages** or base classes, and override selectively.
- Define **custom annotations** to avoid scattering string-based role declarations.
---
> 💡 **Tip:** Access control is enforced transparently — there’s no need to log or repeat the user or message context.
> Flux automatically maintains correlation metadata between the original request and any errors, logs, or events that
> follow.
---
### Where does user info come from?
User roles are resolved by the configured `UserProvider`, which extracts the current user from message metadata (e.g.,
authentication tokens, headers, etc.). By default, Fluxzero uses a pluggable SPI to register this provider.
> 💡 You can override or mock this provider in tests using the TestFixture API.
---
### Providing Your Own User Logic
You can implement a custom `UserProvider` to extract users from headers, JWT tokens, cookies, etc.
```java
public class MyUserProvider extends AbstractUserProvider {
public MyUserProvider() {
super("Authorization", MyUser.class); // metadata key and user type
}
@Override
public User fromMessage(HasMessage message) {
if (message.toMessage() instanceof WebRequest request) {
return decodeToken(request.getHeader("Authorization"));
}
return super.fromMessage(message);
}
private User decodeToken(String header) {
// Implement your own token decoding and verification logic here
return ...;
}
}
```
This allows you to inject meaningful, application-specific `User` objects into your handlers.
---
### System and Testing Support
Your `UserProvider` implementation can also support testing and system behavior by implementing:
- `getSystemUser()` — returns a default **system-level user**, used:
- as the default actor in tests,
- when publishing system-side effects (e.g. from scheduled handlers).
- `getUserById(...)` — resolves a user by ID, used in test utilities like `fixture.whenCommandByUser(...)`.
This ensures your custom user logic is consistently applied, even in automated tests and background execution.
---
### 🔧 Registering your UserProvider
To enable your custom `UserProvider` (e.g., for authenticating users via headers or tokens), register it using Java's
Service Provider mechanism.
Create the file:
```
src/main/resources/META-INF/services/io.fluxzero.sdk.tracking.handling.authentication.UserProvider
```
List your implementation classes (one per line) in order of preference:
```
com.example.authentication.SenderProvider
com.example.authentication.SystemUserProvider
```
Fluxzero will automatically discover and register them at startup.
> 💡 If you're using Spring, your `UserProvider` can also be exposed as a bean — Fluxzero will pick it up
> automatically.
>
> ⚠️ However, tests **not using Spring** will not pick up the bean. For test scenarios or CLI usage, the SPI mechanism
> is still the easiest.
---
## Scheduling
Fluxzero allows scheduling messages for future delivery using the `MessageScheduler`.
Here’s an example that schedules a termination event 30 days after an account is closed:
```java
class UserLifecycleHandler {
@HandleEvent
void handle(AccountClosed event) {
Fluxzero.schedule(
new TerminateAccount(
event.getUserId()),
"AccountClosed-" + event.getUserId(),
Duration.ofDays(30)
);
}
@HandleEvent
void handle(AccountReopened event) {
Fluxzero.cancelSchedule(
"AccountClosed-" + event.getUserId());
}
@HandleSchedule
void handle(TerminateAccount schedule) {
// Perform termination
}
}
```
Alternatively, you can schedule commands using `scheduleCommand`:
```java
class UserLifecycleHandler {
@HandleEvent
void handle(AccountClosed event) {
Fluxzero.scheduleCommand(
new TerminateAccount(
event.getUserId()),
"AccountClosed-" + event.getUserId(),
Duration.ofDays(30));
}
@HandleEvent
void handle(AccountReopened event) {
Fluxzero.cancelSchedule("AccountClosed-" + event.getUserId());
}
}
```
### Periodic scheduling
Fluxzero supports recurring message schedules via the `@Periodic` annotation. This makes it easy to run tasks on a
fixed interval or cron-based schedule — useful for polling, maintenance, background processing, and more.
You can apply `@Periodic` to either a `Schedule` payload or a `@HandleSchedule` method:
```java
@Periodic(delay = 5, timeUnit = TimeUnit.MINUTES)
public record RefreshData(String index) {
}
```
This schedules RefreshData to run every 5 minutes.
Or, to use a cron expression:
```java
@Periodic(cron = "0 0 * * MON", timeZone = "Europe/Amsterdam")
@HandleSchedule
void weeklySync(PollData schedule) {
...
}
```
This example triggers the `weeklySync` method every Monday at 00:00 in the Amsterdam time zone.
Schedule handlers can also be local. Add `@LocalHandler` to a schedule handler, or put the `@HandleSchedule` method
directly on the scheduled payload type without `@TrackSelf`. In those cases Fluxzero uses its `TaskScheduler` to trigger
the schedule in the current application, which makes `@HandleSchedule`/`@Periodic` useful as an alternative to Spring's
`@Scheduled` for Fluxzero-aware local jobs.
#### Behavior and advanced options
- `@Periodic` works only for scheduled messages (see `@HandleSchedule`).
- The schedule **automatically reschedules** itself after each invocation unless canceled.
- You may:
- Return `void` or `null` to continue with the same schedule.
- Return a `Duration` or `Instant` to override the next deadline.
- Return a new payload or `Schedule` to customize the next cycle.
- If an error occurs:
- The schedule continues by default (`continueOnError = true`).
- You may specify a fallback delay via `delayAfterError`.
- Throw `CancelPeriodic` from the handler to stop the schedule completely.
- To prevent startup activation, use `@Periodic(autoStart = false)`.
- `initialDelay = -1` means no explicit initial delay. With compatibility defaults, Fluxzero treats that implicit value
as `0` to preserve immediate autostart. With `fluxzero.defaults.version >= 2026.05.21` or
`fluxzero.scheduling.periodic.useDefaultInitialDelay=true`, fixed-delay schedules first run after `delay` and cron
schedules first run at the next cron match. For example, `@Periodic(delay = 60_000)` first runs after 60 seconds,
while `@Periodic(cron = "*/5 * * * *")` first runs at the next five-minute boundary. Set `initialDelay = 0` when the
first run should be immediate.
- The schedule ID defaults to the class name, but can be customized with `scheduleId`.
Here's an example of robust polling with error fallback:
```java
@Periodic(delay = 60, timeUnit = TimeUnit.MINUTES, delayAfterError = 10)
@HandleSchedule
void pollExternalService(PollTask pollTask) {
try {
externalService.fetchData();
} catch (Exception e) {
log.warn("Polling failed, will retry in 10 minutes", e);
throw e;
}
}
```
In this case:
- The task runs every hour normally.
- If it fails, it retries after 10 minutes.
- It resumes the original schedule if the next invocation succeeds.
---
## User-defined message logs
Fluxzero supports **custom message logs** in addition to built-in ones like commands, events, and queries.
Most applications won't need custom message logs — but they can be very useful in advanced scenarios:
- To **track external systems or integrations**, while keeping the main event log clean.
- To **store batches of updates** for external consumers that poll infrequently.
- To **segment logic** or apply different retention guarantees per topic.
Fluxzero lets you publish and handle custom messages by assigning them to **user-defined topics**. These messages
are written to their own durable logs and can be tracked just like commands or events.
### Custom Handlers
To receive messages from a custom log, annotate your method with `@HandleCustom` and specify the topic name:
```java
@HandleCustom("metering-points")
void on(MeteringPoint event) {
log.info("Metering point: {}", event);
}
```
Like other handlers, this can be:
- **Stateless or stateful**
- Attached to a **custom consumer**
- Used to **return a result** if the message is a request
- **Passive**, meaning it won’t emit result messages
> ✅ Custom logs are fully integrated with Flux tracking and delivery infrastructure.
### 📬 Publishing to a Custom Log
You can publish messages manually to any custom topic:
[//]: # (@formatter:off)
```java
Fluxzero.get()
.customGateway("third-party-events")
.sendAndForget(new AuditEntry("User login"));
```
[//]: # (@formatter:on)
This makes it easy to introduce new asynchronous workflows, specialized event types, or low-frequency control signals.
### 🧹 Setting Retention Time
Each message log in Flux retains messages independently. You can configure how long messages in your **custom log**
should be retained:
[//]: # (@formatter:off)
```java
Fluxzero.get()
.customGateway("third-party-events")
.setRetentionTime(Duration.ofDays(90));
```
[//]: # (@formatter:on)
Retention can also be set for built-in logs like commands or results — though it's advisable to configure it only
for custom topics.
---
## Dispatching Messages
Fluxzero provides a unified and transparent way to send messages of all types—**commands**, **events**,
**queries**, **schedules**, **web requests**, **metrics**, and more. All message types are routed through a shared
dispatch
infrastructure, with built-in support for:
- **Location transparency**: Message destinations are resolved dynamically. Handlers may live within the current
process or in a remote application across the globe—as long as it's connected to the same Fluxzero Runtime.
- **Dispatch interceptors** for metadata enrichment, authorization, logging, or suppression.
- **Local-first handling**: If a handler exists in the local process, it will receive the message immediately.
- **Automatic forwarding** to the Fluxzero Runtime when no local handlers consume the message.
- **Serialization and correlation**: All messages are serialized, tagged with tracing data, and routed by type and
topic.
### Sending a Message
The easiest way to dispatch messages is via static methods on the `Fluxzero` class:
[//]: # (@formatter:off)
```java
Fluxzero.sendCommand(new CreateUser("Alice")); // Asynchronously send a command
Fluxzero.queryAndWait(new GetUserById("user-123")); // Query and block for response
Fluxzero.publishEvent(new UserSignUp(...)); // Fire-and-forget event
Fluxzero.schedule(new RetryPayment(...), Duration.ofMinutes(5)); // Delayed message
```
[//]: # (@formatter:on)
Each message can also include optional metadata:
[//]: # (@formatter:off)
```java
Fluxzero.sendCommand(new CreateUser("Bob"),
Metadata.of("source","admin-ui"));
```
[//]: # (@formatter:on)
> ⚠️ Messages are **not routed to a specific destination**. Instead, any number of matching handlers may receive the
> message, whether they run locally or remotely. Fluxzero abstracts the transport so that handler location is
> irrelevant.
For a deeper look at message handling, see the [Message Handling](#message-handling) section.
---
### Commands
Commands are used to trigger state changes or domain logic. They may return a result or be fire-and-forget.
**Send and forget:**
[//]: # (@formatter:off)
```java
Fluxzero.sendAndForgetCommand(new CreateUser("Alice"));
```
[//]: # (@formatter:on)
**Send and wait (async or blocking):**
[//]: # (@formatter:off)
```java
CompletableFuture future
= Fluxzero.sendCommand(new CreateUser("Bob"));
UserId id = Fluxzero.sendCommandAndWait(
new CreateUser("Charlie"));
```
[//]: # (@formatter:on)
---
### Queries
Queries retrieve data from read models or projections.
**Async:**
[//]: # (@formatter:off)
```java
CompletableFuture result = Fluxzero.query(new GetUserProfile("user123"));
```
[//]: # (@formatter:on)
**Blocking:**
[//]: # (@formatter:off)
```java
UserProfile profile = Fluxzero.queryAndWait(new GetUserProfile("user456"));
```
[//]: # (@formatter:on)
---
### Events
Events can be published via:
[//]: # (@formatter:off)
```java
Fluxzero.publishEvent(new UserLoggedIn("user789"));
```
[//]: # (@formatter:on)
By default:
- ✅ **Events are persisted** in the global event log, making them available for downstream processing, projections, or
analytics.
- ⚠️ **Exception:** If a **local handler** exists the event will not be forwarded or stored, unless
`@LocalHandler(logMessage = true)`.
> For aggregate-related domain events, use `Entity#apply(...)` instead. This ensures the event is applied to the entity
> and conditionally published and logged, depending on the aggregate configuration.
---
### Schedules
You can schedule messages or commands for later delivery:
[//]: # (@formatter:off)
```java
// Schedule a message to be handled in 5 minutes using @HandleSchedule
Fluxzero.schedule(new ReminderFired(), Duration.ofMinutes(5));
// Fire a periodic schedule every hour
Fluxzero.schedulePeriodic(new PollExternalApi());
```
[//]: # (@formatter:on)
---
### Web Requests
Send an outbound HTTP call via the proxy mechanism in Fluxzero Runtime:
[//]: # (@formatter:off)
```java
WebRequest request = WebRequest
.get("https://api.example.com/data").build();
WebResponse response = Fluxzero.get()
.webRequestGateway().sendAndWait(request);
```
[//]: # (@formatter:on)
---
### Metrics
You can publish custom metrics to the Fluxzero Runtime:
[//]: # (@formatter:off)
```java
Fluxzero.publishMetrics(
new SystemLoadMetric(cpu, memory));
```
[//]: # (@formatter:on)
---
### What Happens After You Dispatch?
Once you dispatch a message using any of the above methods, Flux goes through the following pipeline:
#### 1. **Dispatch Interceptors (Pre-Serialization)**
Before the message is serialized, all applicable `DispatchInterceptor`s are given a chance to:
- Inject metadata (e.g. correlation IDs, timestamps)
- Modify or validate the message
- Suppress or block dispatch entirely
#### 2. **Local Handlers**
If any handlers (e.g. `@HandleCommand`, `@HandleQuery`) are registered for this message type and topic, they will be
invoked **locally**.
- If the handler is annotated with `@LocalHandler(logMessage = true)`, the message is still forwarded to the
Fluxzero Runtime.
- If the handler **fully consumes** the message, it won't be forwarded.
#### 3. **Serialization**
The message is converted to a `SerializedMessage`, typically using Jackson. This is where versioning and up/downcasting
may apply.
#### 4. **Dispatch Interceptors (Post-Serialization)**
After serialization, interceptors may again modify the message—for example, to inject correlation headers based on the
serialized form.
#### 5. **Forwarding to Fluxzero Runtime**
If no local handler consumes the message, it is published to the Fluxzero Runtime via the configured `Client` (e.g.
`WebSocketClient`).
- Topics and routing are determined by message type and metadata
- Runtime-side logic (like deduplication, retries, rate limits) may apply
---
### Request Timeouts
The `@Timeout` annotation allows developers to specify how long Flux should wait for a **command** or **query** to
complete when using synchronous (`sendAndWait`) APIs.
This is especially useful for time-sensitive interactions where you want to fail fast or have specific SLA expectations
for certain request types.
### Usage
Apply `@Timeout` to a **payload class** (typically a command or query):
[//]: # (@formatter:off)
```java
@Timeout(value = 3, timeUnit = TimeUnit.SECONDS)
public record CalculatePremium(UserProfile profile) implements Request {}
```
[//]: # (@formatter:on)
When this message is sent using a blocking gateway call, the configured timeout is respected:
[//]: # (@formatter:off)
```java
BigDecimal result = Fluxzero
.sendAndWait(new CalculatePremium(user));
```
[//]: # (@formatter:on)
If the timeout elapses before a response is received, a `TimeoutException` is thrown.
### Notes
- The timeout only applies to blocking calls (e.g., `sendAndWait`). If you use non-blocking `send(...)` methods that
return a `CompletableFuture`, **no timeout is enforced** unless you manually attach one:
[//]: # (@formatter:off)
```java
CompletableFuture future
= Fluxzero.send(new CalculatePremium(user));
future.orTimeout(3, TimeUnit.SECONDS);
```
[//]: # (@formatter:on)
- The default timeout for blocking operations (when `@Timeout` is not present) is **1 minute**.
- To configure timeouts for `WebRequest` calls, use `WebRequestSettings#timeout`.
---
## Testing your Handlers
Fluxzero comes with a flexible, expressive testing framework based on the given-when-then pattern. This enables
writing behavioral tests for your handlers without needing to mock the infrastructure.
Here’s a basic example:
```java
TestFixture testFixture = TestFixture.create(new UserEventHandler());
@Test
void newUserGetsWelcomeEmail() {
testFixture.whenEvent(new CreateUser(userId, myUserProfile))
.expectCommands(new SendWelcomeEmail(myUserProfile));
}
```
This test ensures that when a `CreateUser` event occurs, a `SendWelcomeEmail` command is issued by the handler.
### Testing complete workflows
You can test full workflows across multiple handlers:
```java
TestFixture fixture = TestFixture.create(new UserCommandHandler(), new UserEventHandler());
@Test
void creatingUserTriggersEmail() {
fixture.whenCommand(new CreateUser(userProfile))
.expectCommands(new SendWelcomeEmail(userProfile));
}
```
Use `expectOnlyCommands()` to assert that **only** the expected command was issued:
[//]: # (@formatter:off)
```java
fixture.whenCommand(new CreateUser(userProfile))
.expectOnlyCommands(new SendWelcomeEmail(userProfile));
```
[//]: # (@formatter:on)
You can also match by class, predicate, or Hamcrest matcher:
[//]: # (@formatter:off)
```java
fixture.whenCommand(new CreateUser(userProfile))
.expectCommands(SendWelcomeEmail.class,
isA(AddUserToOrganization.class));
```
[//]: # (@formatter:on)
### Chained expectations
Multiple expectations can be chained to test the full sequence of events and commands:
[//]: # (@formatter:off)
```java
fixture.whenCommand(new CreateUser(userProfile))
.expectCommands(new SendWelcomeEmail(userProfile))
.expectEvents(new UserStatsUpdated(...));
```
[//]: # (@formatter:on)
You can also chain multiple **inputs** using `.andThen()` to simulate a sequence of events, commands, or queries:
[//]: # (@formatter:off)
```java
fixture.whenCommand(new CreateUser(userProfile))
.expectCommands(new SendWelcomeEmail(userProfile))
.andThen()
.whenQuery(new GetUser(userId))
.expectResult(userProfile);
```
[//]: # (@formatter:on)
This example first triggers a `CreateUser` command, expects a `SendWelcomeEmail` event, and then issues a `GetUser`
query,
asserting that it returns the expected result.
### Using givenXxx() for preconditions
Use `givenCommands`, `givenEvents`, etc., to simulate preconditions:
[//]: # (@formatter:off)
```java
fixture.givenCommands(new CreateUser(userProfile), new ResetPassword(...))
.whenCommand(new UpdatePassword(...))
.expectEvents(UpdatePassword.class);
```
[//]: # (@formatter:on)
### Providing External JSON Files
Test fixtures support loading inputs from external JSON resources. This allows you to keep your tests clean and reuse
structured input data.
Any `givenXyz(...)`, `whenXzy(...)`, or `expectXyz(...)` method argument that is a `String` ending with `.json` will be
interpreted as a **classpath resource path**, and deserialized accordingly.
For example:
[//]: # (@formatter:off)
```java
fixture.givenCommands("create-user.json")
.whenQuery(new GetUser(userId))
.expectResult("user-profile.json");
```
[//]: # (@formatter:on)
If your test class is in the `org.example` package, this will resolve to `/org/example/create-user.json` in the
classpath, unless the JSON path is absolute (starts with `/`), e.g.:
```java
fixture.givenCommands("/users/create-user.json");
```
#### Class Resolution with `@class`
Each JSON file must include a `@class` property to enable deserialization:
```json
{
"@class": "org.example.CreateUser",
"userId": "3290328",
"email": "foo.bar@example.com"
}
```
If your classes or packages are annotated with `@RegisterType`, you can use **simple class names**:
```json
{
"@class": "CreateUser"
}
```
Or partial paths:
```json
{
"@class": "example.CreateUser"
}
```
> This enables readable and concise references while still resolving to the correct type.
For Kotlin projects, use `kapt` to run the Java annotation processor. Because Kotlin typically does not use
`package-info.java`, you can register a whole package with a marker type:
```kotlin
@RegisterType(root = "com.example.messages")
object TypeRegistryMarker
```
#### Inheriting from Other JSON Files
JSON resources can **extend** other resources using the `@extends` keyword:
```json
{
"@extends": "create-user.json",
"details": {
"lastName": "Johnson"
}
}
```
This will recursively merge the referenced file (`/org/example/create-user.json`) with the current one, allowing you
to override or augment deeply nested structures.
> 🧠 This is especially useful for composing test scenarios with shared defaults or inheritance-like setups.
---
### Adding or asserting metadata
Wrap your payload in a Message to add or assert metadata:
```java
@Test
void newAdminGetsAdditionalEmail() {
testFixture.whenCommand(new Message(new CreateUser(...),
Metadata.of("roles", Arrays.asList("Customer", "Admin"))))
.expectCommands(new SendWelcomeEmail(...),
new SendAdminEmail(...));
}
```
### Result and exception assertions
You can assert the result returned by a command or query:
[//]: # (@formatter:off)
```java
fixture.givenCommands(new CreateUser(userProfile))
.whenQuery(new GetUser(userId))
.expectResult(userProfile);
```
[//]: # (@formatter:on)
To assert an exception:
[//]: # (@formatter:off)
```java
fixture.givenCommands(new CreateUser(userProfile))
.whenCommand(new CreateUser(userProfile))
.expectExceptionalResult(IllegalCommandException.class);
```
[//]: # (@formatter:on)
### User-aware tests
You can also simulate a command from a specific user:
[//]: # (@formatter:off)
```java
var user = new MyUser("pete");
fixture.whenCommandByUser(user, "confirm-user.json")
.expectExceptionalResult(UnauthorizedException.class);
```
[//]: # (@formatter:on)
You can also pass a user ID string directly instead of a full User object. The test fixture will resolve it using the
configured `UserProvider` (by default loaded via Java's `ServiceLoader`):
[//]: # (@formatter:off)
```java
fixture
.givenCommands("create-user-pete.json")
.whenCommandByUser("pete", "confirm-user.json")
.expectExceptionalResult(UnauthorizedException.class);
```
[//]: # (@formatter:on)
In this example, the string "pete" is resolved to a `User` instance using the active `UserProvider`, and the test
asserts
that the confirmation is unauthorized.
### Verifying side effects
Use `expectThat()` or `expectTrue()` to assert custom logic or verify interactions (e.g., using Mockito):
[//]: # (@formatter:off)
```java
fixture.whenCommand("create-user-pete.json")
.expectThat(fc -> Mockito.verify(emailService).sendEmail(...));
```
[//]: # (@formatter:on)
### Triggering side effects manually
Use `whenExecuting()` to test code outside of message dispatching, like HTTP calls:
[//]: # (@formatter:off)
```java
fixture.whenExecuting(fc -> httpClient.put("/user", "/users/user-profile-pete.json"))
.expectEvents("create-user-pete.json");
```
[//]: # (@formatter:on)
### Asynchronous tests
By default, `TestFixture.create(...)` creates synchronous fixtures where handlers are executed locally in the same
thread
that publishes the message. This provides fast, deterministic behavior ideal for most unit tests.
However, in production your handlers are typically dispatched asynchronously by consumers running on separate threads.
To better simulate this behavior in tests — especially when testing event-driven flows or stateful consumers — you can
use an asynchronous fixture instead.
```java
TestFixture fixture = TestFixture.createAsync(new MyHandler(), MyStatefulHandler.class);
```
This ensures that:
- Handlers are tracked via the same consumer infrastructure used in production.
- Behavior involving asynchronous dispatch, retries, or stateful models is tested realistically.
- Eventual consistency is respected (e.g., expect...() calls will wait for outcomes to materialize).
> **Note:** Handlers annotated with `@LocalHandler` are executed synchronously, even in async fixtures, just as they
> would in production.
### Using Test Fixtures in Spring
Fluxzero provides seamless integration with Spring Boot for testing. You can inject a `TestFixture` directly:
```java
@SpringBootTest
class AsyncAppTest {
@Autowired
TestFixture fixture;
@Test
void testSomething() {
fixture.whenCommand("commands/my-command.json")
.expectEvents("events/expected-event.json");
}
}
```
- ✅ **Spring Boot**: No manual setup needed—`TestFixture` is auto-configured.
- ⚠️ **Spring Core (non-Boot)**: Manually import the test configuration:
```java
@Import(FluxzeroTestConfig.class)
```
This ensures the `TestFixture` and related infrastructure are available in the Spring context.
---
#### Switching to Synchronous Mode
By default, the injected fixture is **asynchronous**. To use a **synchronous** fixture instead:
##### Globally via `application.properties`:
```properties
fluxzero.test.sync=true
```
##### Or per test class:
```java
@TestPropertySource(properties = "fluxzero.test.sync=true")
@SpringBootTest
class SyncAppTest {
@Autowired
TestFixture fixture;
// test logic...
}
```
### Testing schedules
Fluxzero’s scheduling engine makes it easy to test time-based workflows. Scheduled messages are handled just
like any other message, except they are delayed until their due time.
Use the `TestFixture` to simulate the passage of time and trigger scheduled actions. Here’s a typical example:
```java
TestFixture testFixture = TestFixture.create(new UserCommandHandler(), new UserLifecycleHandler());
@Test
void accountIsTerminatedAfterClosing() {
testFixture
.givenCommands(new CreateUser(myUserProfile),
new CloseAccount(userId))
.whenTimeElapses(Duration.ofDays(30))
.expectEvents(new AccountTerminated(userId));
}
```
In this test:
- We schedule the AccountTerminated message as a result of the CloseAccount command.
- Then we simulate that 30 days have passed using whenTimeElapses(...).
- Finally, we verify that the expected AccountTerminated event was published.
You can also test cancellation logic:
```java
@Test
void accountReopeningCancelsTermination() {
testFixture
.givenCommands(new CreateUser(myUserProfile),
new CloseAccount(userId),
new ReopenAccount(userId))
.whenTimeElapses(Duration.ofDays(30))
.expectNoEventsLike(AccountTerminated.class);
}
```
If needed, you can advance time to an absolute timestamp using:
[//]: # (@formatter:off)
```java
fixture.whenTimeAdvancesTo(Instant.parse("2050-12-31T00:00:00Z"));
```
[//]: # (@formatter:on)
This is especially useful for workflows tied to specific deadlines or calendar dates.
---
## Handling Web Requests
Fluxzero supports first-class **WebRequest handling** via the `@HandleWeb` annotation and its HTTP-specific
variants such as `@HandleGet`, `@HandlePost`, `@HandleDelete`, etc.
Instead of exposing a public HTTP server per application, Fluxzero uses a central Web Gateway that **proxies all external
HTTP(S) and WebSocket traffic into the Runtime as `WebRequest` messages**. These messages are:
- **Logged** for traceability and auditing
- **Routed to client applications** using the same handler system as for commands, events, and queries
- **Handled by consumer applications** which return a `WebResponse`
#### Why This Design?
This architecture enables several key benefits:
- ✅ **Zero exposure**: client apps do not require a public-facing HTTP server and are thus *invisible* to attackers
- ✅ **Back-pressure support**: applications control load by polling their own messages
- ✅ **Audit-friendly**: every incoming request is automatically logged and correlated to its response
- ✅ **Multiple consumers possible**: multiple handlers can react to a WebRequest, though typically only one produces the
response (others use `passive = true`)
#### Example
```java
@HandleGet("/users")
public List listUsers() {
return userService.getAllUsers();
}
```
This will match incoming GET requests to `/users` and return a list of users. The response is published back as a
`WebResponse`.
You can use the general `@HandleWeb` if you want to match multiple paths or methods or define a custom HTTP method:
```java
@HandleWeb(value = "/users/{userId}", method = {"GET", "DELETE"})
public CompletableFuture> handleUserRequest(WebRequest request, @PathParam String userId) {
return switch (request.getMethod()) {
case "GET" -> Fluxzero.query(new GetUser(userId));
case "DELETE" -> Fluxzero.sendCommand(new DeleteUser(userId));
default -> throw new UnsupportedOperationException();
};
}
```
> 💡 **Tip:** To match any HTTP method without listing them explicitly, use `HttpRequestMethod.ANY`.
> This is equivalent to `"*"` and will match all incoming requests for the given path.
```java
@HandleWeb(value = "/users/{userId}", method = HttpRequestMethod.ANY)
public CompletableFuture> handleAllUserMethods(
WebRequest request, @PathParam String userId) {
// Handle any method (GET, POST, DELETE, etc.)
...
}
```
#### Suppressing a Response
If you want to listen to a WebRequest without generating a response (e.g. for auditing or monitoring), use
`passive = true`:
```java
@HandlePost(value = "/log", passive = true)
public void log(WebRequest request) {
logService.store(request);
}
```
#### Dynamic Path Parameters
Use the `@PathParam` annotation to extract dynamic segments from the URI path into handler method parameters:
```java
@HandleGet("/users/{id}")
public UserAccount getUser(@PathParam String id) {
return userService.get(id);
}
```
If the `value` is left empty, the framework will use the parameter name (`id` in this case).
#### Route Matching Specificity
Route patterns support literal segments, `{name}` path parameters, `{name:regex}` constrained parameters, and `*`
wildcards. A non-final `*` matches within a single path segment, so routes like `/meters/*/readings` are valid. A final
`*` matches the rest of the path and is mainly useful for static or SPA fallback routes.
Optional path fragments can be wrapped in square brackets, for example `/users[/{id}]` matches both `/users` and
`/users/42`.
Trailing slashes on non-root paths are ignored, so `/users` and `/users/` match the same route.
When multiple web handlers match the same request, Fluxzero selects the most specific route: literal segments win over
path parameters, constrained parameters win over plain parameters, and wildcard/catch-all routes are treated as
fallbacks. For example, `/api/projects/active` wins over `/api/projects/{id:[a-z]+}`, which wins over
`/api/projects/{id}`, which wins over `/api/projects/*`.
#### Automatic HEAD and OPTIONS
Fluxzero can derive common HTTP helper responses from your declared routes:
- A `HEAD` request can be routed to the matching `GET` handler when no explicit `@HandleHead`, `@HandleWeb(method =
"HEAD")`, or `ANY` route matches. The response keeps the GET status and headers but has no body.
- An `OPTIONS` request can return `204 No Content` with an `Allow` header when no explicit `@HandleOptions`,
`@HandleWeb(method = "OPTIONS")`, or `ANY` route matches.
Explicit handlers always win, including wildcard handlers registered in another handler class in the same application.
In multi-service setups, another application can still provide the explicit `HEAD` or `OPTIONS` handler. Disable the
derived helpers on the route that should not claim them:
```java
@HandleGet(value = "/meters/{id}/readings", autoHead = false, autoOptions = false)
public MeterReadings readings(@PathParam String id) {
return service.readings(id);
}
```
When requests enter through `fluxzero-proxy`, configured and allowed CORS preflight requests are answered by the proxy
before they reach the runtime. The automatic `OPTIONS` helper only applies to `OPTIONS` requests that are actually
forwarded as `WebRequest`s.
#### API Documentation and OpenAPI
Fluxzero can extract a format-neutral API documentation catalog from web handlers and render it as OpenAPI 3.0.1 JSON.
OpenAPI 3.1 can be enabled when needed.
Most route, parameter, request body, and response type information is inferred from existing `@Handle...`, `@Path`,
and parameter annotations:
```java
ApiDocCatalog catalog = ApiDocExtractor.extract(MeterEndpoint.class);
String openApiJson = OpenApiRenderer.renderPrettyJson(
catalog, new OpenApiOptions("Meters API", "1.0.0", "", List.of("https://api.example.com")));
```
When annotation processing is enabled, Fluxzero also generates `META-INF/fluxzero/openapi.json` during compilation if
the module contains web handlers opted in with `@ApiDoc`. Configure global document metadata with javac options such as
`-Afluxzero.openapi.title="Meters API"`, `-Afluxzero.openapi.version=1.0.0`, and
`-Afluxzero.openapi.servers=https://api.example.com`. The generated path can be changed with
`-Afluxzero.openapi.output=...`, the OpenAPI version with `-Afluxzero.openapi.specVersion=3.1.0`, and generation can
be disabled with `-Afluxzero.openapi.enabled=false`.
For dependency-free document metadata in source, place `@ApiDocInfo` on a package or handler type:
```java
@ApiDocInfo(
title = "Meters API",
version = "1.0.0",
description = "Public meter endpoints.",
contactName = "API support",
contactEmail = "support@example.com",
logoUrl = "https://example.com/logo.png",
logoAltText = "Example logo",
servers = @ApiDocServer(url = "https://api.example.com", description = "Production"),
security = "bearerAuth",
components = {
@ApiDocComponent(path = "responses.error", json = """
{"description":"Invalid request"}
"""),
@ApiDocComponent(path = "securitySchemes.bearerAuth", json = """
{"type":"http","scheme":"bearer"}
""")
})
package com.example.meters;
```
Set `serveOpenApi = true` on `@ApiDocInfo` to expose the generated document through Fluxzero web handling. The default
endpoint is `openapi.json` relative to the `@Path` value on the same package or handler type, so a package annotated
with `@Path("/v1")` serves `/v1/openapi.json`. Override this with `openApiPath`; absolute paths start at the
application root. The generated endpoint is registered internally and uses `@NoUserRequired`. For class-based handler
registration it can serve the compiled `META-INF/fluxzero/openapi.json` resource when available; for handler instances
it renders the document from the runtime handler scope.
Set `serveApiReference = true` to expose a small HTML API reference page for the same document. The default endpoint is
`docs` relative to the same `@Path`, so `@Path("/v1")` serves `/v1/docs` and automatically also serves
`/v1/openapi.json`. Redoc is the default renderer; use `apiReferenceRenderer = ApiReferenceRenderer.SCALAR` or
`SWAGGER_UI` to use Scalar or Swagger UI instead. The SDK does not bundle renderer frontend assets. It references
default CDN URLs, which can be replaced with `apiReferenceScriptUrl` and `apiReferenceStylesheetUrl` for self-hosted
assets.
Only web handlers opted in with `@ApiDoc` are included in generated API documentation. Place `@ApiDoc` on a package,
handler class, or handler method; empty `@ApiDoc` is enough when all metadata should be inferred. Use it for
human-authored metadata such as summaries, descriptions, operation ids, tags, operation security requirements, and
schema hints that cannot be inferred. It can also be placed on fields, parameters, record components, and type
arguments, for example
`List<@ApiDoc(description = "Connection item") Connection>` to document array items without OpenAPI-specific
annotations. Optional schema hints include `type`, `format`, `example`, `defaultValue`, `minimum`, `maximum`,
`allowableValues`, `required`, and `implementation`. Jakarta validation annotations such as `@NotNull`, `@Min`,
`@Size`, `@Pattern`, and `@Email` are reflected in endpoint parameter and model schemas when present. Use repeatable
`@ApiDocResponse` annotations for additional error/status responses, or to describe an inferred response without
repeating its body type; `@ApiDocResponse(status = 400, ref = "error")` references
`#/components/responses/error`. Array properties in response models are documented as required by default; input models
only use explicit `@ApiDoc(required = true)` or validation metadata for required properties. Use `@ApiDocExclude` to
exclude a package, class, method, parameter, field, record component, or type use from generated documentation without
disabling the runtime endpoint or model. Use `@ApiDocInfo.security` for top-level security requirements and
`@ApiDocComponent` through `@ApiDocInfo.components` for shared OpenAPI components such as reusable responses or
security schemes when automatic inference is not enough.
#### Other Parameter Annotations
In addition to `@PathParam`, you can extract other values from the request using:
- `@QueryParam` – extract query string values
- `@HeaderParam` – extract HTTP headers
- `@CookieParam` – extract cookie values
- `@FormParam` – extract URL-encoded form values or multipart form parts
For `multipart/form-data`, use `@FormParam String` for text fields, `@FormParam byte[]` or
`@FormParam InputStream` for file contents, or `@FormParam WebFormPart` when you also need the filename, content type,
or part headers.
Each of these annotations supports the same rules:
- If no name is given, the method parameter name is used
- Values are automatically converted to the target parameter type
#### Response Content Negotiation
When a web handler returns a regular value, Fluxzero uses the request's `Accept` header to choose a response
`Content-Type` where possible. Object payloads can be returned as `application/json`, `String` payloads can be returned
as `text/plain` or JSON strings, and `byte[]`/`InputStream` payloads can be returned as `application/octet-stream`.
Explicit `WebResponse` content types always win. If no supported representation matches the `Accept` header, Fluxzero
keeps the existing default instead of returning `406 Not Acceptable`.
#### URI Prefixing and Composition with `@Path`
The `@Path` annotation can be used at the **package**, **class**, **method**, or **property** level to construct URI
path segments. Paths are chained together from the outermost package to the innermost handler method. If a path segment
starts with `/`, it resets the chain from that point downward.
- Empty path values use the simple name of the package.
- If placed on a field or getter, the property value is used as the path segment (enabling dynamic routing).
```java
@Path
package my.example.api;
@Path("users")
public class UserHandler {
@Path("{id}")
@HandleGet
public User getUser(@PathParam String id) { ...}
}
```
This matches `/api/users/{id}`. If the class annotation had started with a `/`, i.e.: had been `@Path("/users")`, the
pattern would have become `/users/{id}`.
---
## Serving Static Files
Fluxzero supports serving static files directly from a handler class by using the `@ServeStatic` annotation.
This allows client applications to expose static resources (HTML, JS, CSS, images, etc.) without needing an external web
server.
```java
@ServeStatic(value = "/web", resourcePath = "/static")
public class WebAssets {
}
```
This will serve all files under `/static` (from the classpath or file system) under the URI path `/web`.
### Features
- Supports both **file system** and **classpath** resources
- Optional **fallback file** (e.g. for single-page apps)
- Clean URLs for statically exported routes
- Automatic `Cache-Control` headers
- Compression via Brotli and GZIP (if precompressed variants exist)
### Annotation Reference
```java
@ServeStatic(
value = "/assets",
resourcePath = "/public",
fallbackFile = "index.html",
cleanUrls = true,
immutableCandidateExtensions = {"js", "css", "svg"},
maxAgeSeconds = 86400
)
```
#### Parameters:
- `value`: Web path(s) where static content is served. Relative paths are prefixed by `@Path` values.
- `resourcePath`: The resource root (either on the file system or classpath).
- `fallbackFile`: A file to serve when the requested path doesn’t exist (e.g. `index.html`). Set to `""` to disable.
- `cleanUrls`: Whether extensionless paths first try `.html` and `/index.html` before the fallback. Set to
`false` to disable.
- `immutableCandidateExtensions`: Extensions that are eligible for aggressive caching if fingerprinted (e.g.
`main.123abc.js`).
- `maxAgeSeconds`: Default cache duration for non-immutable resources.
### Example: Serving a React App
```java
@ServeStatic(value = "/app", resourcePath = "/static", fallbackFile = "index.html")
public class WebFrontend { ...
}
```
This will serve files under `/app/**` and fallback to `index.html` for unknown paths—ideal for single-page apps.
Requests without a file extension also support statically exported clean URLs: `/app/onboarding` tries the exact path,
then `/app/onboarding.html`, then `/app/onboarding/index.html`, and only then the configured fallback.
> 📁 Files in `/static` on the classpath (e.g. under `resources/static/`) or `/static` on disk will be served.
> When both file system and classpath contain a file, the **file system takes precedence**.
> If the `resourcePath` starts with `classpath:`, **only classpath resources** are served.
> If it starts with `file:`, **only file system resources** are served.
This allows precise control over where content is loaded from and ensures classpath-only or file-system-only resolution
depending on the use case.
#### Combining Static and Dynamic Handlers
You can freely combine `@ServeStatic` with dynamic handler methods in the same class or package.
This is especially useful when your application serves a combination of:
- **Static assets** like HTML, JS, CSS
- **Dynamic endpoints** like REST APIs or view-rendered pages
```java
@Path("/app")
@ServeStatic("static") // serves static files from the /static resource directory for web paths /app/static/**
public class AppController {
@HandleGet("/status")
Status getStatus() {
return new Status("OK", Instant.now());
}
@HandlePost("/submit")
SubmissionResult submitForm(FormData data) {
return formService.handle(data);
}
}
```
This will:
- Serve `/app/static/index.html`, `/app/static/styles.css`, etc. from the `static/` resource directory
- Also respond to `/app/status` and `/app/submit` dynamically
The static file handling applies to all routes **not matched** by other methods in the class. This makes it ideal for
combining SPAs or hybrid web apps with API endpoints under a shared route prefix.
---
## Handling WebSocket Messages
Fluxzero provides first-class support for **WebSocket communication**, enabling stateful or stateless message
handling using the same annotation-based model as other requests.
WebSocket requests are published to the **WebRequest** log after being reverse-forwarded from the Fluxzero Runtime, and can
be consumed and responded to like any other request type.
---
### Two Styles of WebSocket Handling
#### 1. **Stateless Handlers** (Singleton Style)
Use annotations like `@HandleSocketOpen`, `@HandleSocketMessage`, and `@HandleSocketClose` directly on singleton handler
classes:
```java
@HandleSocketOpen("/chat")
public String onOpen() {
return "Welcome!";
}
@HandleSocketMessage("/chat")
public String onMessage(String incoming) {
return "Echo: " + incoming;
}
@HandleSocketClose("/chat")
public void onClose(SocketSession session) {
System.out.println("Socket closed: " + session.sessionId());
}
```
Responses can be returned directly from `@HandleSocketMessage` and `@HandleSocketOpen` methods. For more control, you
can inject the `SocketSession` parameter and send messages manually.
Other available annotations:
- `@HandleSocketPong` — handle pong responses
- `@HandleSocketHandshake` — override the default handshake logic
#### 2. **Stateful Sessions with `@SocketEndpoint`**
If you need to maintain per-session state, use `@SocketEndpoint`. Each WebSocket session will instantiate a fresh
handler object:
```java
@SocketEndpoint
@Path("/chat")
public class ChatSession {
private final List messages = new ArrayList<>();
@HandleSocketOpen
public String onOpen() {
return "Connected!";
}
@HandleSocketMessage
public void onMessage(String text, SocketSession session) {
messages.add(text);
session.sendMessage("Stored message: " + text);
}
@HandleSocketClose
public void onClose() {
System.out.println("Messages in this session: " + messages.size());
}
}
```
Stateful sessions are useful for flows involving authentication, message accumulation, or temporal context (e.g. cursor,
buffer, sequence).
> ✅ `@SocketEndpoint` handlers are prototype-scoped, meaning they're constructed once per session.
> ℹ️ Like other handlers, socket endpoints may be annotated with `@Consumer` for tracking isolation.
---
### Automatic Ping-Pong & Keep-Alive
When using `@SocketEndpoint`, Fluxzero automatically manages **keep-alive pings**:
- Pings are sent at regular intervals (default: every 60s)
- If a `pong` is not received within a timeout, the session is closed
- You can customize this behavior via the `aliveCheck` attribute on `@SocketEndpoint`
```java
@SocketEndpoint(aliveCheck = @SocketEndpoint.AliveCheck(pingDelay = 30, pingTimeout = 15))
public class MySession { ...
}
```
---
### Summary
| Annotation | Description |
|----------------------------|----------------------------------------------------|
| `@HandleSocketOpen` | Handles WebSocket connection open |
| `@HandleSocketMessage` | Handles incoming text or binary WebSocket messages |
| `@HandleSocketPong` | Handles pong responses (usually for health checks) |
| `@HandleSocketClose` | Handles WebSocket session closing |
| `@HandleSocketHandshake` | Allows customizing the handshake phase |
| `@SocketEndpoint` | Declares a per-session WebSocket handler class |
| `SocketSession` (injected) | Controls sending messages, pinging, and closing |
Fluxzero makes WebSocket communication secure, observable, and composable—integrated seamlessly into your
distributed, event-driven architecture.
---
## Testing Web Endpoint Behavior
Fluxzero allows you to simulate and verify HTTP interactions as part of your test flows. Web requests
can be tested like any other command, query, or event.
Here’s a complete test for a `POST /games` handler that accepts a JSON request and publishes a command:
```java
@Test
void registerGame() {
testFixture
.whenPost("/games", "/game/game-details.json") // Simulates POST with payload
.expectResult(GameId.class) // Asserts a result is returned
.expectEvents(RegisterGame.class); // Asserts a command was published
}
```
The corresponding handler is:
```java
@HandlePost("/games")
CompletableFuture addGame(GameDetails details) {
return Fluxzero.sendCommand(new RegisterGame(details));
}
```
As always, the `.json` file is automatically loaded from the classpath, allowing you to cleanly separate test data:
📄 `/game/game-details.json`
```json
{
"title": "Legend of the Skylands",
"description": "An epic singleplayer adventure with puzzles and secrets.",
"releaseDate": "2025-11-12T00:00:00Z",
"tags": [
"adventure",
"puzzle",
"singleplayer"
]
}
```
---
### Example: Querying with `GET`
You can test GET endpoints just as easily. This example first registers a game via `POST /games`, then fetches the list
of all games via `GET /games` and checks the result:
```java
@Test
void getGames() {
testFixture
.givenPost("/games", "/game/game-details.json") // Precondition: register a game
.whenGet("/games") // Perform GET request
.>expectResult(r -> r.size() == 1) // Assert one game is returned
.expectWebResponse(r -> r.getStatus() == 200); // Assert the status of the response
}
```
This corresponds to the following handler method:
```java
@HandleGet
@Path("/games")
CompletableFuture> getGames(@QueryParam String term) {
return Fluxzero.query(new FindGames(term));
}
```
---
### Testing Error Responses and Exceptions
Fluxzero also allows you to verify how your web endpoints handle exceptional scenarios. This includes asserting
the type of exception thrown as well as inspecting the resulting HTTP status code or response body.
Here’s a test that triggers a `403 Forbidden` error by throwing an `IllegalCommandException`:
```java
@Test
void postReturnsError() {
testFixture
.whenPost("/error", "body") // Simulate POST request
.expectExceptionalResult(IllegalCommandException.class) // Assert thrown exception
.expectWebResponse(r -> r.getStatus() == 403); // Assert HTTP 403 response
}
```
The corresponding handler might look like:
```java
@HandlePost("/error")
void postForError(String body) {
throw new IllegalCommandException("error: " + body);
}
```
This enables robust testing of both successful and failure paths for all your web request handlers.
---
### Path Parameter Substitution in Tests
When simulating web requests, Fluxzero automatically substitutes `{...}` placeholders in the request path using
results from previous steps:
- The result of the **first `when...()` step** is saved when `.andThen()` is called.
- If a subsequent path (e.g. `/games/{gameId}/buy`) contains placeholders, Fluxzero will:
- Attempt to replace each placeholder (like `{gameId}`) with the string value of a previously returned result.
- Track all resolved placeholders across steps. This allows chaining:
[//]: # (@formatter:off)
```java
testFixture.whenPost("/games", "/game/game-details.json") // returns gameId
.andThen()
.whenPost("/games/{gameId}/buy") // uses gameId, returns orderId
.andThen()
.whenPost("/games/{gameId}/refund/{orderId}"); // uses gameId from step 1, orderId from step 2
```
[//]: # (@formatter:on)
Substitutions are based purely on the order of results, not types or field names. The `.toString()` value of each result
is used to fill the next unresolved placeholder.
This keeps tests expressive and avoids boilerplate, especially for flows that involve chained resource creation (e.g.
`POST /games → POST /games/{gameId}/buy → ...`).
---
## Outbound Web Requests
Fluxzero provides a unified API for sending HTTP requests through the `WebRequestGateway`.
Unlike traditional HTTP clients, Flux logs outbound requests as `WebRequest` messages. These are then handled by:
- A **local handler** that tracks requests if the URL is **relative**, or
- A **connected remote client or proxy**, if the URL is **absolute**.
### Sending a Request
```java
WebRequest request = WebRequest.get("https://api.example.com/data")
.header("Authorization", "Bearer token123")
.build();
WebResponse response = Fluxzero.get()
.webRequestGateway().sendAndWait(request);
String body = response.getBodyString();
```
> ✅ All outbound traffic is logged and traceable in the Fluxzero Runtime.
### Asynchronous and Fire-and-Forget
You can send requests asynchronously:
[//]: # (@formatter:off)
```java
Fluxzero.get().webRequestGateway()
.send(request)
.thenAccept(response -> log
.info("Received: {}",response.getBodyString()));
```
[//]: # (@formatter:off)
Or fire-and-forget:
[//]: # (@formatter:off)
```java
Fluxzero.get().webRequestGateway()
.sendAndForget(Guarantee.STORED, request);
```
[//]: # (@formatter:on)
### Relative vs Absolute URLs
Flux supports both local and remote handling:
- **Absolute URLs** (e.g., `https://...`): The request is forwarded via the Flux **Web Proxy** and executed externally.
- **Relative URLs** (e.g., `/internal/doSomething`): The request is routed to a handler within another connected Flux
application.
This allows decoupled request-response workflows across services and environments.
### Isolating Traffic with `consumer`
You can specify a `consumer` in the request settings:
```java
WebRequestSettings settings = WebRequestSettings.builder()
.consumer("external-api-xyz")
.timeout(Duration.ofSeconds(5))
.build();
```
When set, the Flux Web Proxy will isolate this request in its own internal processing pipeline. This is useful when you:
- Want to isolate third-party integrations (e.g., API rate limits)
- Need different retry or error handling strategies per destination
- Want fault isolation between outgoing endpoints
### Mocking External Endpoints in Tests
Flux makes it easy to test full workflows—including outbound `WebRequest` calls—by **mocking** the response during
tests:
```java
static class EndpointMock {
@HandleGet("https://api.example.com/1.1/locations")
WebResponse handleLocations() {
return WebResponse.builder()
.header("X-Limit", "100")
.payload("/example-api/get-locations.json")
.build();
}
}
@Test
void testGetLocations() {
TestFixture.create(new EndpointMock())
.whenGet("https://api.example.com/1.1/locations")
.>expectResult(r -> r.size() == 2);
}
```
You can match requests by:
- Method and URL
- Headers, body, or any other property
> ✅ This gives you **full end-to-end test coverage**, even when integrating with external APIs.
---
### Summary
- ✅ Use `WebRequest` for centralized, traceable outbound HTTP calls.
- ✅ Automatically routes to a proxy or local handler depending on URL.
- ✅ Supports timeouts, consumers, and structured request settings.
- ✅ Easily mock remote endpoints for testing full business flows.
---
## Metrics Messages
Fluxzero supports a built-in message type for metrics: `MessageType.METRICS`.
These messages provide a powerful way to observe and trace system behavior across clients, handlers, and infrastructure.
Metrics messages are:
- Lightweight, structured, and traceable
- Logged like any other message
- Routable to handlers (via `@HandleMetrics`)
- Stored by default for **1 month** due to volume
---
### Publishing Metrics
You can publish metrics manually using the `Fluxzero.publishMetrics(...)` method:
```java
Fluxzero.publishMetrics(new SystemMetrics("slowProjection", "thresholdExceeded"));
```
This emits a structured metrics message to the metrics topic.
All metrics are wrapped in a regular `Message`, so you can include metadata or delivery guarantees:
[//]: # (@formatter:off)
```java
Fluxzero.get()
.metricsGateway()
.publish(new MyMetric("foo"),
Metadata.of("critical","true"),
Guarantee.STORED);
```
[//]: # (@formatter:off)
---
### Automatic Metrics from Clients
Many metrics are automatically emitted by the Flux Java client:
- **Connect / Disconnect events** when clients (re)connect
- **Tracking updates** (throughput, handler times, latency)
- **Search / state / document store operations**
- **Web request round-trip timings**
These are particularly helpful in troubleshooting or auditing system performance.
---
### Consuming Metrics
You can treat metrics like any other message type:
```java
@HandleMetrics
void on(MetricEvent event) {
log.debug("Observed metric: {}", event);
}
```
Use this to power custom dashboards, counters, diagnostics, or trigger alerts.
---
### Disabling Metrics
To reduce noise or overhead, you can selectively disable automatic metrics:
#### Disable per handler or batch using an interceptor
```java
@Consumer(handlerInterceptors = DisableMetrics.class)
public class SilentHandler {
@HandleEvent
void on(MyEvent event) { ... }
}
```
You can also use `batchInterceptors` to disable metrics for an entire consumer instance.
#### Disable globally per client
If you're instantiating a `WebSocketClient`, you can pass `disableMetrics = true` via the client config.
#### Use programmatic interceptors
If needed, you can suppress metric dispatch programmatically using:
[//]: # (@formatter:off)
```java
AdhocDispatchInterceptor.runWithAdhocInterceptor(() -> {
// your code here
}, (message, messageType, topic) -> null, MessageType.METRICS);
```
[//]: # (@formatter:on)
---
### Common Use Cases
- **Audit debugging**: trace which handler caused a slowdown
- **Observability**: track real-time stats like search throughput
- **Dashboards**: expose per-entity or per-consumer metrics
- **Trigger alerting**: when retries or handler delays exceed thresholds
Metrics messages provide lightweight hooks into system behavior — use them for visibility without overhead.
---
## Domain Modeling
Fluxzero allows you to model the state of your domain using entities that evolve over time by applying updates.
These entities — such as users, orders, etc. — maintain state and enforce invariants through controlled updates,
typically driven by commands.
### Defining the Aggregate Entity
To define a stateful domain object, annotate it with `@Aggregate`:
```java
@Aggregate
@Builder(toBuilder = true)
public record UserAccount(@EntityId UserId userId,
UserProfile profile,
boolean accountClosed) {
}
```
T