-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture Overview
The single authoritative source for this material is
.claude/ARCHITECTURE.md
in the repository. This page is the navigable summary.
Evento implements the RECQ pattern: reactive, event-driven commands and queries. Two runtime components:
| Component | Role |
|---|---|
| Evento Server | Central broker: bundle registry, event store, command routing, query routing, performance store |
| Evento Bundle | Application unit hosting one or more domain components; connects to the server over TCP |
Bundles register their handler types at startup. All inter-bundle traffic flows through the broker — bundles never talk directly to each other. That single choke point is what makes the whole system observable: the server sees every command, query and event, which is where the GUI's interaction graph, catalog and performance model come from.
evento-framework/
├── evento-transport-api/ Wire SPI: sealed Message records, Codec, Transport, state machine, reconnect
├── evento-transport-netty/ Netty impl: CBOR pipeline, chunking, TLS, heartbeat, backpressure
├── evento-common/ Shared domain types: annotations, messaging types, consumer SPI
├── evento-bundle/ Bundle runtime: client, consumer engines, component managers
├── evento-server/ Server runtime: bus lifecycle, event store, REST API, Spring Boot app
├── evento-consumer-state-store/
│ └── evento-consumer-state-store-jdbc/ JDBC impls (Postgres + MySQL) for the consumer SPIs
├── evento-gui/ Web GUI (React, static assets)
├── evento-lab/ In-process integration tests (single-bundle RTT, failure matrix, …)
└── evento-lab-microservices/ Multi-bundle integration tests (RECQ microservices scenario)
├── evento-lab-ms-api/
├── evento-lab-ms-command/
├── evento-lab-ms-query/
├── evento-lab-ms-saga/
├── evento-lab-ms-observer/
└── evento-lab-ms-it/
Dependency direction. transport-api knows nothing about business types; transport-netty
knows nothing about Evento domain concepts; evento-server knows nothing about the contents of a
payload. Business classes are loaded only inside bundles.
Versions are centralised in the root build.gradle under allprojects { ext { … } }. As of
2.4.0: Java toolchain 25, Jackson 2.22.1, Netty 4.2.16.Final, Flyway 13.0.0,
HikariCP 7.1.0, Micrometer 1.13.6; Spring Boot 4.1.0 is applied in
evento-server/build.gradle.
Framing, codec, connection state, reconnect. See Wire Protocol.
| Class | Where | Role |
|---|---|---|
Transport |
api | SPI: send(Message), sendRaw(byte[]), onFrame, onStateChange, close()
|
TransportServer |
api | Server SPI: bind(port), onChildTransport, stop()
|
Frame |
api | Parsed Message plus the retained raw wire bytes (enables zero-copy relay) |
ConnectionStateMachine |
api |
AtomicReference-backed; only legal transitions permitted |
ExponentialBackoffWithJitter |
api |
min(maxBackoff, base × 2^attempt) × (1 ± jitter); base 500 ms, max 30 s, jitter 0.2 |
MessageTypeRegistry |
api |
byte tag ↔ Class<? extends Message> whitelist |
JacksonCborCodec |
api | Default Codec implementation |
InMemoryTransport |
api | Test double; simulateDisconnect() yields DISCONNECTED
|
NettyClientTransport |
netty | Client-side channel + reconnect |
NettyServerTransport |
netty | Accept loop; each accepted channel becomes a ServerChildTransport
|
NettyTransportConfig |
netty | Optional SslContext (prepends SslHandler when set) |
Routing, correlation, registries, QoS. See Server Bus.
Connection supervision, correlation tracking, inbound dispatch. See Bundle Client.
Projector / saga / observer run loops over five focused SPIs. See Consumer Engines.
These are deliberate and interlocking. Undoing one usually breaks something several layers away, so they are not to be changed without discussion.
1. The server is payload-agnostic. Request / Response / Notification carry
payloadType: String plus payload: byte[]. The server routes by string; bundles hold the business
class loader and deserialize locally. No business class loading server-side — which is why the server
can broker bundles it was never compiled against.
2. Sealed Message + MessageTypeRegistry. Adding a wire type means extending permits and
registering a byte tag. The compiler then enforces exhaustive dispatch everywhere. There is no
"accept anything under this package" escape hatch.
3. Two correlation maps, two purposes. CorrelationStore holds server-initiated requests with a
CompletableFuture to await. ForwardingTable holds bundle-A → server → bundle-B relays — no future,
it just remembers where the response goes. Disconnect drains via drainByDestination, not
drainInvolving, so originator-side entries survive for reconnect delivery.
4. One event stream, sealed BusEvent. Replaces v1's four parallel listener lists. Subscribers
pattern-match with switch expressions.
5. Lifecycle is explicit. BusLifecycle.start(port) / stop(Duration deadline); Spring's
BusStarter owns @PostConstruct / @PreDestroy. Constructors do no work — no threads, no I/O.
6. ConnectionState.canSend() is true for CONNECTED and DEGRADED. DEGRADED is advisory
backpressure from the Netty high-water mark; the TCP socket is alive. Responses are critical-path and
must not be dropped because a channel is momentarily busy.
7. No System.exit anywhere. BundleClient.start() returns a CompletableFuture that fails with
the rejection reason. The caller decides what to do about it.
8. Virtual-thread executors, not StructuredTaskScope, for long-running engines. The stable
JDK 25 StructuredTaskScope API is owner-thread-scoped — close auto-joins — which does not fit an
engine that starts in one method and stops in another. Short-lived batch fan-out inside a run()
can still open a scope locally.
9. The framework does not manage instance lifecycle. It emits performance metrics only
(PerformanceInvocationsMessage, PerformanceServiceTimeMessage). Scaling and instance lifecycle
belong to the external orchestrator (k8s, Nomad).
10. MessageHandlerInterceptor methods are all default. All 24 of them — an interface-segregation
fix, so implementors override only the hooks they need.
The aggregate command path, brokered by CommandBrokerHandler (com.evento.server.es), which
subscribes to BusEvent.BundleDiscovered and registers a LocalRequestHandler per
AggregateCommandHandler and service CommandHandler payload type:
- Acquire the optional distributed lock (
lockId ?? aggregateId) -
BrokerEventStore.fetchAggregateStory(aggregateId)→ event stream plus optional snapshot - Wrap
DomainCommandMessageintoDecoratedDomainCommandMessage(state + event stream attached) -
BusLifecycle.forward(...)to the aggregate's bundle -
BrokerEventStore.publishEvent(DomainEventMessage), plussaveSnapshotwhen due - Return the stored event to the caller
The service command path is the same flow without decoration, storing a ServiceEventMessage.
BrokerEventStore is a four-method interface, which is what decouples CommandBrokerHandler from the
concrete Spring JPA EventStore and lets integration tests supply an in-memory implementation.
Tests live at the boundary: integration tests use real TCP transports (NettyServerTransport +
BundleClient), not mocks. Mocks appear only as the in-memory transport test double. Two harnesses
support this — evento-lab for single-bundle scenarios and evento-lab-microservices for a full
multi-bundle RECQ scenario. See Building and Testing.
- Wire Protocol — the framing and message types this architecture rests on
- Contributing and Conventions — the code conventions that follow from these decisions
- Migrating from v1 — what this replaced
Evento Framework — Copyright 2020–2026 © Gabor Galazzo. Dual-licensed under AGPL-3.0 and a commercial licence.
This wiki documents the implementation; the repository is authoritative where the two disagree. Found something out of date? Open an issue.
Getting oriented
Internals
Operations
- Server Configuration
- Throughput and Capacity
- Observability
- Security Model
- Server REST API
- Troubleshooting
Project