{"id":33179154,"url":"https://github.com/fluxzero-io/flux-capacitor-client","last_synced_at":"2026-04-03T13:56:49.612Z","repository":{"id":26327826,"uuid":"105290507","full_name":"fluxzero-io/flux-capacitor-client","owner":"fluxzero-io","description":"Client libraries to interface with Flux Capacitor","archived":false,"fork":false,"pushed_at":"2025-09-03T11:46:45.000Z","size":101741,"stargazers_count":25,"open_issues_count":5,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-03-13T11:40:43.890Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://flux-capacitor.io","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fluxzero-io.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2017-09-29T15:51:26.000Z","updated_at":"2025-10-24T17:44:24.000Z","dependencies_parsed_at":"2023-10-05T00:01:29.786Z","dependency_job_id":"709ec133-bf87-4e97-ac1b-5e87b97cc66c","html_url":"https://github.com/fluxzero-io/flux-capacitor-client","commit_stats":null,"previous_names":["fluxzero-io/flux-capacitor-client","flux-capacitor-io/flux-capacitor-client"],"tags_count":1274,"template":false,"template_full_name":null,"purl":"pkg:github/fluxzero-io/flux-capacitor-client","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxzero-io%2Fflux-capacitor-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxzero-io%2Fflux-capacitor-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxzero-io%2Fflux-capacitor-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxzero-io%2Fflux-capacitor-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fluxzero-io","download_url":"https://codeload.github.com/fluxzero-io/flux-capacitor-client/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluxzero-io%2Fflux-capacitor-client/sbom","scorecard":{"id":404917,"data":{"date":"2025-08-11","repo":{"name":"github.com/flux-capacitor-io/flux-capacitor-client","commit":"ea97c120acfad2f022f854240d27fda6e5a82429"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.5,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":10,"reason":"30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/deploy-any-version.yml:1","Warn: no topLevel permission defined: .github/workflows/deploy.yml:1","Warn: no topLevel permission defined: .github/workflows/publish-javadoc.yml:1","Warn: no topLevel permission defined: .github/workflows/pull-requests.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:31: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:64: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:71: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:78: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:90: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:97: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy-any-version.yml:106: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy-any-version.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:30: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:62: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:69: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:76: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:88: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:95: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:104: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/deploy.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish-javadoc.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/publish-javadoc.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish-javadoc.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/publish-javadoc.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/publish-javadoc.yml:42: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/publish-javadoc.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pull-requests.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/pull-requests.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pull-requests.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/flux-capacitor-io/flux-capacitor-client/pull-requests.yml/master?enable=pin","Warn: containerImage not pinned by hash: proxy/Dockerfile:1: pin your Docker image by updating azul/zulu-openjdk-alpine:21-jre-headless to azul/zulu-openjdk-alpine:21-jre-headless@sha256:0fd3270364313f763f270d0b7474dd770a930012de3dd9d43c2a038838826b32","Warn: containerImage not pinned by hash: test-server/Dockerfile:1: pin your Docker image by updating azul/zulu-openjdk-alpine:21-jre-headless to azul/zulu-openjdk-alpine:21-jre-headless@sha256:0fd3270364313f763f270d0b7474dd770a930012de3dd9d43c2a038838826b32","Info:   0 out of   8 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of  15 third-party GitHubAction dependencies pinned","Info:   0 out of   2 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/deploy.yml:13"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Vulnerabilities","score":8,"reason":"2 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-c76h-2ccp-4975","Warn: Project is vulnerable to: GHSA-cxrh-j4jr-qwg3"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-18T20:56:59.141Z","repository_id":26327826,"created_at":"2025-08-18T20:56:59.141Z","updated_at":"2025-08-18T20:56:59.141Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31355361,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-03T08:03:20.796Z","status":"ssl_error","status_checked_at":"2026-04-03T08:00:37.834Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-11-16T03:00:36.825Z","updated_at":"2026-04-03T13:56:49.602Z","avatar_url":"https://github.com/fluxzero-io.png","language":"Java","funding_links":[],"categories":["进程间通信"],"sub_categories":[],"readme":"\u003ca href=\"https://flux-capacitor.io\"\u003e\n    \u003cimg src=\"https://flux-capacitor.io/assets/brand/flux-capacitor-white.svg\" alt=\"Flux Capacitor logo\" title=\"Flux Capacitor\" align=\"right\" height=\"60\" /\u003e\n\u003c/a\u003e\n\n# Flux Capacitor java client\n\n[![Build](https://github.com/flux-capacitor-io/flux-capacitor-client/actions/workflows/deploy.yml/badge.svg)](https://github.com/flux-capacitor-io/flux-capacitor-client/actions)\n[![Maven Central](https://img.shields.io/maven-central/v/io.flux-capacitor/flux-capacitor-client)](https://central.sonatype.com/artifact/io.flux-capacitor/flux-capacitor-client?smo=true)\n[![Javadoc](https://img.shields.io/badge/javadoc-main-blue)](https://flux-capacitor.io/flux-capacitor-client/javadoc/apidocs/)\n[![Cheatsheet](https://img.shields.io/badge/cheatsheet-PDF-red.svg)](https://raw.githubusercontent.com/flux-capacitor-io/flux-capacitor-client/refs/heads/master/docs/cheatsheet.pdf)\n[![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](https://www.apache.org/licenses/LICENSE-2.0)\n\nThis repository contains the official Java client for [flux.host](https://flux.host), the Flux Capacitor platform. For a\nshort overview of functionalities, check out this [cheatsheet](docs/cheatsheet.pdf).\n\n---\n\n## Installation\n\n### Maven Users\n\nImport the [Flux Capacitor BOM](https://mvnrepository.com/artifact/io.flux-capacitor/flux-capacitor-bom) in your\n`dependencyManagement` section to centralize version management:\n\n```xml\n\n\u003cdependencyManagement\u003e\n    \u003cdependencies\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eio.flux-capacitor\u003c/groupId\u003e\n            \u003cartifactId\u003eflux-capacitor-bom\u003c/artifactId\u003e\n            \u003cversion\u003e${flux-capacitor.version}\u003c/version\u003e \u003c!-- See version badge above --\u003e\n            \u003ctype\u003epom\u003c/type\u003e\n            \u003cscope\u003eimport\u003c/scope\u003e\n        \u003c/dependency\u003e\n    \u003c/dependencies\u003e\n\u003c/dependencyManagement\u003e\n```\n\nThen declare only the dependencies you actually need (no version required):\n\n```xml\n\n\u003cdependencies\u003e\n    \u003cdependency\u003e\n        \u003cgroupId\u003eio.flux-capacitor\u003c/groupId\u003e\n        \u003cartifactId\u003ejava-client\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n        \u003cgroupId\u003eio.flux-capacitor\u003c/groupId\u003e\n        \u003cartifactId\u003ejava-client\u003c/artifactId\u003e\n        \u003cclassifier\u003etests\u003c/classifier\u003e\n        \u003cscope\u003etest\u003c/scope\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n        \u003cgroupId\u003eio.flux-capacitor\u003c/groupId\u003e\n        \u003cartifactId\u003etest-server\u003c/artifactId\u003e\n        \u003cscope\u003etest\u003c/scope\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n        \u003cgroupId\u003eio.flux-capacitor\u003c/groupId\u003e\n        \u003cartifactId\u003eproxy\u003c/artifactId\u003e\n        \u003cscope\u003etest\u003c/scope\u003e\n    \u003c/dependency\u003e\n\u003c/dependencies\u003e\n```\n\n\u003e 💡 **Note:** The `test-server` and `proxy` modules are **optional**, and can be included if you want to test your\n\u003e application locally against an **in-memory Flux server**.\n\n---\n\n### Gradle Users\n\nUse [platform BOM support](https://docs.gradle.org/current/userguide/platforms.html) to align dependency versions\nautomatically:\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eKotlin DSL (build.gradle.kts)\u003c/strong\u003e\u003c/summary\u003e\n\n```kotlin\ndependencies {\n    implementation(platform(\"io.flux-capacitor:flux-capacitor-bom:${fluxCapacitorVersion}\"))\n    implementation(\"io.flux-capacitor:java-client\")\n    testImplementation(\"io.flux-capacitor:java-client\", classifier = \"tests\")\n    testImplementation(\"io.flux-capacitor:test-server\")\n    testImplementation(\"io.flux-capacitor:proxy\")\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eGroovy DSL (build.gradle)\u003c/strong\u003e\u003c/summary\u003e\n\n```groovy\ndependencies {\n    implementation platform(\"io.flux-capacitor:flux-capacitor-bom:${fluxCapacitorVersion}\")\n    implementation 'io.flux-capacitor:java-client'\n    testImplementation('io.flux-capacitor:java-client') {\n        classifier = 'tests'\n    }\n    testImplementation 'io.flux-capacitor:test-server'\n    testImplementation 'io.flux-capacitor:proxy'\n}\n```\n\n\u003c/details\u003e\n\n---\n\n\u003e 🔖 **Tip:** You only need to update the version number **once** in the BOM reference. All included modules will\n\u003e automatically align\n\u003e with it.\n\n---\n\n## Basic example\n\nCreate a new project and add an event class:\n\n```java\npublic record HelloWorld() {\n}\n```\n\nCreate a handler for the event:\n\n```java\npublic class HelloWorldEventHandler {\n    @HandleEvent\n    void handle(HelloWorld event) {\n        System.out.println(\"Hello World!\");\n    }\n}\n```\n\nPublish the event:\n\n```java\npublic class ExampleMain {\n    public static void main(final String[] args) {\n        var fluxCapacitor = DefaultFluxCapacitor.builder()\n                .build(LocalClient.newInstance());\n        fluxCapacitor.registerHandlers(new HelloWorldEventHandler());\n        fluxCapacitor.eventGateway().publish(new HelloWorld());\n    }\n}\n```\n\nOutput:\n\n```\nHello World!\n```\n\n### With Spring\n\nFlux Capacitor integrates seamlessly with Spring. Here’s how the above example looks with Spring Boot:\n\n```java\n\n@SpringBootApplication\npublic class ExampleMain {\n    public static void main(String... args) {\n        SpringApplication.run(ExampleMain.class, args);\n        FluxCapacitor.publishEvent(new HelloWorld());\n    }\n}\n```\n\nAnd annotate your handler with `@Component`:\n\n```java\n\n@Component\npublic class HelloWorldEventHandler {\n    @HandleEvent\n    void handle(HelloWorld event) {\n        System.out.println(\"Hello World!\");\n    }\n}\n```\n\n\u003e ⚠️ Using Spring non-Boot? Add `@Import(FluxCapacitorSpringConfig.class)` to register FluxCapacitor and related beans.\n\n### Testing your handler\n\nFlux Capacitor includes a powerful TestFixture utility for testing your handlers without needing a full application\nor infrastructure setup.\n\nHere’s how to test our HelloWorldEventHandler from earlier:\n\n```java\nclass HelloWorldEventHandlerTest {\n\n    @Test\n    void testHelloWorldHandler() {\n        TestFixture.create(new HelloWorldEventHandler())\n                .whenEvent(new HelloWorld())\n                .expectThat(fc -\u003e System.out.println(\n                        \"Event handled successfully!\"));\n    }\n}\n```\n\nThis will invoke your handler exactly like Flux Capacitor would in production, but entirely in memory and synchronously\nby default.\n\n\u003e ✅ We’ll explore more powerful testing patterns — including assertions on results, published commands, exceptions,\n\u003e and full event flows — later in this guide.\n\n---\n\n# Features\n\nThe java client supports all features of Flux Capacitor but also offers plenty of additional functionality.\nWhat follows is a summary of the most important features.\n\n## Table of Contents\n\n### Messaging\n\n- [Message Handling](#message-handling)\n- [Tracking Messages](#tracking-messages)\n- [Message Replays](#message-replays)\n- [Dynamic Dead Letter Queue (DLQ)](#dynamic-dead-letter-queue-dlq)\n- [Local Handlers](#local-handlers)\n- [Payload Validation](#payload-validation)\n- [User and Role-Based Access Control](#user-and-role-based-access-control)\n- [Scheduling](#scheduling)\n- [User-defined message logs](#user-defined-message-logs)\n- [Dispatching Messages](#dispatching-messages)\n- [Testing your Handlers](#testing-your-handlers)\n- [Handling Web Requests](#handling-web-requests)\n- [Serving Static Files](#serving-static-files)\n- [Handling WebSocket Messages](#handling-websocket-messages)\n- [Testing Web Endpoint Behavior](#testing-web-endpoint-behavior)\n- [Outbound Web Requests](#outbound-web-requests)\n- [Metrics Messages](#metrics-messages)\n\n### Domain Modeling, Persistence, and Search\n\n- [Domain Modeling](#domain-modeling)\n- [Applying Updates to Entities](#applying-updates-to-entities)\n- [Nested Entities](#nested-entities)\n- [Model Persistence](#model-persistence)\n- [Stateful Handlers](#stateful-handlers)\n- [Document Indexing and Search](#document-indexing-and-search)\n- [Tracking and Updating Documents](#tracking-and-updating-documents)\n\n### Data Handling and Serialization\n\n- [Protecting Sensitive Data](#protecting-sensitive-data)\n- [Serialization, Upcasting, and Downcasting](#serialization-upcasting-and-downcasting)\n- [Filtering Object Content](#filtering-object-content)\n- [Kotlin Support](#kotlin-support)\n\n### Configuration and Extensibility\n\n- [Configuring Application Properties](#configuring-application-properties)\n- [Parameter Injection with Custom Resolvers](#parameter-injection-with-custom-resolvers)\n- [Interceptors: Dispatching, Handling, and Batching](#interceptors-dispatching-handling-and-batching)\n- [Configuring Flux Capacitor](#configuring-flux-capacitor)\n- [WebSocketClient: Connect to the Flux Platform](#websocketclient-connect-to-the-flux-platform)\n- [Compatibility and Dependencies](#compatibility-and-dependencies)\n\n---\n\n## Message Handling\n\nFlux Capacitor is centered around sending and receiving messages — such as __commands__, __events__, __queries__, and\n__web requests__. These messages can originate from your own application or any other client connected to the same Flux\nPlatform.\n\nHandlers are simply methods annotated with `@HandleCommand`, `@HandleEvent`, `@HandleQuery`, etc. Here’s a basic example\nof an event handler that dispatches a command to send a welcome email when a user is created:\n\n```java\nclass UserEventHandler {\n    @HandleEvent\n    void handle(CreateUser event) {\n        FluxCapacitor.sendCommand(\n                new SendWelcomeEmail(event.getUserProfile()));\n    }\n}\n```\n\nThis handler uses the static `sendCommand` method on FluxCapacitor, which works because the client is automatically\ninjected into the thread-local context before message handling begins. This approach eliminates the need to inject\n`FluxCapacitor` into every handler.\n\nTo receive that command, you would define a corresponding command handler:\n\n```java\nclass EmailCommandHandler {\n    @HandleCommand\n    void handle(SendWelcomeEmail command) {\n        //send welcome email to user\n    }\n}\n```\n\nHandlers can return a result (e.g. from queries or commands), which will automatically be published as a __Result__\nmessage and sent back to the originating client:\n\n```java\nclass UserQueryHandler {\n    @HandleQuery\n    UserProfile handle(GetUserProfile query) {\n        //return the user profile\n    }\n}\n```\n\nTo perform a query and wait for its result synchronously, you can use:\n\n```java\nclass UserEventHandler {\n    @HandleEvent\n    void handle(ResetPassword event) {\n        UserProfile userProfile = FluxCapacitor\n                .queryAndWait(new GetUserProfile(event.getUserId()));\n        // Perform reset using userProfile\n    }\n}\n```\n\n### Returning Futures\n\nHandler methods may also return a `CompletableFuture\u003cT\u003e` instead of a direct value. In that case, Flux Capacitor will\npublish the result to the result log once the future completes:\n\n```java\n\n@HandleQuery\nCompletableFuture\u003cUserProfile\u003e handle(GetUserProfile query) {\n    return userService.fetchAsync(query.getUserId());\n}\n```\n\nThis can be useful when calling asynchronous services (e.g. via HTTP or database drivers).\n\n\u003e ⚠️ **Caution:** While supported, returning a future means Flux will consider the message *handled* as soon as the\n\u003e handler returns the future—not when the future completes. This can be problematic if:\n\u003e\n\u003e - The operation must be guaranteed to complete (e.g. business-critical updates),\n\u003e - You rely on message acknowledgment for progress tracking,\n\u003e - Or you need back-pressure to avoid overloading the system.\n\u003e\n\u003e For most handlers, **synchronous return types are recommended**, or use `.join()` to explicitly block when necessary.\n\nThis pattern works best for non-critical side effects or purely read-oriented queries that can tolerate eventual\ncompletion.\n\n### Handler Matching and Passive Handlers\n\nFlux Capacitor dynamically resolves which handler method(s) should respond to a message based on **handler specificity**\nand **message type**. This resolution behavior is consistent across all message types — including **commands**,\n**queries**, **events**, **errors**, **metrics**, and more.\n\n#### Most Specific Handler Wins (Per Class)\n\nIf multiple handler methods in the *same class* can handle a message (e.g. a `CreateUser` event), **only the most\nspecific method** is invoked. This allows you to define fallback methods at a more general level if no exact match is\nfound.\n\n```java\n\n@HandleEvent\nvoid handle(Object event) {\n    log.info(\"Generic fallback\");\n}\n\n@HandleEvent\nvoid handle(CreateUser event) {\n    log.info(\"Handling specific CreateUser event\");\n}\n```\n\n➡️ In this example, only the `handle(CreateUser)` method runs when a `CreateUser` event is dispatched.\n\n#### Multiple Handler Classes Are Invoked\n\nWhen the same message is handled by **different classes**, all eligible handlers are invoked independently.\n\n```java\npublic class BusinessHandler {\n    @HandleEvent\n    void handle(CreateUser event) {\n        // perform business logic\n    }\n}\n\npublic class LoggingHandler {\n    @HandleEvent\n    void logEvent(Object event) {\n        log.info(\"Observed event {}\", event);\n    }\n}\n```\n\n➡️ Here, both handlers are invoked when a `CreateUser` event is dispatched.\n\n#### Requests Prefer a Single Active Handler\n\nFor **request-like messages** — such as commands, queries and web requests — only **one handler** class is\nexpected to produce a response. If multiple are eligible, only the ones marked as **non-passive** will be considered for\nproducing the result.\n\nAdditional handlers may still be registered using `passive = true`, e.g., for logging, auditing, metrics, or outbox\npatterns.\n\n```java\npublic class UserHandler {\n    @HandleQuery\n    UserAccount handle(GetUser query) {\n        return userRepository.find(query.getUserId());\n    }\n}\n\npublic class QueryMetricsHandler {\n    @HandleQuery(passive = true)\n    void record(Object query) {\n        metrics.increment(\"queries.\" + query.getClass().getSimpleName());\n    }\n}\n```\n\n➡️ In this case:\n\n- The `handle(...)` method produces the result.\n- The `record(...)` method logs the query,\n\nBy combining **handler specificity**, **class-level isolation**, and the `passive` flag, Flux Capacitor gives you\nprecise control over how messages are processed — even across mixed concerns like logging, read models, business logic,\nand cross-cutting concerns.\n\n### Metadata Support\n\nMessages can include **metadata**, which are contextual key-value pairs (typically of a technical nature). These are\nuseful for passing user context, correlation IDs, request info, etc.\n\nFor example, sending a command with metadata:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.sendCommand(\n    new CreateUser(...),\n    Metadata.of(\"userAgent\", userAgent)\n);\n```\n[//]: # (@formatter:on)\n\nReading metadata in a handler is just as easy:\n\n```java\nclass UserCommandHandler {\n    @HandleCommand\n    void handle(CreateUser command, Metadata metadata) {\n        String userAgent = metadata.get(\"userAgent\");\n        ...\n    }\n}\n```\n\n## Tracking Messages\n\nFlux Capacitor handles message dispatch asynchronously by default. When a message such as a command is published:\n\n1. It is sent to the Flux Platform.\n2. The platform logs the message and notifies all subscribed consumers.\n3. Consumers stream these messages to the relevant handler methods.\n\n### Default Consumer Behavior\n\nBy default, handlers join the **default consumer** for a given message type. For example:\n\n```java\nclass MyHandler {\n    @HandleCommand\n    void handle(SomeCommand command) {\n        ...\n    }\n}\n```\n\nThis handler joins the default **command consumer** automatically.\n\n### Custom Consumers with @Consumer\n\nYou can override the default behavior using the @Consumer annotation:\n\n```java\n\n@Consumer(name = \"MyConsumer\")\nclass MyHandler {\n    @HandleCommand\n    void handle(SomeCommand command) {\n        ...\n    }\n}\n```\n\nTo apply this to an entire package (and its subpackages), add a package-info.java file:\n\n```java\n@Consumer(name = \"MyConsumer\")\npackage com.example.handlers;\n```\n\n### Customizing Consumer Configuration\n\nYou can tune the behavior using additional attributes on the @Consumer annotation:\n\n```java\n\n@Consumer(name = \"MyConsumer\", threads = 2, maxFetchSize = 100)\nclass MyHandler {\n    @HandleCommand\n    void handle(SomeCommand command) {\n        ...\n    }\n}\n```\n\n- threads = 2: Two threads per application instance will fetch commands.\n- maxFetchSize = 100: Up to 100 messages fetched per request, helping apply backpressure.\n\nEach thread runs a **tracker**. If you deploy the app multiple times, Flux automatically load-balances messages across\nall available trackers.\n\n### Default Consumer Settings\n\n| Setting      | Default Value |\n|--------------|---------------|\n| threads      | 1             |\n| maxFetchSize | 1024          |\n\nThese defaults are sufficient for most scenarios. You can always override them for improved performance or control.\n\n---\n\n## Message Replays\n\nFlux Capacitor allows you to **replay past messages** by tracking from an earlier index in the message log.  \nThis is useful for:\n\n- Rebuilding projections or read models\n- Repairing missed or failed messages\n- Retrospectively introducing new functionality\n\nSince message logs — including **commands**, **events**, **errors**, etc. — are durably stored (with **events** retained\nindefinitely by default), replays are usually available.\n\n#### Replaying from the Past\n\nThe most common way to initiate a replay is to define a **new consumer** using the `@Consumer` annotation with a\n`minIndex`.\n\n```java\n\n@Consumer(name = \"auditReplay\", minIndex = 111677748019200000L)\npublic class AuditReplayHandler {\n\n    @HandleEvent\n    void on(CreateUser event) {\n        ...\n    }\n}\n```\n\nIn this example, the consumer `auditReplay` will process all events starting from **January 1st, 2024**.\n\n\u003e ℹ️ The `minIndex` value specifies the *inclusive* starting point for the message log.  \n\u003e Index values are based on time and can be derived using `IndexUtils`:\n\n```java\nlong index = IndexUtils.indexFromTimestamp(\n        Instant.parse(\"2024-01-01T00:00:00Z\"));\n// returns: 111677748019200000L\n```\n\nThis approach is perfect for:\n\n- Starting **fresh consumers** for replays\n- Bootstrapping projections without interfering with live handlers\n- Keeping logic encapsulated and isolated\n\n#### Resetting Existing Consumers\n\nIf you want to reset an existing consumer to an earlier point in the log:\n\n[//]: # (@formatter:off)\n```java\nlong replayIndex = 111677748019200000L;\nFluxCapacitor.client()\n    .getTrackingClient(MessageType.EVENT)\n    .resetPosition(\"myConsumer\", replayIndex, Guarantee.STORED);\n```\n[//]: # (@formatter:on)\n\nThis restarts the consumer from the given index and causes old messages to be redelivered and reprocessed.\n\n\u003e ⚠️ Replaying over existing handlers may require extra caution (e.g. to avoid duplicating side effects).\n\n#### 🔁 Parallel Replays with `exclusive = false`\n\nSometimes you want to **re-use the same handler class** for both:\n\n- **Live processing** (e.g. default consumer)\n- **Replaying past messages** (e.g. rebuilding projections)\n\nTo achieve this, annotate the handler with `exclusive = false`. This tells Flux that the handler may participate in\n**multiple consumers simultaneously**:\n\n```java\n\n@Consumer(name = \"live\", exclusive = false)\npublic class OrderProcessor {\n    @HandleCommand\n    void handle(SendOrder command) {\n        //submit an order\n    }\n}\n```\n\nNow, register a **second consumer** at runtime for the **same handler** using a different tracking position:\n\n[//]: # (@formatter:off)\n```java\nfluxCapacitorBuilder.addConsumerConfiguration(\n    ConsumerConfiguration.builder()\n        .name(\"replay\") // A new consumer\n        .handlerFilter(handler -\u003e handler instanceof OrderProcessor) // same class\n        .minIndex(111677748019200000L) // Start of replay window\n        .maxIndex(111853279641600000L) // End of replay window\n        .build(),\n    MessageType.COMMAND\n);\n```\n[//]: # (@formatter:on)\n\nThis will spin up a **parallel tracker** that replays all commands since **2024-01-01T00:00:00Z**\nuntil **2024-02-01T00:00:00Z**, while the original `live` consumer continues uninterrupted. In this example, orders\nsent during the month of January 2024 will be resubmitted.\n\n\u003e ✅ This is ideal for **bootstrapping read models** or **running repair jobs** without affecting production flow\n\u003e or duplicating a handler class.\n\n---\n\n### Handling Failures by Replaying from the Error Log\n\nFlux Capacitor automatically logs **all message handling errors** to a dedicated **error log**. This includes failures\nfor:\n\n- Commands\n- Queries\n- WebRequests\n- Schedule messages\n- Events (if a handler throws)\n- And more\n\nEach error message includes detailed **stack traces**, **metadata**, and most importantly, a **reference to the original\nmessage** that caused the failure.\n\n### Error Handling with `@HandleError`\n\nTo react to failures programmatically, define a handler method with the `@HandleError` annotation:\n\n```java\n\n@HandleError\nvoid onError(Throwable error) {\n    log.warn(\"Something went wrong!\", error);\n}\n```\n\nYou can inject the **failed message** using the `@Trigger` annotation:\n\n```java\n\n@HandleError\nvoid onError(Throwable error, @Trigger SomeCommand failedCommand) {\n    log.warn(\"Failed to process {}: {}\", failedCommand, error.getMessage());\n}\n```\n\n\u003e ℹ️ The trigger can be the payload, or a full `Message` or `DeserializingMessage`.\n\nYou can also **filter** error handlers by trigger type, message type, or originating consumer:\n\n```java\n\n@HandleError\n@Trigger(messageType = MessageType.COMMAND, consumer = \"my-app\")\nvoid retryFailedCommand(MyCommand failed) {\n    FluxCapacitor.sendCommand(failed);\n}\n```\n\n---\n\n## Dynamic Dead Letter Queue (DLQ)\n\nThe **error log is durable** and **replayable**, which means you can treat it as a **powerful, dynamic DLQ**.\n\nHere’s how:\n\n1. **Deploy a special consumer** that tracks the error log.\n2. Use `@Trigger` to access and inspect failed messages.\n3. Filter and replay failures based on time, payload type, or originating app.\n\n### Example: Retrying Failed Commands from the Past\n\nLet’s assume a bug caused command processing to fail in January 2024. The following setup reprocesses those failed\ncommands:\n\n```java\n\n@Consumer(name = \"command-dlq\",\n        minIndex = 111677748019200000L, maxIndexExclusive = 111853279641600000L) // 2024-01-01 to 2024-02-01 \nclass CommandReplayHandler {\n\n    @HandleError\n    @Trigger(messageType = MessageType.COMMAND)\n    void retry(MyCommand failed) {\n        FluxCapacitor.sendCommand(failed);\n    }\n}\n```\n\n\u003e ✅ The original `MyCommand` payload is restored and retried transparently.\n\n\u003e 🧠 You can combine this with logic that deduplicates, transforms, or **selectively suppresses** retries.\n\n### When to Use the Error Log\n\n| Use Case                       | How the Error Log Helps              |\n|--------------------------------|--------------------------------------|\n| 🛠 Fix a bug retroactively     | Replay failed commands from the past |\n| 🚧 Validate new handler logic  | Test it against real-world errors    |\n| 🔁 Retry transient failures    | Re-issue requests with retry logic   |\n| 🧹 Clean up or suppress errors | Filter out known false-positives     |\n\nThe error log acts as a **time-travel debugger** — it gives you full control over how and when to address failures, now\nor in the future.\n\n---\n\n### Routing with `@RoutingKey`\n\nIn Flux Capacitor, routing is used to assign messages to **segments** using consistent hashing. This ensures that\nmessages about the same entity — for example, all events for a given `OrderId` — are always handled by the **same\nconsumer**, in **the correct order**.\n\nThis is critical when you're handling messages **in parallel**, but still want to ensure **per-entity consistency**.\n\n#### Declaring the Routing Key\n\nBy default, the routing key is derived from the message ID. But you can override this by annotating a field, getter, or\nmethod in your **payload class** with `@RoutingKey`.\n\n```java\npublic record ShipOrder(@RoutingKey OrderId orderId) {\n}\n```\n\nOr explicitly reference a nested property:\n\n```java\n\n@RoutingKey(\"customer/id\")\npublic record OrderPlaced(Customer customer) {\n}\n```\n\nThis instructs Flux to extract `customer.id` and use it as the routing key when publishing or consuming the message.\n\n#### Handler-Level Routing Keys\n\nIn more advanced cases, you may want to **override routing at the handler level**, regardless of how the message was\npublished. You can place `@RoutingKey(...)` on the handler method itself:\n\n```java\n\n@HandleEvent\n@RoutingKey(\"organisationId\")\nvoid handle(OrganisationUpdate event) {\n    // Will route based on organisationId in metadata or payload\n}\n```\n\n\u003e ⚠️ When doing this, be sure to declare your consumer with `ignoreSegment = true`. Otherwise, this routing override\n\u003e may cause certain messages to be silently skipped.\n\n```java\n\n@Consumer(ignoreSegment = true)\npublic class OrganisationHandler {\n    ...\n}\n```\n\n#### Metadata-Based Routing\n\nRouting keys can also be extracted from **message metadata**. For example:\n\n```java\n\n@RoutingKey(\"userId\")\npublic class AuditLogEntry { ...\n}\n```\n\nThis will first try to extract `userId` from metadata, and fall back to the payload if not present.\n\n#### Summary\n\n| Placement      | Meaning                                                               |\n|----------------|-----------------------------------------------------------------------|\n| Field/getter   | Use the property's value as routing key                               |\n| Class-level    | Use the named property in metadata or payload                         |\n| Handler method | Overrides routing key used during handling (requires `ignoreSegment`) |\n\n---\n\n## Local handlers\n\nFlux Capacitor supports both asynchronous and local (synchronous) message handling. **Local handlers** process messages\nin the same thread that published them, bypassing the message dispatch infrastructure entirely. This typically results\nin faster response times and is ideal for simple or time-sensitive use cases.\n\nTo define a local handler, annotate the handler method, class, or its enclosing package with `@LocalHandler`:\n\n```java\n\n@LocalHandler(logMetrics = true)\npublic class SomeLocalHandler {\n    @HandleEvent\n    void handle(ApplicationStarted event) {\n        //do something\n    }\n}\n```\n\n\u003e 💡 Use logMetrics = true to track performance metrics even for local handlers.\n\n### Self-handling messages\n\nInstead of defining message handlers externally, you can embed handler logic directly in the message payload. This is\noften useful for queries or simple commands.\n\n```java\npublic class GetUserProfile {\n    String userId;\n\n    @HandleQuery\n    UserProfile handle() {\n        //fetch the user profile and return\n    }\n}\n```\n\nBy default, such handlers are treated as local. To process them asynchronously (i.e., as part of a consumer),\nannotate the class with `@TrackSelf`:\n\n```java\n\n@TrackSelf\n@Consumer(name = \"user-management\")\npublic class GetUserProfile {\n    String userId;\n\n    @HandleQuery\n    UserProfile handle() {\n        // Async handler\n    }\n}\n```\n\nWhen component-scanned (e.g., via Spring), `@TrackSelf` classes will be automatically discovered and registered.\nThis works even if the annotation is placed on an interface rather than the concrete class—allowing for reusable handler\npatterns.\n\nFor example, a generic command handler interface can be tracked and reused:\n\n```java\n\n@TrackSelf\npublic interface UserUpdate {\n    @HandleCommand\n    default void handle() {\n        //default behavior\n    }\n}\n```\n\nImplementations of this interface will then be handled asynchronously, using the configured consumer (or the default\none if unspecified).\n\n### Response typing with Request\u003cR\u003e\n\nFlux Capacitor allows you to formalize the expected return type of commands and queries by implementing the `Request\u003cR\u003e`\ninterface. This lets the framework:\n\n- Infer the response type at runtime and during testing.\n- Validate handler methods at compile-time (e.g., `@HandleQuery` must return an R).\n- Simplify `queryAndWait(...)` or `sendCommand(...)` invocations.\n\nHere’s an example for a query:\n\n```java\n\n@Value\npublic class GetUserProfile implements Request\u003cUserProfile\u003e {\n    String userId;\n\n    @HandleQuery\n    UserProfile handle() {\n        return loadProfile(userId);\n    }\n}\n```\n\nNow, when calling this query, the return type is automatically known:\n\n```java\nUserProfile profile = FluxCapacitor.queryAndWait(new GetUserProfile(\"123\"));\n```\n\nWhen implementing Request\u003cR\u003e, the expected result type (R) is automatically inferred during testing and execution. This\nenables type-safe tests like:\n\n[//]: # (@formatter:off)\n```java\ntestFixture.whenQuery(new GetUserProfile(\"123\"))\n        .expectResult(profile -\u003e profile.getUserId().equals(\"123\"));\n```\n[//]: # (@formatter:on)\n\nIf a class implements `Request\u003cR\u003e`, Flux will use its declared generic type (R) to check that:\n\n- A compatible handler exists\n- The handler returns the correct result type\n- The correct response is expected during testing\n\n\u003e This pattern is highly recommended for queries, as it reduces boilerplate and improves correctness.\n\n---\n\n## Payload Validation\n\nFlux Capacitor automatically validates incoming request payloads using [JSR 380](https://beanvalidation.org/2.0/)\n(Bean Validation 2.0) annotations.\n\nThis includes support for:\n\n- `@NotNull`, `@NotBlank`, `@Size`, etc.\n- `@Valid` on nested objects\n- Constraint violations in command/query/webrequest payloads\n\nIf a constraint is violated, the handler method is **never called**. Instead, a `ValidationException`,\nis thrown before the handler is invoked.\n\n```java\n\npublic record CreateUser(@NotBlank String userId,\n                         @NotNull @Valid UserProfile profile) {\n}\n```\n\nYou can disable this validation entirely by calling:\n\n[//]: # (@formatter:off)\n```java\nDefaultFluxCapacitor.builder().disablePayloadValidation();\n```\n[//]: # (@formatter:on)\n\nOf course, it is also easy to provide your own validation if desired. For how to do that, please refer to the section\non `HandlerInterceptors`.\n\n\u003e 💡 **Tip**: Flux Capacitor automatically correlates errors with the triggering message (e.g.: command or event).\n\u003e\n\u003e This means you don’t need to log extra context like the message payload or user ID — that information is already\n\u003e available in the audit trail in Flux Platform. This also encourages using **clear, user-facing error messages**\n\u003e without leaking internal details.\n\n---\n\n## User and Role-Based Access Control\n\nFlux Capacitor allows you to restrict message handling based on the authenticated user's roles. This access control\nhappens **before** the message reaches the handler — similar to how payload validation is enforced.\n\nThere are several annotations for declaring user and role requirements:\n\n### `@RequiresAnyRole`\n\nUse this annotation to ensure that a handler is only invoked if the user has **at least one** of the\nspecified roles.\n\n```java\n\n@HandleCommand\n@RequiresAnyRole({\"admin\", \"editor\"})\nvoid handle(UpdateArticle command) { ...}\n```\n\n```java\n\n@RequiresAnyRole(\"admin\")\npublic record DeleteAccount(String userId) {\n}\n```\n\n### `@ForbidsAnyRole`\n\nThis annotation works the other way around — it **prevents** message handling if the user has any of the specified\nroles.\n\n```java\n\n@ForbidsAnyRole(\"guest\")\n@HandleCommand\nvoid handle(SensitiveOperation command) { ...}\n```\n\n### `@RequiresUser`\n\nEnsures that a message can only be handled if an **authenticated user** is present. If no user is found, the message is\nrejected with an `UnauthenticatedException`.\n\nThis is useful for requiring login in scenarios like user account updates, sensitive commands, or personal data access.\n\n```java\n\n@RequiresUser\n@HandleCommand\nvoid handle(UpdateProfile command) { ...}\n```\n\n### `@NoUserRequired`\n\nAllows a message to be processed even if **no authenticated user** is present — ideal for public APIs, or health checks.\n\n```java\n\n@NoUserRequired\n@HandleCommand\nvoid handle(SignUpUser command) { ...}\n```\n\n### `@ForbidsUser`\n\nPrevents message handling if an **authenticated user is present**. This is useful for restricting certain flows to\nunauthenticated users — such as registration endpoints, or guest-only operations.\n\n```java\n@ForbidsUser\n@HandleCommand\nvoid handle(SignUpAsGuest command) { ... }\n```\n\n---\n\n### Controlling Behavior on Unauthorized Access\n\nAll authorization annotations include an optional `throwIfUnauthorized()` property (default: `true`) that controls what\nhappens when access is denied:\n\n- If `throwIfUnauthorized = true`:\n    - If a user is required but **not present**, an `UnauthenticatedException` is thrown.\n    - If a user is present but **lacks the required roles**, an `UnauthorizedException` is thrown.\n\n- If `throwIfUnauthorized = false`:\n    - The handler or message is silently **skipped**, allowing delegation to other eligible handlers (if any).\n\nThis mechanism allows fine-grained control over how authentication and authorization failures are handled across your\nsystem.\n\n---\n\n### Role annotations support nesting and overrides\n\nFlux evaluates these annotations hierarchically. For example:\n\n- If `@RequiresAnyRole(\"admin\")` is placed on a **package**, it applies to all handlers and payloads in that package by\n  default.\n- You can override that requirement on a specific method or class using `@RequiresAnyRole(...)`, `@ForbidsAnyRole(...)`,\n  or `@NoUserRequired`.\n\n```java\n// package-info.java\n@RequiresUser\npackage com.myapp.handlers;\n```\n\n```java\n\n@NoUserRequired\n@HandleCommand\nvoid handle(PublicPing ping) { ...} // Overrides the package-level requirement\n```\n\nThis allows you to apply coarse-grained defaults and override them where needed.\n\n---\n\n### Enum-based role annotations (meta-annotations)\n\nFor more structure, you can define **custom annotations** using enums or strongly typed roles. For example:\n\n```java\npublic enum Role {\n    ADMIN, EDITOR, USER\n}\n```\n\n```java\n\n@RequiresAnyRole\n@Target({ElementType.TYPE, ElementType.METHOD})\npublic @interface RequiresRole {\n    Role[] value();\n}\n```\n\nYou can now annotate your handlers like this:\n\n```java\n\n@HandleCommand\n@RequiresRole(Role.ADMIN)\nvoid handle(DeleteAccount command) { ...}\n```\n\nFlux will interpret the enum-based annotation through the underlying `@RequiresAnyRole`.\n\n---\n\n### Best Practices\n\n- Use role annotations on **payload classes** to guarantee strict access checks in all environments.\n- Use them on **handlers** to control fallback behavior or define role-specific processing.\n- Set security defaults on **packages** or base classes, and override selectively.\n- Define **custom annotations** to avoid scattering string-based role declarations.\n\n---\n\n\u003e 💡 **Tip:** Access control is enforced transparently — there’s no need to log or repeat the user or message context.\n\u003e Flux automatically maintains correlation metadata between the original request and any errors, logs, or events that\n\u003e follow.\n\n---\n\n### Where does user info come from?\n\nUser roles are resolved by the configured `UserProvider`, which extracts the current user from message metadata (e.g.,\nauthentication tokens, headers, etc.). By default, Flux Capacitor uses a pluggable SPI to register this provider.\n\n\u003e 💡 You can override or mock this provider in tests using the TestFixture API.\n\n---\n\n### Providing Your Own User Logic\n\nYou can implement a custom `UserProvider` to extract users from headers, JWT tokens, cookies, etc.\n\n```java\npublic class MyUserProvider extends AbstractUserProvider {\n    public MyUserProvider() {\n        super(\"Authorization\", MyUser.class); // metadata key and user type\n    }\n\n    @Override\n    public User fromMessage(HasMessage message) {\n        if (message.toMessage() instanceof WebRequest request) {\n            return decodeToken(request.getHeader(\"Authorization\"));\n        }\n        return super.fromMessage(message);\n    }\n\n    private User decodeToken(String header) {\n        // Implement your own token decoding and verification logic here\n        return ...;\n    }\n}\n```\n\nThis allows you to inject meaningful, application-specific `User` objects into your handlers.\n\n---\n\n### System and Testing Support\n\nYour `UserProvider` implementation can also support testing and system behavior by implementing:\n\n- `getSystemUser()` — returns a default **system-level user**, used:\n    - as the default actor in tests,\n    - when publishing system-side effects (e.g. from scheduled handlers).\n- `getUserById(...)` — resolves a user by ID, used in test utilities like `fixture.whenCommandByUser(...)`.\n\nThis ensures your custom user logic is consistently applied, even in automated tests and background execution.\n\n---\n\n### 🔧 Registering your UserProvider\n\nTo enable your custom `UserProvider` (e.g., for authenticating users via headers or tokens), register it using Java's\nService Provider mechanism.\n\nCreate the file:\n\n```\nsrc/main/resources/META-INF/services/io.fluxcapacitor.javaclient.tracking.handling.authentication.UserProvider\n```\n\nList your implementation classes (one per line) in order of preference:\n\n```\ncom.example.authentication.SenderProvider\ncom.example.authentication.SystemUserProvider\n```\n\nFlux Capacitor will automatically discover and register them at startup.\n\n\u003e 💡 If you're using Spring, your `UserProvider` can also be exposed as a bean — Flux Capacitor will pick it up\n\u003e automatically.\n\u003e\n\u003e ⚠️ However, tests **not using Spring** will not pick up the bean. For test scenarios or CLI usage, the SPI mechanism\n\u003e is still the easiest.\n\n---\n\n## Scheduling\n\nFlux Capacitor allows scheduling messages for future delivery using the `MessageScheduler`.\n\nHere’s an example that schedules a termination event 30 days after an account is closed:\n\n```java\nclass UserLifecycleHandler {\n    @HandleEvent\n    void handle(AccountClosed event) {\n        FluxCapacitor.schedule(\n                new TerminateAccount(\n                        event.getUserId()),\n                \"AccountClosed-\" + event.getUserId(),\n                Duration.ofDays(30)\n        );\n    }\n\n    @HandleEvent\n    void handle(AccountReopened event) {\n        FluxCapacitor.cancelSchedule(\n                \"AccountClosed-\" + event.getUserId());\n    }\n\n    @HandleSchedule\n    void handle(TerminateAccount schedule) {\n        // Perform termination\n    }\n}\n```\n\nAlternatively, you can schedule commands using `scheduleCommand`:\n\n```java\nclass UserLifecycleHandler {\n    @HandleEvent\n    void handle(AccountClosed event) {\n        FluxCapacitor.scheduleCommand(\n                new TerminateAccount(\n                        event.getUserId()),\n                \"AccountClosed-\" + event.getUserId(),\n                Duration.ofDays(30));\n    }\n\n    @HandleEvent\n    void handle(AccountReopened event) {\n        FluxCapacitor.cancelSchedule(\"AccountClosed-\" + event.getUserId());\n    }\n}\n```\n\n### Periodic scheduling\n\nFlux Capacitor supports recurring message schedules via the `@Periodic` annotation. This makes it easy to run tasks on a\nfixed interval or cron-based schedule — useful for polling, maintenance, background processing, and more.\n\nYou can apply `@Periodic` to either a `Schedule` payload or a `@HandleSchedule` method:\n\n```java\n\n@Periodic(delay = 5, timeUnit = TimeUnit.MINUTES)\npublic record RefreshData(String index) {\n}\n```\n\nThis schedules RefreshData to run every 5 minutes.\n\nOr, to use a cron expression:\n\n```java\n\n@Periodic(cron = \"0 0 * * MON\", timeZone = \"Europe/Amsterdam\")\n@HandleSchedule\nvoid weeklySync(PollData schedule) {\n    ...\n}\n```\n\nThis example triggers the `weeklySync` method every Monday at 00:00 in the Amsterdam time zone.\n\n#### Behavior and advanced options\n\n- `@Periodic` works only for scheduled messages (see `@HandleSchedule`).\n- The schedule **automatically reschedules** itself after each invocation unless canceled.\n- You may:\n    - Return `void` or `null` to continue with the same schedule.\n    - Return a `Duration` or `Instant` to override the next deadline.\n    - Return a new payload or `Schedule` to customize the next cycle.\n- If an error occurs:\n    - The schedule continues by default (`continueOnError = true`).\n    - You may specify a fallback delay via `delayAfterError`.\n    - Throw `CancelPeriodic` from the handler to stop the schedule completely.\n- To prevent startup activation, use `@Periodic(autoStart = false)`.\n- The schedule ID defaults to the class name, but can be customized with `scheduleId`.\n\nHere's an example of robust polling with error fallback:\n\n```java\n\n@Periodic(delay = 60, timeUnit = TimeUnit.MINUTES, delayAfterError = 10)\n@HandleSchedule\nvoid pollExternalService(PollTask pollTask) {\n    try {\n        externalService.fetchData();\n    } catch (Exception e) {\n        log.warn(\"Polling failed, will retry in 10 minutes\", e);\n        throw e;\n    }\n}\n```\n\nIn this case:\n\n- The task runs every hour normally.\n- If it fails, it retries after 10 minutes.\n- It resumes the original schedule if the next invocation succeeds.\n\n---\n\n## User-defined message logs\n\nFlux Capacitor supports **custom message logs** in addition to built-in ones like commands, events, and queries.\n\nMost applications won't need custom message logs — but they can be very useful in advanced scenarios:\n\n- To **track external systems or integrations**, while keeping the main event log clean.\n- To **store batches of updates** for external consumers that poll infrequently.\n- To **segment logic** or apply different retention guarantees per topic.\n\nFlux Capacitor lets you publish and handle custom messages by assigning them to **user-defined topics**. These messages\nare written to their own durable logs and can be tracked just like commands or events.\n\n### Custom Handlers\n\nTo receive messages from a custom log, annotate your method with `@HandleCustom` and specify the topic name:\n\n```java\n\n@HandleCustom(\"metering-points\")\nvoid on(MeteringPoint event) {\n    log.info(\"Metering point: {}\", event);\n}\n```\n\nLike other handlers, this can be:\n\n- **Stateless or stateful**\n- Attached to a **custom consumer**\n- Used to **return a result** if the message is a request\n- **Passive**, meaning it won’t emit result messages\n\n\u003e ✅ Custom logs are fully integrated with Flux tracking and delivery infrastructure.\n\n### 📬 Publishing to a Custom Log\n\nYou can publish messages manually to any custom topic:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.get()\n    .customGateway(\"third-party-events\")\n    .sendAndForget(new AuditEntry(\"User login\"));\n```\n[//]: # (@formatter:on)\n\nThis makes it easy to introduce new asynchronous workflows, specialized event types, or low-frequency control signals.\n\n### 🧹 Setting Retention Time\n\nEach message log in Flux retains messages independently. You can configure how long messages in your **custom log**\nshould be retained:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.get()\n    .customGateway(\"third-party-events\")\n    .setRetentionTime(Duration.ofDays(90));\n```\n[//]: # (@formatter:on)\n\nRetention can also be set for built-in logs like commands or results — though it's advisable to configure it only\nfor custom topics.\n\n---\n\n## Dispatching Messages\n\nFlux Capacitor provides a unified and transparent way to send messages of all types—**commands**, **events**,\n**queries**, **schedules**, **web requests**, **metrics**, and more. All message types are routed through a shared\ndispatch\ninfrastructure, with built-in support for:\n\n- **Location transparency**: Message destinations are resolved dynamically. Handlers may live within the current\n  process or in a remote application across the globe—as long as it's connected to the same Flux Platform.\n- **Dispatch interceptors** for metadata enrichment, authorization, logging, or suppression.\n- **Local-first handling**: If a handler exists in the local process, it will receive the message immediately.\n- **Automatic forwarding** to the Flux Platform when no local handlers consume the message.\n- **Serialization and correlation**: All messages are serialized, tagged with tracing data, and routed by type and\n  topic.\n\n### Sending a Message\n\nThe easiest way to dispatch messages is via static methods on the `FluxCapacitor` class:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.sendCommand(new CreateUser(\"Alice\"));  // Asynchronously send a command\nFluxCapacitor.queryAndWait(new GetUserById(\"user-123\"));  // Query and block for response\nFluxCapacitor.publishEvent(new UserSignUp(...));  // Fire-and-forget event\nFluxCapacitor.schedule(new RetryPayment(...), Duration.ofMinutes(5));  // Delayed message\n```\n[//]: # (@formatter:on)\n\nEach message can also include optional metadata:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.sendCommand(new CreateUser(\"Bob\"),\n                          Metadata.of(\"source\",\"admin-ui\"));\n```\n[//]: # (@formatter:on)\n\n\u003e ⚠️ Messages are **not routed to a specific destination**. Instead, any number of matching handlers may receive the\n\u003e message, whether they run locally or remotely. Flux Capacitor abstracts the transport so that handler location is\n\u003e irrelevant.\n\nFor a deeper look at message handling, see the [Message Handling](#message-handling) section.\n\n---\n\n### Commands\n\nCommands are used to trigger state changes or domain logic. They may return a result or be fire-and-forget.\n\n**Send and forget:**\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.sendAndForgetCommand(new CreateUser(\"Alice\"));\n```\n[//]: # (@formatter:on)\n\n**Send and wait (async or blocking):**\n\n[//]: # (@formatter:off)\n```java\nCompletableFuture\u003cUserId\u003e future \n        = FluxCapacitor.sendCommand(new CreateUser(\"Bob\"));\n\nUserId id = FluxCapacitor.sendCommandAndWait(\n        new CreateUser(\"Charlie\"));\n```\n[//]: # (@formatter:on)\n\n---\n\n### Queries\n\nQueries retrieve data from read models or projections.\n\n**Async:**\n\n[//]: # (@formatter:off)\n```java\nCompletableFuture\u003cUserProfile\u003e result = FluxCapacitor.query(new GetUserProfile(\"user123\"));\n```\n[//]: # (@formatter:on)\n\n**Blocking:**\n\n[//]: # (@formatter:off)\n```java\nUserProfile profile = FluxCapacitor.queryAndWait(new GetUserProfile(\"user456\"));\n```\n[//]: # (@formatter:on)\n\n---\n\n### Events\n\nEvents can be published via:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.publishEvent(new UserLoggedIn(\"user789\"));\n```\n[//]: # (@formatter:on)\n\nBy default:\n\n- ✅ **Events are persisted** in the global event log, making them available for downstream processing, projections, or\n  analytics.\n- ⚠️ **Exception:** If a **local handler** exists the event will not be forwarded or stored, unless\n  `@LocalHandler(logMessage = true)`.\n\n\u003e For aggregate-related domain events, use `Entity#apply(...)` instead. This ensures the event is applied to the entity\n\u003e and conditionally published and logged, depending on the aggregate configuration.\n\n---\n\n### Schedules\n\nYou can schedule messages or commands for later delivery:\n\n[//]: # (@formatter:off)\n```java\n// Schedule a message to be handled in 5 minutes using @HandleSchedule\nFluxCapacitor.schedule(new ReminderFired(), Duration.ofMinutes(5));\n\n// Fire a periodic schedule every hour\nFluxCapacitor.schedulePeriodic(new PollExternalApi());\n```\n[//]: # (@formatter:on)\n\n---\n\n### Web Requests\n\nSend an outbound HTTP call via the proxy mechanism in Flux Platform:\n\n[//]: # (@formatter:off)\n```java\nWebRequest request = WebRequest\n        .get(\"https://api.example.com/data\").build();\nWebResponse response = FluxCapacitor.get()\n        .webRequestGateway().sendAndWait(request);\n```\n[//]: # (@formatter:on)\n\n---\n\n### Metrics\n\nYou can publish custom metrics to the Flux Platform:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.publishMetrics(\n        new SystemLoadMetric(cpu, memory));\n```\n[//]: # (@formatter:on)\n\n---\n\n### What Happens After You Dispatch?\n\nOnce you dispatch a message using any of the above methods, Flux goes through the following pipeline:\n\n#### 1. **Dispatch Interceptors (Pre-Serialization)**\n\nBefore the message is serialized, all applicable `DispatchInterceptor`s are given a chance to:\n\n- Inject metadata (e.g. correlation IDs, timestamps)\n- Modify or validate the message\n- Suppress or block dispatch entirely\n\n#### 2. **Local Handlers**\n\nIf any handlers (e.g. `@HandleCommand`, `@HandleQuery`) are registered for this message type and topic, they will be\ninvoked **locally**.\n\n- If the handler is annotated with `@LocalHandler(logMessage = true)`, the message is still forwarded to the Flux\n  Platform.\n- If the handler **fully consumes** the message, it won't be forwarded.\n\n#### 3. **Serialization**\n\nThe message is converted to a `SerializedMessage`, typically using Jackson. This is where versioning and up/downcasting\nmay apply.\n\n#### 4. **Dispatch Interceptors (Post-Serialization)**\n\nAfter serialization, interceptors may again modify the message—for example, to inject correlation headers based on the\nserialized form.\n\n#### 5. **Forwarding to Flux Platform**\n\nIf no local handler consumes the message, it is published to the Flux Platform via the configured `Client` (e.g.\n`WebSocketClient`).\n\n- Topics and routing are determined by message type and metadata\n- Platform-side logic (like deduplication, retries, rate limits) may apply\n\n---\n\n### Request Timeouts\n\nThe `@Timeout` annotation allows developers to specify how long Flux should wait for a **command** or **query** to\ncomplete when using synchronous (`sendAndWait`) APIs.\n\nThis is especially useful for time-sensitive interactions where you want to fail fast or have specific SLA expectations\nfor certain request types.\n\n### Usage\n\nApply `@Timeout` to a **payload class** (typically a command or query):\n\n[//]: # (@formatter:off)\n```java\n@Timeout(value = 3, timeUnit = TimeUnit.SECONDS)\npublic record CalculatePremium(UserProfile profile) implements Request\u003cBigDecimal\u003e {}\n```\n[//]: # (@formatter:on)\n\nWhen this message is sent using a blocking gateway call, the configured timeout is respected:\n\n[//]: # (@formatter:off)\n```java\nBigDecimal result = FluxCapacitor\n        .sendAndWait(new CalculatePremium(user));\n```\n[//]: # (@formatter:on)\n\nIf the timeout elapses before a response is received, a `TimeoutException` is thrown.\n\n### Notes\n\n- The timeout only applies to blocking calls (e.g., `sendAndWait`). If you use non-blocking `send(...)` methods that\n  return a `CompletableFuture`, **no timeout is enforced** unless you manually attach one:\n\n  [//]: # (@formatter:off)\n  ```java\n  CompletableFuture\u003cBigDecimal\u003e future \n        = FluxCapacitor.send(new CalculatePremium(user));\n  future.orTimeout(3, TimeUnit.SECONDS);\n  ```\n  [//]: # (@formatter:on)\n\n- The default timeout for blocking operations (when `@Timeout` is not present) is **1 minute**.\n- To configure timeouts for `WebRequest` calls, use `WebRequestSettings#timeout`.\n\n---\n\n## Testing your Handlers\n\nFlux Capacitor comes with a flexible, expressive testing framework based on the given-when-then pattern. This enables\nwriting behavioral tests for your handlers without needing to mock the infrastructure.\n\nHere’s a basic example:\n\n```java\nTestFixture testFixture = TestFixture.create(new UserEventHandler());\n\n@Test\nvoid newUserGetsWelcomeEmail() {\n    testFixture.whenEvent(new CreateUser(userId, myUserProfile))\n            .expectCommands(new SendWelcomeEmail(myUserProfile));\n}\n```\n\nThis test ensures that when a `CreateUser` event occurs, a `SendWelcomeEmail` command is issued by the handler.\n\n### Testing complete workflows\n\nYou can test full workflows across multiple handlers:\n\n```java\nTestFixture fixture = TestFixture.create(new UserCommandHandler(), new UserEventHandler());\n\n@Test\nvoid creatingUserTriggersEmail() {\n    fixture.whenCommand(new CreateUser(userProfile))\n            .expectCommands(new SendWelcomeEmail(userProfile));\n}\n```\n\nUse `expectOnlyCommands()` to assert that **only** the expected command was issued:\n\n[//]: # (@formatter:off)\n```java\nfixture.whenCommand(new CreateUser(userProfile))\n       .expectOnlyCommands(new SendWelcomeEmail(userProfile));\n```\n[//]: # (@formatter:on)\n\nYou can also match by class, predicate, or Hamcrest matcher:\n\n[//]: # (@formatter:off)\n```java\nfixture.whenCommand(new CreateUser(userProfile))\n       .expectCommands(SendWelcomeEmail.class, \n                       isA(AddUserToOrganization.class));\n```\n[//]: # (@formatter:on)\n\n### Chained expectations\n\nMultiple expectations can be chained to test the full sequence of events and commands:\n\n[//]: # (@formatter:off)\n```java\nfixture.whenCommand(new CreateUser(userProfile))\n       .expectCommands(new SendWelcomeEmail(userProfile))\n       .expectEvents(new UserStatsUpdated(...));\n```\n[//]: # (@formatter:on)\n\nYou can also chain multiple **inputs** using `.andThen()` to simulate a sequence of events, commands, or queries:\n\n[//]: # (@formatter:off)\n```java\nfixture.whenCommand(new CreateUser(userProfile))\n       .expectCommands(new SendWelcomeEmail(userProfile))\n       .andThen()\n       .whenQuery(new GetUser(userId))\n       .expectResult(userProfile);\n```\n[//]: # (@formatter:on)\n\nThis example first triggers a `CreateUser` command, expects a `SendWelcomeEmail` event, and then issues a `GetUser`\nquery,\nasserting that it returns the expected result.\n\n### Using givenXxx() for preconditions\n\nUse `givenCommands`, `givenEvents`, etc., to simulate preconditions:\n\n[//]: # (@formatter:off)\n```java\nfixture.givenCommands(new CreateUser(userProfile), new ResetPassword(...))\n       .whenCommand(new UpdatePassword(...))\n       .expectEvents(UpdatePassword.class);\n```\n[//]: # (@formatter:on)\n\n### Providing External JSON Files\n\nTest fixtures support loading inputs from external JSON resources. This allows you to keep your tests clean and reuse\nstructured input data.\n\nAny `givenXyz(...)`, `whenXzy(...)`, or `expectXyz(...)` method argument that is a `String` ending with `.json` will be\ninterpreted as a **classpath resource path**, and deserialized accordingly.\n\nFor example:\n\n[//]: # (@formatter:off)\n```java\nfixture.givenCommands(\"create-user.json\")\n    .whenQuery(new GetUser(userId))\n    .expectResult(\"user-profile.json\");\n```\n[//]: # (@formatter:on)\n\nIf your test class is in the `org.example` package, this will resolve to `/org/example/create-user.json` in the\nclasspath, unless the JSON path is absolute (starts with `/`), e.g.:\n\n```java\nfixture.givenCommands(\"/users/create-user.json\");\n```\n\n#### Class Resolution with `@class`\n\nEach JSON file must include a `@class` property to enable deserialization:\n\n```json\n{\n  \"@class\": \"org.example.CreateUser\",\n  \"userId\": \"3290328\",\n  \"email\": \"foo.bar@example.com\"\n}\n```\n\nIf your classes or packages are annotated with `@RegisterType`, you can use **simple class names**:\n\n```json\n{\n  \"@class\": \"CreateUser\"\n}\n```\n\nOr partial paths:\n\n```json\n{\n  \"@class\": \"example.CreateUser\"\n}\n```\n\n\u003e This enables readable and concise references while still resolving to the correct type.\n\n#### Inheriting from Other JSON Files\n\nJSON resources can **extend** other resources using the `@extends` keyword:\n\n```json\n{\n  \"@extends\": \"create-user.json\",\n  \"details\": {\n    \"lastName\": \"Johnson\"\n  }\n}\n```\n\nThis will recursively merge the referenced file (`/org/example/create-user.json`) with the current one, allowing you\nto override or augment deeply nested structures.\n\n\u003e 🧠 This is especially useful for composing test scenarios with shared defaults or inheritance-like setups.\n\n---\n\n### Adding or asserting metadata\n\nWrap your payload in a Message to add or assert metadata:\n\n```java\n\n@Test\nvoid newAdminGetsAdditionalEmail() {\n    testFixture.whenCommand(new Message(new CreateUser(...),\n    Metadata.of(\"roles\", Arrays.asList(\"Customer\", \"Admin\"))))\n        .expectCommands(new SendWelcomeEmail(...),\n    new SendAdminEmail(...));\n}\n```\n\n### Result and exception assertions\n\nYou can assert the result returned by a command or query:\n\n[//]: # (@formatter:off)\n```java\nfixture.givenCommands(new CreateUser(userProfile))\n       .whenQuery(new GetUser(userId))\n       .expectResult(userProfile);\n```\n[//]: # (@formatter:on)\n\nTo assert an exception:\n\n[//]: # (@formatter:off)\n```java\nfixture.givenCommands(new CreateUser(userProfile))\n        .whenCommand(new CreateUser(userProfile))\n        .expectExceptionalResult(IllegalCommandException.class);\n```\n[//]: # (@formatter:on)\n\n### User-aware tests\n\nYou can also simulate a command from a specific user:\n\n[//]: # (@formatter:off)\n```java\nvar user = new MyUser(\"pete\");\n\nfixture.whenCommandByUser(user, \"confirm-user.json\")\n       .expectExceptionalResult(UnauthorizedException.class);\n```\n[//]: # (@formatter:on)\n\nYou can also pass a user ID string directly instead of a full User object. The test fixture will resolve it using the\nconfigured `UserProvider` (by default loaded via Java's `ServiceLoader`):\n\n[//]: # (@formatter:off)\n```java\nfixture\n    .givenCommands(\"create-user-pete.json\")\n    .whenCommandByUser(\"pete\", \"confirm-user.json\")\n    .expectExceptionalResult(UnauthorizedException.class);\n```\n[//]: # (@formatter:on)\n\nIn this example, the string \"pete\" is resolved to a `User` instance using the active `UserProvider`, and the test\nasserts\nthat the confirmation is unauthorized.\n\n### Verifying side effects\n\nUse `expectThat()` or `expectTrue()` to assert custom logic or verify interactions (e.g., using Mockito):\n\n[//]: # (@formatter:off)\n```java\nfixture.whenCommand(\"create-user-pete.json\")\n       .expectThat(fc -\u003e Mockito.verify(emailService).sendEmail(...));\n```\n[//]: # (@formatter:on)\n\n### Triggering side effects manually\n\nUse `whenExecuting()` to test code outside of message dispatching, like HTTP calls:\n\n[//]: # (@formatter:off)\n```java\nfixture.whenExecuting(fc -\u003e httpClient.put(\"/user\", \"/users/user-profile-pete.json\"))\n       .expectEvents(\"create-user-pete.json\");\n```\n[//]: # (@formatter:on)\n\n### Asynchronous tests\n\nBy default, `TestFixture.create(...)` creates synchronous fixtures where handlers are executed locally in the same\nthread\nthat publishes the message. This provides fast, deterministic behavior ideal for most unit tests.\n\nHowever, in production your handlers are typically dispatched asynchronously by consumers running on separate threads.\nTo better simulate this behavior in tests — especially when testing event-driven flows or stateful consumers — you can\nuse an asynchronous fixture instead.\n\n```java\nTestFixture fixture = TestFixture.createAsync(new MyHandler(), MyStatefulHandler.class);\n```\n\nThis ensures that:\n\n- Handlers are tracked via the same consumer infrastructure used in production.\n- Behavior involving asynchronous dispatch, retries, or stateful models is tested realistically.\n- Eventual consistency is respected (e.g., expect...() calls will wait for outcomes to materialize).\n\n\u003e **Note:** Handlers annotated with `@LocalHandler` are executed synchronously, even in async fixtures, just as they\n\u003e would in production.\n\n### Using Test Fixtures in Spring\n\nFlux Capacitor provides seamless integration with Spring Boot for testing. You can inject a `TestFixture` directly:\n\n```java\n\n@SpringBootTest\nclass AsyncAppTest {\n\n    @Autowired\n    TestFixture fixture;\n\n    @Test\n    void testSomething() {\n        fixture.whenCommand(\"commands/my-command.json\")\n                .expectEvents(\"events/expected-event.json\");\n    }\n}\n```\n\n- ✅ **Spring Boot**: No manual setup needed—`TestFixture` is auto-configured.\n- ⚠️ **Spring Core (non-Boot)**: Manually import the test configuration:\n\n  ```java\n  @Import(FluxCapacitorTestConfig.class)\n  ```\n\n  This ensures the `TestFixture` and related infrastructure are available in the Spring context.\n\n---\n\n#### Switching to Synchronous Mode\n\nBy default, the injected fixture is **asynchronous**. To use a **synchronous** fixture instead:\n\n##### Globally via `application.properties`:\n\n```properties\nfluxcapacitor.test.sync=true\n```\n\n##### Or per test class:\n\n```java\n\n@TestPropertySource(properties = \"fluxcapacitor.test.sync=true\")\n@SpringBootTest\nclass SyncAppTest {\n\n    @Autowired\n    TestFixture fixture;\n\n    // test logic...\n}\n```\n\n### Testing schedules\n\nFlux Capacitor’s scheduling engine makes it easy to test time-based workflows. Scheduled messages are handled just\nlike any other message, except they are delayed until their due time.\n\nUse the `TestFixture` to simulate the passage of time and trigger scheduled actions. Here’s a typical example:\n\n```java\nTestFixture testFixture = TestFixture.create(new UserCommandHandler(), new UserLifecycleHandler());\n\n@Test\nvoid accountIsTerminatedAfterClosing() {\n    testFixture\n            .givenCommands(new CreateUser(myUserProfile),\n                           new CloseAccount(userId))\n            .whenTimeElapses(Duration.ofDays(30))\n            .expectEvents(new AccountTerminated(userId));\n}\n```\n\nIn this test:\n\n- We schedule the AccountTerminated message as a result of the CloseAccount command.\n- Then we simulate that 30 days have passed using whenTimeElapses(...).\n- Finally, we verify that the expected AccountTerminated event was published.\n\nYou can also test cancellation logic:\n\n```java\n\n@Test\nvoid accountReopeningCancelsTermination() {\n    testFixture\n            .givenCommands(new CreateUser(myUserProfile),\n                           new CloseAccount(userId),\n                           new ReopenAccount(userId))\n            .whenTimeElapses(Duration.ofDays(30))\n            .expectNoEventsLike(AccountTerminated.class);\n}\n```\n\nIf needed, you can advance time to an absolute timestamp using:\n\n[//]: # (@formatter:off)\n```java\nfixture.whenTimeAdvancesTo(Instant.parse(\"2050-12-31T00:00:00Z\"));\n```\n[//]: # (@formatter:on)\n\nThis is especially useful for workflows tied to specific deadlines or calendar dates.\n\n---\n\n## Handling Web Requests\n\nFlux Capacitor supports first-class **WebRequest handling** via the `@HandleWeb` annotation and its HTTP-specific\nvariants such as `@HandleGet`, `@HandlePost`, `@HandleDelete`, etc.\n\nInstead of exposing a public HTTP server per application, Flux uses a central Web Gateway that **proxies all external\nHTTP(S) and WebSocket traffic into the platform as `WebRequest` messages**. These messages are:\n\n- **Logged** for traceability and auditing\n- **Routed to client applications** using the same handler system as for commands, events, and queries\n- **Handled by consumer applications** which return a `WebResponse`\n\n#### Why This Design?\n\nThis architecture enables several key benefits:\n\n- ✅ **Zero exposure**: client apps do not require a public-facing HTTP server and are thus *invisible* to attackers\n- ✅ **Back-pressure support**: applications control load by polling their own messages\n- ✅ **Audit-friendly**: every incoming request is automatically logged and correlated to its response\n- ✅ **Multiple consumers possible**: multiple handlers can react to a WebRequest, though typically only one produces the\n  response (others use `passive = true`)\n\n#### Example\n\n```java\n\n@HandleGet(\"/users\")\npublic List\u003cUserAccount\u003e listUsers() {\n    return userService.getAllUsers();\n}\n```\n\nThis will match incoming GET requests to `/users` and return a list of users. The response is published back as a\n`WebResponse`.\n\nYou can use the general `@HandleWeb` if you want to match multiple paths or methods or define a custom HTTP method:\n\n```java\n\n@HandleWeb(value = \"/users/{userId}\", method = {\"GET\", \"DELETE\"})\npublic CompletableFuture\u003c?\u003e handleUserRequest(WebRequest request, @PathParam String userId) {\n    return switch (request.getMethod()) {\n        case \"GET\" -\u003e FluxCapacitor.query(new GetUser(userId));\n        case \"DELETE\" -\u003e FluxCapacitor.sendCommand(new DeleteUser(userId));\n        default -\u003e throw new UnsupportedOperationException();\n    };\n}\n```\n\n\u003e 💡 **Tip:** To match any HTTP method without listing them explicitly, use `HttpRequestMethod.ANY`.\n\u003e This is equivalent to `\"*\"` and will match all incoming requests for the given path.\n\n```java\n\n@HandleWeb(value = \"/users/{userId}\", method = HttpRequestMethod.ANY)\npublic CompletableFuture\u003c?\u003e handleAllUserMethods(\n        WebRequest request, @PathParam String userId) {\n    // Handle any method (GET, POST, DELETE, etc.)\n    ...\n}\n```\n\n#### Suppressing a Response\n\nIf you want to listen to a WebRequest without generating a response (e.g. for auditing or monitoring), use\n`passive = true`:\n\n```java\n\n@HandlePost(value = \"/log\", passive = true)\npublic void log(WebRequest request) {\n    logService.store(request);\n}\n```\n\n#### Dynamic Path Parameters\n\nUse the `@PathParam` annotation to extract dynamic segments from the URI path into handler method parameters:\n\n```java\n\n@HandleGet(\"/users/{id}\")\npublic UserAccount getUser(@PathParam String id) {\n    return userService.get(id);\n}\n```\n\nIf the `value` is left empty, the framework will use the parameter name (`id` in this case).\n\n#### Other Parameter Annotations\n\nIn addition to `@PathParam`, you can extract other values from the request using:\n\n- `@QueryParam` – extract query string values\n- `@HeaderParam` – extract HTTP headers\n- `@CookieParam` – extract cookie values\n- `@FormParam` – extract form-encoded values (for POST/PUT)\n\nEach of these annotations supports the same rules:\n\n- If no name is given, the method parameter name is used\n- Values are automatically converted to the target parameter type\n\n#### URI Prefixing and Composition with `@Path`\n\nThe `@Path` annotation can be used at the **package**, **class**, **method**, or **property** level to construct URI\npath segments. Paths are chained together from the outermost package to the innermost handler method. If a path segment\nstarts with `/`, it resets the chain from that point downward.\n\n- Empty path values use the simple name of the package.\n- If placed on a field or getter, the property value is used as the path segment (enabling dynamic routing).\n\n```java\n@Path\npackage my.example.api;\n\n@Path(\"users\")\npublic class UserHandler {\n\n    @Path(\"{id}\")\n    @HandleGet\n    public User getUser(@PathParam String id) { ...}\n}\n```\n\nThis matches `/api/users/{id}`. If the class annotation had started with a `/`, i.e.: had been `@Path(\"/users\")`, the\npattern would have become `/users/{id}`.\n\n---\n\n## Serving Static Files\n\nFlux Capacitor supports serving static files directly from a handler class by using the `@ServeStatic` annotation.\nThis allows client applications to expose static resources (HTML, JS, CSS, images, etc.) without needing an external web\nserver.\n\n```java\n\n@ServeStatic(value = \"/web\", resourcePath = \"/static\")\npublic class WebAssets {\n}\n```\n\nThis will serve all files under `/static` (from the classpath or file system) under the URI path `/web`.\n\n### Features\n\n- Supports both **file system** and **classpath** resources\n- Optional **fallback file** (e.g. for single-page apps)\n- Automatic `Cache-Control` headers\n- Compression via Brotli and GZIP (if precompressed variants exist)\n\n### Annotation Reference\n\n```java\n@ServeStatic(\n        value = \"/assets\",\n        resourcePath = \"/public\",\n        fallbackFile = \"index.html\",\n        immutableCandidateExtensions = {\"js\", \"css\", \"svg\"},\n        maxAgeSeconds = 86400\n)\n```\n\n#### Parameters:\n\n- `value`: Web path(s) where static content is served. Relative paths are prefixed by `@Path` values.\n- `resourcePath`: The resource root (either on the file system or classpath).\n- `fallbackFile`: A file to serve when the requested path doesn’t exist (e.g. `index.html`). Set to `\"\"` to disable.\n- `immutableCandidateExtensions`: Extensions that are eligible for aggressive caching if fingerprinted (e.g.\n  `main.123abc.js`).\n- `maxAgeSeconds`: Default cache duration for non-immutable resources.\n\n### Example: Serving a React App\n\n```java\n\n@ServeStatic(value = \"/app\", resourcePath = \"/static\", fallbackFile = \"index.html\")\npublic class WebFrontend { ...\n}\n```\n\nThis will serve files under `/app/**` and fallback to `index.html` for unknown paths—ideal for single-page apps.\n\n\u003e 📁 Files in `/static` on the classpath (e.g. under `resources/static/`) or `/static` on disk will be served.\n\n\u003e When both file system and classpath contain a file, the **file system takes precedence**.\n\n\u003e If the `resourcePath` starts with `classpath:`, **only classpath resources** are served.  \n\u003e If it starts with `file:`, **only file system resources** are served.\n\nThis allows precise control over where content is loaded from and ensures classpath-only or file-system-only resolution\ndepending on the use case.\n\n#### Combining Static and Dynamic Handlers\n\nYou can freely combine `@ServeStatic` with dynamic handler methods in the same class or package.\n\nThis is especially useful when your application serves a combination of:\n\n- **Static assets** like HTML, JS, CSS\n- **Dynamic endpoints** like REST APIs or view-rendered pages\n\n```java\n\n@Path(\"/app\")\n@ServeStatic(\"static\") // serves static files from the /static resource directory for web paths /app/static/**\npublic class AppController {\n\n    @HandleGet(\"/status\")\n    Status getStatus() {\n        return new Status(\"OK\", Instant.now());\n    }\n\n    @HandlePost(\"/submit\")\n    SubmissionResult submitForm(FormData data) {\n        return formService.handle(data);\n    }\n}\n```\n\nThis will:\n\n- Serve `/app/static/index.html`, `/app/static/styles.css`, etc. from the `static/` resource directory\n- Also respond to `/app/status` and `/app/submit` dynamically\n\nThe static file handling applies to all routes **not matched** by other methods in the class. This makes it ideal for\ncombining SPAs or hybrid web apps with API endpoints under a shared route prefix.\n\n---\n\n## Handling WebSocket Messages\n\nFlux Capacitor provides first-class support for **WebSocket communication**, enabling stateful or stateless message\nhandling using the same annotation-based model as other requests.\n\nWebSocket requests are published to the **WebRequest** log after being reverse-forwarded from the Flux platform, and can\nbe consumed and responded to like any other request type.\n\n---\n\n### Two Styles of WebSocket Handling\n\n#### 1. **Stateless Handlers** (Singleton Style)\n\nUse annotations like `@HandleSocketOpen`, `@HandleSocketMessage`, and `@HandleSocketClose` directly on singleton handler\nclasses:\n\n```java\n\n@HandleSocketOpen(\"/chat\")\npublic String onOpen() {\n    return \"Welcome!\";\n}\n\n@HandleSocketMessage(\"/chat\")\npublic String onMessage(String incoming) {\n    return \"Echo: \" + incoming;\n}\n\n@HandleSocketClose(\"/chat\")\npublic void onClose(SocketSession session) {\n    System.out.println(\"Socket closed: \" + session.sessionId());\n}\n```\n\nResponses can be returned directly from `@HandleSocketMessage` and `@HandleSocketOpen` methods. For more control, you\ncan inject the `SocketSession` parameter and send messages manually.\n\nOther available annotations:\n\n- `@HandleSocketPong` — handle pong responses\n- `@HandleSocketHandshake` — override the default handshake logic\n\n#### 2. **Stateful Sessions with `@SocketEndpoint`**\n\nIf you need to maintain per-session state, use `@SocketEndpoint`. Each WebSocket session will instantiate a fresh\nhandler object:\n\n```java\n\n@SocketEndpoint\n@Path(\"/chat\")\npublic class ChatSession {\n\n    private final List\u003cString\u003e messages = new ArrayList\u003c\u003e();\n\n    @HandleSocketOpen\n    public String onOpen() {\n        return \"Connected!\";\n    }\n\n    @HandleSocketMessage\n    public void onMessage(String text, SocketSession session) {\n        messages.add(text);\n        session.sendMessage(\"Stored message: \" + text);\n    }\n\n    @HandleSocketClose\n    public void onClose() {\n        System.out.println(\"Messages in this session: \" + messages.size());\n    }\n}\n```\n\nStateful sessions are useful for flows involving authentication, message accumulation, or temporal context (e.g. cursor,\nbuffer, sequence).\n\n\u003e ✅ `@SocketEndpoint` handlers are prototype-scoped, meaning they're constructed once per session.\n\n\u003e ℹ️ Like other handlers, socket endpoints may be annotated with `@Consumer` for tracking isolation.\n\n---\n\n### Automatic Ping-Pong \u0026 Keep-Alive\n\nWhen using `@SocketEndpoint`, Flux Capacitor automatically manages **keep-alive pings**:\n\n- Pings are sent at regular intervals (default: every 60s)\n- If a `pong` is not received within a timeout, the session is closed\n- You can customize this behavior via the `aliveCheck` attribute on `@SocketEndpoint`\n\n```java\n\n@SocketEndpoint(aliveCheck = @SocketEndpoint.AliveCheck(pingDelay = 30, pingTimeout = 15))\npublic class MySession { ...\n}\n```\n\n---\n\n### Summary\n\n| Annotation                 | Description                                        |\n|----------------------------|----------------------------------------------------|\n| `@HandleSocketOpen`        | Handles WebSocket connection open                  |\n| `@HandleSocketMessage`     | Handles incoming text or binary WebSocket messages |\n| `@HandleSocketPong`        | Handles pong responses (usually for health checks) |\n| `@HandleSocketClose`       | Handles WebSocket session closing                  |\n| `@HandleSocketHandshake`   | Allows customizing the handshake phase             |\n| `@SocketEndpoint`          | Declares a per-session WebSocket handler class     |\n| `SocketSession` (injected) | Controls sending messages, pinging, and closing    |\n\nFlux Capacitor makes WebSocket communication secure, observable, and composable—integrated seamlessly into your\ndistributed, event-driven architecture.\n\n---\n\n## Testing Web Endpoint Behavior\n\nFlux Capacitor allows you to simulate and verify HTTP interactions as part of your test flows. Web requests\ncan be tested like any other command, query, or event.\n\nHere’s a complete test for a `POST /games` handler that accepts a JSON request and publishes a command:\n\n```java\n\n@Test\nvoid registerGame() {\n    testFixture\n            .whenPost(\"/games\", \"/game/game-details.json\")  // Simulates POST with payload\n            .expectResult(GameId.class)                     // Asserts a result is returned\n            .expectEvents(RegisterGame.class);              // Asserts a command was published\n}\n```\n\nThe corresponding handler is:\n\n```java\n\n@HandlePost(\"/games\")\nCompletableFuture\u003cGameId\u003e addGame(GameDetails details) {\n    return FluxCapacitor.sendCommand(new RegisterGame(details));\n}\n```\n\nAs always, the `.json` file is automatically loaded from the classpath, allowing you to cleanly separate test data:\n\n📄 `/game/game-details.json`\n\n```json\n{\n  \"title\": \"Legend of the Skylands\",\n  \"description\": \"An epic singleplayer adventure with puzzles and secrets.\",\n  \"releaseDate\": \"2025-11-12T00:00:00Z\",\n  \"tags\": [\n    \"adventure\",\n    \"puzzle\",\n    \"singleplayer\"\n  ]\n}\n```\n\n---\n\n### Example: Querying with `GET`\n\nYou can test GET endpoints just as easily. This example first registers a game via `POST /games`, then fetches the list\nof all games via `GET /games` and checks the result:\n\n```java\n\n@Test\nvoid getGames() {\n    testFixture\n            .givenPost(\"/games\", \"/game/game-details.json\")   // Precondition: register a game\n            .whenGet(\"/games\")                                // Perform GET request\n            .\u003cList\u003cGame\u003e\u003eexpectResult(r -\u003e r.size() == 1)     // Assert one game is returned\n            .expectWebResponse(r -\u003e r.getStatus() == 200);    // Assert the status of the response\n}\n```\n\nThis corresponds to the following handler method:\n\n```java\n\n@HandleGet\n@Path(\"/games\")\nCompletableFuture\u003cList\u003cGame\u003e\u003e getGames(@QueryParam String term) {\n    return FluxCapacitor.query(new FindGames(term));\n}\n```\n\n---\n\n### Testing Error Responses and Exceptions\n\nFlux Capacitor also allows you to verify how your web endpoints handle exceptional scenarios. This includes asserting\nthe type of exception thrown as well as inspecting the resulting HTTP status code or response body.\n\nHere’s a test that triggers a `403 Forbidden` error by throwing an `IllegalCommandException`:\n\n```java\n\n@Test\nvoid postReturnsError() {\n    testFixture\n            .whenPost(\"/error\", \"body\")                              // Simulate POST request\n            .expectExceptionalResult(IllegalCommandException.class)  // Assert thrown exception\n            .expectWebResponse(r -\u003e r.getStatus() == 403);           // Assert HTTP 403 response\n}\n```\n\nThe corresponding handler might look like:\n\n```java\n\n@HandlePost(\"/error\")\nvoid postForError(String body) {\n    throw new IllegalCommandException(\"error: \" + body);\n}\n```\n\nThis enables robust testing of both successful and failure paths for all your web request handlers.\n\n---\n\n### Path Parameter Substitution in Tests\n\nWhen simulating web requests, Flux Capacitor automatically substitutes `{...}` placeholders in the request path using\nresults from previous steps:\n\n- The result of the **first `when...()` step** is saved when `.andThen()` is called.\n- If a subsequent path (e.g. `/games/{gameId}/buy`) contains placeholders, Flux Capacitor will:\n- Attempt to replace each placeholder (like `{gameId}`) with the string value of a previously returned result.\n- Track all resolved placeholders across steps. This allows chaining:\n\n[//]: # (@formatter:off)\n```java\ntestFixture.whenPost(\"/games\", \"/game/game-details.json\") // returns gameId\n     .andThen()\n     .whenPost(\"/games/{gameId}/buy\") // uses gameId, returns orderId\n     .andThen()\n     .whenPost(\"/games/{gameId}/refund/{orderId}\"); // uses gameId from step 1, orderId from step 2\n```\n[//]: # (@formatter:on)\n\nSubstitutions are based purely on the order of results, not types or field names. The `.toString()` value of each result\nis used to fill the next unresolved placeholder.\n\nThis keeps tests expressive and avoids boilerplate, especially for flows that involve chained resource creation (e.g.\n`POST /games → POST /games/{gameId}/buy → ...`).\n\n---\n\n## Outbound Web Requests\n\nFlux Capacitor provides a unified API for sending HTTP requests through the `WebRequestGateway`.\n\nUnlike traditional HTTP clients, Flux logs outbound requests as `WebRequest` messages. These are then handled by:\n\n- A **local handler** that tracks requests if the URL is **relative**, or\n- A **connected remote client or proxy**, if the URL is **absolute**.\n\n### Sending a Request\n\n```java\nWebRequest request = WebRequest.get(\"https://api.example.com/data\")\n        .header(\"Authorization\", \"Bearer token123\")\n        .build();\n\nWebResponse response = FluxCapacitor.get()\n        .webRequestGateway().sendAndWait(request);\n\nString body = response.getBodyString();\n```\n\n\u003e ✅ All outbound traffic is logged and traceable in the Flux platform.\n\n### Asynchronous and Fire-and-Forget\n\nYou can send requests asynchronously:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.get().webRequestGateway()\n        .send(request)\n        .thenAccept(response -\u003e log\n              .info(\"Received: {}\",response.getBodyString()));\n```\n[//]: # (@formatter:off)\n\nOr fire-and-forget:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.get().webRequestGateway()\n        .sendAndForget(Guarantee.STORED, request);\n```\n[//]: # (@formatter:on)\n\n### Relative vs Absolute URLs\n\nFlux supports both local and remote handling:\n\n- **Absolute URLs** (e.g., `https://...`): The request is forwarded via the Flux **Web Proxy** and executed externally.\n- **Relative URLs** (e.g., `/internal/doSomething`): The request is routed to a handler within another connected Flux\n  application.\n\nThis allows decoupled request-response workflows across services and environments.\n\n### Isolating Traffic with `consumer`\n\nYou can specify a `consumer` in the request settings:\n\n```java\nWebRequestSettings settings = WebRequestSettings.builder()\n        .consumer(\"external-api-xyz\")\n        .timeout(Duration.ofSeconds(5))\n        .build();\n```\n\nWhen set, the Flux Web Proxy will isolate this request in its own internal processing pipeline. This is useful when you:\n\n- Want to isolate third-party integrations (e.g., API rate limits)\n- Need different retry or error handling strategies per destination\n- Want fault isolation between outgoing endpoints\n\n### Mocking External Endpoints in Tests\n\nFlux makes it easy to test full workflows—including outbound `WebRequest` calls—by **mocking** the response during\ntests:\n\n```java\nstatic class EndpointMock {\n    @HandleGet(\"https://api.example.com/1.1/locations\")\n    WebResponse handleLocations() {\n        return WebResponse.builder()\n                .header(\"X-Limit\", \"100\")\n                .payload(\"/example-api/get-locations.json\")\n                .build();\n    }\n}\n\n@Test\nvoid testGetLocations() {\n    TestFixture.create(new EndpointMock())\n            .whenGet(\"https://api.example.com/1.1/locations\")\n            .\u003cList\u003cExampleLocation\u003e\u003eexpectResult(r -\u003e r.size() == 2);\n}\n```\n\nYou can match requests by:\n\n- Method and URL\n- Headers, body, or any other property\n\n\u003e ✅ This gives you **full end-to-end test coverage**, even when integrating with external APIs.\n\n---\n\n### Summary\n\n- ✅ Use `WebRequest` for centralized, traceable outbound HTTP calls.\n- ✅ Automatically routes to a proxy or local handler depending on URL.\n- ✅ Supports timeouts, consumers, and structured request settings.\n- ✅ Easily mock remote endpoints for testing full business flows.\n\n---\n\n## Metrics Messages\n\nFlux Capacitor supports a built-in message type for metrics: `MessageType.METRICS`.  \nThese messages provide a powerful way to observe and trace system behavior across clients, handlers, and infrastructure.\n\nMetrics messages are:\n\n- Lightweight, structured, and traceable\n- Logged like any other message\n- Routable to handlers (via `@HandleMetrics`)\n- Stored by default for **1 month** due to volume\n\n---\n\n### Publishing Metrics\n\nYou can publish metrics manually using the `FluxCapacitor.publishMetrics(...)` method:\n\n```java\nFluxCapacitor.publishMetrics(new SystemMetrics(\"slowProjection\", \"thresholdExceeded\"));\n```\n\nThis emits a structured metrics message to the metrics topic.\n\nAll metrics are wrapped in a regular `Message`, so you can include metadata or delivery guarantees:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.get()\n    .metricsGateway()\n    .publish(new MyMetric(\"foo\"), \n             Metadata.of(\"critical\",\"true\"),\n             Guarantee.STORED);\n```\n[//]: # (@formatter:off)\n\n---\n\n### Automatic Metrics from Clients\n\nMany metrics are automatically emitted by the Flux Java client:\n\n- **Connect / Disconnect events** when clients (re)connect\n- **Tracking updates** (throughput, handler times, latency)\n- **Search / state / document store operations**\n- **Web request round-trip timings**\n\nThese are particularly helpful in troubleshooting or auditing system performance.\n\n---\n\n### Consuming Metrics\n\nYou can treat metrics like any other message type:\n\n```java\n\n@HandleMetrics\nvoid on(MetricEvent event) {\n    log.debug(\"Observed metric: {}\", event);\n}\n```\n\nUse this to power custom dashboards, counters, diagnostics, or trigger alerts.\n\n---\n\n### Disabling Metrics\n\nTo reduce noise or overhead, you can selectively disable automatic metrics:\n\n#### Disable per handler or batch using an interceptor\n\n```java\n\n@Consumer(handlerInterceptors = DisableMetrics.class)\npublic class SilentHandler {\n    @HandleEvent\n    void on(MyEvent event) { ... }\n}\n```\n\nYou can also use `batchInterceptors` to disable metrics for an entire consumer instance.\n\n#### Disable globally per client\n\nIf you're instantiating a `WebSocketClient`, you can pass `disableMetrics = true` via the client config.\n\n#### Use programmatic interceptors\n\nIf needed, you can suppress metric dispatch programmatically using:\n\n[//]: # (@formatter:off)\n```java\nAdhocDispatchInterceptor.runWithAdhocInterceptor(() -\u003e {\n    // your code here\n}, (message, messageType, topic) -\u003e null, MessageType.METRICS);\n```\n[//]: # (@formatter:on)\n\n---\n\n### Common Use Cases\n\n- **Audit debugging**: trace which handler caused a slowdown\n- **Observability**: track real-time stats like search throughput\n- **Dashboards**: expose per-entity or per-consumer metrics\n- **Trigger alerting**: when retries or handler delays exceed thresholds\n\nMetrics messages provide lightweight hooks into system behavior — use them for visibility without overhead.\n\n---\n\n## Domain Modeling\n\nFlux Capacitor allows you to model the state of your domain using entities that evolve over time by applying updates.\nThese entities — such as users, orders, etc. — maintain state and enforce invariants through controlled updates,\ntypically driven by commands.\n\n### Defining the Aggregate Entity\n\nTo define a stateful domain object, annotate it with `@Aggregate`:\n\n```java\n\n@Aggregate\n@Builder(toBuilder = true)\npublic record UserAccount(@EntityId UserId userId,\n                          UserProfile profile,\n                          boolean accountClosed) {\n}\n```\n\nThis `UserAccount` class models an aggregate with state such as `profile` and `accountClosed`. Each entity may contain a\nfield\nannotated with `@EntityId` that acts as a unique identifier. For aggregates, this is optional — the aggregate itself is\ntypically loaded using `FluxCapacitor.loadAggregate(id)`.\n\nAn **aggregate** is a specialized root entity that serves as an entry point into a domain model. It may contain nested\nchild entities (modeled via `@Member`), but represents a single unit of consistency.\n\n### 💡 **Tip:** Use strongly typed identifiers\n\nWhile `@EntityId` can be placed on any object (e.g. a `String` or `UUID`), consider using a strongly typed `Id\u003cT\u003e` class\ninstead. This lets you:\n\n- Enforce consistent ID prefixes (e.g. `\"user-\"`)\n- Avoid collisions between entities with the same functional ID\n- Enable case-insensitive matching (optional)\n- Retain type information for safer entity loading and deserialization\n\n```java\nclass UserId extends Id\u003cUserAccount\u003e {\n    public UserId(String value) {\n        super(value, \"user-\");\n    }\n}\n\n@Aggregate\npublic record UserAccount(@EntityId UserId userId) {\n}\n```\n\nNow you can easily load the entity via:\n\n```java\nEntity\u003cUserAccount\u003e user = FluxCapacitor.loadAggregate(new UserId(\"1234\"));\n```\n\n---\n\n### Applying Updates to entities\n\nEntities evolve in response to **updates** — typically the payload of a command. Updates define what change should\nhappen and contain the logic to validate and apply those changes.\n\nHere’s an example using two commands — one to create a user, and another to update their profile.\n\n```java\n\npublic record CreateUser(UserId userId,\n                         UserProfile profile) {\n\n    @AssertLegal\n    void assertNotExists(UserAccount current) {\n        throw new IllegalCommandException(\"Account already exists\");\n    }\n\n    @Apply\n    UserAccount apply() {\n        return new UserAccount(userId, profile, false);\n    }\n}\n```\n\nThis update creates a new `UserAccount` entity after checking that no user with the same ID currently exists.\n\n```java\n\npublic record UpdateProfile(UserId userId,\n                            UserProfile profile) {\n\n    @AssertLegal\n    void assertExists(@Nullable UserAccount current) {\n        if (current == null) {\n            throw new IllegalCommandException(\"Account not found\");\n        }\n    }\n\n    @AssertLegal\n    void assertAccountNotClosed(UserAccount current) {\n        if (current.isAccountClosed()) {\n            throw new IllegalCommandException(\"Account is closed\");\n        }\n    }\n\n    @Apply\n    UserAccount apply(UserAccount current) {\n        return current.toBuilder().profile(profile).build();\n    }\n}\n```\n\nThis update first ensures the user exists and their account isn’t closed before applying the change.\n\n\u003e **Note**: Handler method parameters (like `UserAccount current`) are only injected if non-null. Use `@Nullable`\n\u003e to allow for missing values.\n\n---\n\n### Intercepting and Transforming Updates\n\nIn addition to applying and validating updates, you can also **intercept** them *before* they reach the legal or apply\nphase.\n\nUse `@InterceptApply` to:\n\n- Suppress updates that are irrelevant or no-ops\n- Rewrite updates that would otherwise be invalid\n- Split a single update into multiple smaller updates\n\n```java\n\n@InterceptApply\nObject ignoreNoChange(UserAccount current) {\n    if (current.getProfile().equals(profile)) {\n        return null; // suppress update, nothing to change\n    }\n    return this;\n}\n```\n\nYou can even rewrite the update entirely:\n\n```java\n\n@InterceptApply\nUpdateProfile downgradeCommand(CreateUser command, UserAccount current) {\n    //the current account could be injected, hence it already exists\n    return new UpdateProfile(command.getUserId(), command.getProfile());\n}\n```\n\nOr expand a bulk command into many atomic ones:\n\n```java\n\n@InterceptApply\nList\u003cCreateTask\u003e expandBulk(BulkCreateTasks bulk) {\n    return bulk.getTasks();\n}\n```\n\n\u003e 🔁 Flux will apply interceptors **recursively** until the final update no longer changes.\n\n---\n\n### Invocation Order\n\nThe update lifecycle flows as follows:\n\n1. **Intercept** using `@InterceptApply`\n2. **Assert legality** using `@AssertLegal`\n3. **Apply changes** using `@Apply`\n\nThis allows you to rewrite or suppress updates *before* they’re validated or stored — a powerful tool for protecting\ndata integrity and simplifying update logic.\n\n### Return Values\n\n`@InterceptApply` supports flexible return types:\n\n- `null` or `void` → suppress the update\n- `this` → no change\n- A **new update object** → rewrites the update\n- A **Collection**, `Stream`, or `Optional` → emits zero or more new updates\n\n\u003e 📌 You typically don’t need to intercept just to avoid storing no-ops. Instead, annotate the `@Aggregate` or `@Apply`\n\u003e method with `eventPublication = IF_MODIFIED` to avoid persisting unchanged state.\n\n---\n\n### Summary\n\n| Annotation        | Purpose                                        | Phase        |\n|-------------------|------------------------------------------------|--------------|\n| `@InterceptApply` | Rewrite, suppress, or expand updates           | Pre-check    |\n| `@AssertLegal`    | Validate preconditions                         | Validation   |\n| `@Apply`          | Apply state transformation                     | Execution    |\n\nTogether, these annotations offer full control over your entity update lifecycle — with a clean, declarative style.\n\n---\n\n### Why Keep Logic in the Updates?\n\nWhile it’s possible to implement domain logic inside entities, this is **generally discouraged**. Instead, it is best\npractice to define business logic directly inside **command payloads** — the updates themselves.\n\nThis update-driven approach has several advantages:\n\n- **Behavior stays with the update** – each update class (e.g. `CreateUser`, `UpdateProfile`) encapsulates its own\n  validation and transformation logic.\n- **Entities stay focused** – entities remain concise, responsible only for maintaining state and enforcing invariants.\n- **Easy feature cleanup** – removing an update class cleanly disables that feature.\n- **Traceable domain behavior** – it’s clear what each update does and how it affects the system.\n\n---\n\n### Alternative: Logic in the Entity\n\nAlthough possible, modeling behavior inside the aggregate can quickly become unmanageable. Here's a glimpse of what that\nlooks like:\n\n```java\n\n@Aggregate\n@Builder(toBuilder = true)\npublic record UserAccount(@EntityId UserId userId,\n                          UserProfile profile,\n                          boolean accountClosed) {\n\n    @AssertLegal\n    static void assertNotExists(CreateUser update, @Nullable UserAccount user) {\n        if (user != null) {\n            throw new IllegalCommandException(\"Account already exists\");\n        }\n    }\n\n    @Apply\n    static UserAccount create(CreateUser update) {\n        return new UserAccount(update.getUserId(), update.getProfile(), false);\n    }\n\n    @AssertLegal\n    static void assertExists(UpdateProfile update, @Nullable UserAccount user) {\n        if (user == null) {\n            throw new IllegalCommandException(\"Account does not exist\");\n        }\n    }\n\n    @AssertLegal\n    void assertAccountNotClosed(UpdateProfile update) {\n        if (accountClosed) {\n            throw new IllegalCommandException(\"Account is closed\");\n        }\n    }\n\n    @Apply\n    UserAccount update(UpdateProfile update) {\n        return toBuilder().profile(update.getProfile()).build();\n    }\n}\n```\n\nIn this model, the `UserAccount` aggregate handles all validation and transformation logic. Over time, this\ncentralization leads to bloat and tight coupling — especially in larger systems with many features.\n\n---\n\n### Mixing strategies\n\nFlux Capacitor allows **mixed approaches**. You can define:\n\n- `@AssertLegal` methods on the command payload\n- `@Apply` methods inside the entity\n- or vice versa\n\nJust keep in mind: logic that lives in updates is **easier to test, extend, and remove**.\n\n---\n\n## Applying Updates to Entities\n\nTo change the state of an entity, use `FluxCapacitor.loadAggregate(...)` to retrieve the aggregate and apply updates to\nit.\n\nHere's a basic example of a command handler applying a `CreateUser` update:\n\n```java\npublic class UserCommandHandler {\n    @HandleCommand\n    void handle(CreateUser command) {\n        FluxCapacitor.loadAggregate(command.getUserId(), UserAccount.class).assertAndApply(command);\n    }\n}\n```\n\nThis loads the `UserAccount` entity by ID and applies the `CreateUser` command. Internally, Flux Capacitor will:\n\n1. **Rehydrate** the entity using stored events or snapshots\n2. **Run all `@AssertLegal` methods** to verify preconditions\n3. **Call the `@Apply` method** to produce a new entity state\n4. **Persist the event** in the aggregate event log (if event sourcing is active)\n5. **Publish a domain event** (using the applied payload, unless overridden)\n\nThis example used `assertAndApply()`, which combines two steps:\n\n[//]: # (@formatter:off)\n```java\n.aggregate(...)\n  .assertLegal(update)\n  .apply(update);\n```\n[//]: # (@formatter:on)\n\nThis style is recommended if you want to ensure validations happen before the entity changes state.\n\n---\n\n## Nested Entities\n\nFlux Capacitor allows aggregates to contain nested entities — for example, users with authorizations or orders with line\nitems. These nested entities can be added, updated, or removed using the same `@Apply` pattern used for root aggregates.\n\nTo define a nested structure, annotate the collection or field with `@Member`:\n\n```java\n\n@Aggregate\n@Builder(toBuilder = true)\npublic record UserAccount(@EntityId UserId userId,\n                          UserProfile profile,\n                          boolean accountClosed) {\n\n    @Member\n    List\u003cAuthorization\u003e authorizations;\n}\n```\n\nChild entities must define their own `@EntityId`:\n\n```java\n\npublic record Authorization(@EntityId AuthorizationId authorizationId,\n                            Grant grant) {\n}\n```\n\n### Adding a Child Entity\n\nTo add a nested entity like `Authorization`, simply return a new instance from the `@Apply` method:\n\n```java\n\npublic record AuthorizeUser(AuthorizationId authorizationId,\n                            Grant grant) {\n\n    @Apply\n    Authorization apply() {\n        return new Authorization(authorizationId, grant);\n    }\n}\n```\n\nThe `UserAccount` aggregate is automatically updated to include this new child entity.\n\n### Removing a Child Entity\n\nTo remove a nested entity, return `null` from the `@Apply` method:\n\n```java\n\npublic record RevokeAuthorization(AuthorizationId authorizationId) {\n\n    @AssertLegal\n    void assertExists(@Nullable Authorization authorization) {\n        if (authorization == null) {\n            throw new IllegalCommandException(\"Authorization not found\");\n        }\n    }\n\n    @Apply\n    Authorization apply(Authorization authorization) {\n        return null;\n    }\n}\n```\n\nFlux will automatically prune the child entity with the given `authorizationId`.\n\n\u003e ⚠️ **Note:** When a child entity is added, updated, or removed using an `@Apply` method, Flux Capacitor will:\n\u003e\n\u003e - Automatically **locate the parent aggregate**\n\u003e - Apply the update to the child entity\n\u003e - And return a **new instance of the parent** (if it's immutable), with the updated child state included\n\u003e\n\u003e This allows you to use immutable models (e.g. Java records or classes with Lombok’s `@Value`) without extra\n\u003e boilerplate.\n\n---\n\n### Loading Entities and Aggregates\n\nFlux Capacitor supports a flexible and powerful approach to loading aggregates and their internal entities using\n`FluxCapacitor.loadAggregateFor(...)` and `FluxCapacitor.loadEntity(...)`.\n\n---\n\n#### `loadEntity(entityId)`\n\nUse this method to load a specific entity **without needing to know the aggregate root** it belongs to. This allows APIs\nto remain focused and concise—for example:\n\n```java\npublic record CompleteTask(TaskId taskId) {\n}\n```\n\nWith Flux Capacitor, you can handle this using:\n\n[//]: # (@formatter:off)\n```java\nFluxCapacitor.loadEntity(taskId).assertAndApply(new CompleteTask(taskId));\n```\n[//]: # (@formatter:on)\n\nEven if the `Task` is deeply nested within a `Project` or other parent aggregate, this method works because of the\n**entity relationship tracking** automatically maintained by Flux Capacitor.\n\nAdditional behavior:\n\n- If **multiple entities** match the given ID, the one with the **most recently added relationship** is used.\n- The **entire aggregate** containing the entity is loaded, ensuring consistency.\n- The returned `Entity\u003cT\u003e` provides methods like `assertAndApply(...)` or `apply(...)`, and includes a reference to the\n  enclosing aggregate root.\n\n\u003e This enables true *location transparency* for commands and queries: you don’t need to know or pass along the full\n\u003e aggregate path.\n\n#### 🔁 Finding All Aggregates for an Entity\n\nIn some scenarios, an entity may be **referenced by multiple aggregates**—for example, when using shared reference\ndata (e.g. a `Currency`, `Role`, or `Label`). If such an entity is updated, you might want to update *all* aggregates\nthat reference it.\n\nTo retrieve all aggregates that currently include a given entity ID:\n\n```java\nMap\u003cString, Class\u003c?\u003e\u003e aggregates = FluxCapacitor.get()\n        .aggregateRepository()\n        .getAggregatesFor(myEntityId);\n```\n\nThis returns a map of aggregate IDs and their types.\n\n\u003e ⚠️ **Caution:** This pattern should only be used if you know the number of associated aggregates will remain small and\n\u003e bounded. If many aggregates accumulate over time, this lookup can grow unbounded and lead to performance issues.\n\nWhen used responsibly, this enables patterns like:\n\n[//]: # (@formatter:off)\n```java\n// Rerender or update every Project referencing a shared Tag\nfor(Map.Entry\u003cString, Class\u003c?\u003e\u003e entry : FluxCapacitor.get()\n                .aggregateRepository().getAggregatesFor(tagId).entrySet()) {\n        FluxCapacitor.loadAggregate(entry.getKey(), entry.getValue())\n        .apply(new RefreshTag(tagId));\n}\n```\n[//]: # (@formatter:on)\n\nThis approach can help keep derived or denormalized data consistent across aggregates.\n\n---\n\n#### `loadAggregateFor(entityId)`\n\nUse this method to retrieve the **aggregate root** that currently contains the specified entity ID.\n\n```java\nEntity\u003cMyAggregate\u003e aggregate = FluxCapacitor\n        .loadAggregateFor(\"some-entity-id\");\n```\n\nBehavior:\n\n- If the ID matches a **child entity**, the enclosing aggregate is returned.\n- If the ID refers to an **aggregate root**, that root is returned directly.\n- If no aggregate exists, an **empty aggregate** of type `Object` is returned. This enables bootstrapping a new one by\n  applying events.\n\n\u003e Use `loadAggregateFor(entityId, Class\u003cT\u003e)` when you need type safety or to avoid relying on inference.\n\n### Alternative Entity Identifiers\n\nFlux Capacitor supports alternative ways to reference an entity using the `@Alias` annotation. This is especially useful\nwhen:\n\n- The entity needs to be looked up using a secondary identifier (e.g. an email address or external ID)\n- An entity wants to reference another entity without identifier collisions\n\n#### Lookup via Aliases\n\nAliases are used when:\n\n- Loading an aggregate or entity using `FluxCapacitor.loadAggregateFor(alias)` or `FluxCapacitor.loadEntity(alias)`.\n- Calling `Entity#getEntity(Object id)` on a parent entity.\n\n\u003e If multiple entities share the same alias, behavior is undefined—avoid alias collisions unless intentional.\n\n#### Supported Targets\n\nYou can place `@Alias` on:\n\n- **Fields** (e.g., `@Alias String externalId`)\n- **Property methods** (e.g., `@Alias String legacyId()`)\n\nIf the property is a **collection**, all non-null elements are treated as aliases. If the value is `null` or an empty\ncollection, it is ignored.\n\n#### Prefix and Postfix\n\nTo avoid clashes between IDs in different domains, use the optional `prefix` and `postfix` parameters:\n\n```java\n\n@Alias(prefix = \"email:\")\nString email;\n\n@Alias(postfix = \"@external\"","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluxzero-io%2Fflux-capacitor-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffluxzero-io%2Fflux-capacitor-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluxzero-io%2Fflux-capacitor-client/lists"}