Enforced,not hoped for.
Let any service call any other and you get a big ball of mud. In Evento, Java components talk only through commands, events and queries, and the runtime enforces it.
TodoListCheckTodoCommand
Someone checks the last todo in a list. The Invoker sends a Command.
- Invoker
- Aggregate
- Service
- Projector
- Projection
- Saga
- Observer
- Invoker
- Aggregate
- Service
- Projector
- Projection
- Saga
- Observer
01 · Architecture
Exactly seven components.
That's the whole vocabulary. Seven kinds of component, three kinds of message, and no other way to talk. Pick one below to see what it's allowed to do.
-
Invoker
The door. HTTP requests, a CLI, a cron trigger: whatever comes from outside enters here, as Commands and Queries.
-
Aggregate
Decides. It rebuilds its state from its own events, says yes or no to a Command, and records what happened.
-
Service
Deals with what you don't own: e-mail, a payment provider, any outside system. No state of its own, and it need not publish anything.
-
Projector
Reads the events in order and builds the read model. It may ask questions, but it never gives orders.
-
Projection
Answers Queries from the read model the Projector built. Stateless, so it scales at will.
-
Saga
Runs long processes. Follows the events in order, remembers where it is, and sends the next Command.
-
Observer
Reacts to one event, and forgets it. No state, no ordering to keep, and free to send Commands and Queries.
Just annotated Java.
A component is a class with an annotation. No deployment descriptors, no broker configuration. Use Spring, Micronaut, Quarkus, or nothing at all.
Read the RECQ patterns@Invoker
public class TodoListInvoker extends InvokerWrapper {
@InvocationHandler
public String createTodoList(String name, String user) throws Exception {
var identifier = "TDLS_" + UUID.randomUUID();
getCommandGateway()
.send(new TodoListCreateCommand(identifier, name),
toUserMetadata(user))
.get(30, TimeUnit.SECONDS);
return identifier;
}
@InvocationHandler
public CompletableFuture<TodoListView> findTodoListByIdentifier(String id) {
return getQueryGateway()
.query(new TodoListViewFindByIdentifierQuery(id))
.thenApply(Single::getData);
}
}
@Saga(version = 1)
public class ErpTodoListActivityRegistrationSaga {
@SagaEventHandler(init = true, associationProperty = "identifier")
public SagaState on(TodoListCreatedEvent event) { /* start tracking */ }
@SagaEventHandler(associationProperty = "identifier")
public void on(TodoListTodoCheckedEvent event, SagaState state,
CommandGateway commandGateway) throws Exception {
if (event.isAllChecked()) {
commandGateway.send(new ErpUserActivityRegisterCommand(
"TodoList", event.getIdentifier(), state.getUsages()
)).get();
}
}
}
@Projection
public class TodoListProjection {
@QueryHandler
public Single<TodoListView> handle(TodoListViewFindByIdentifierQuery q) {
return Single.of(repository.findById(q.getIdentifier())
.map(TodoList::toView).orElseThrow());
}
@QueryHandler
public Multiple<TodoListListItemView> handle(TodoListListItemViewSearchQuery q) {
return Multiple.of(repository.search("%" + q.getNameLike() + "%",
PageRequest.of(q.getPage(), q.getSize()))
.map(TodoList::toListItemView).toList());
}
}
@Projector(version = 2)
public class TodoListProjector {
@EventHandler
public void on(TodoListTodoCheckedEvent event, Metadata metadata,
Instant timestamp, ProjectorStatus status) {
var list = repository.findById(event.getIdentifier()).orElseThrow();
var todo = list.todo(event.getTodoIdentifier());
todo.setCompletedAt(timestamp.atZone(ZoneId.systemDefault()));
todo.setCompletedBy(metadata.get("user"));
repository.save(list);
if (status.isHeadReached()) sse.notifyListChanged(list.getId());
}
}
@Aggregate
public class TodoListAggregate {
@AggregateCommandHandler
public TodoListTodoAddedEvent handle(TodoListAddTodoCommand command,
TodoListAggregateState state) {
Assert.isTrue(state.getTodos().size() < 5,
"Error: Todo list is full");
return new TodoListTodoAddedEvent(command.getIdentifier(),
command.getTodoIdentifier(), command.getContent());
}
@EventSourcingHandler
public void on(TodoListTodoAddedEvent event,
TodoListAggregateState state) {
state.getTodos().put(event.getTodoIdentifier(), false);
}
}
02 · Resilience
We killed the servers.
Anyone can promise exactly-once. We try to break it: on a three-continent lab, through rolling upgrades, and against the test suites of two production systems.
0
acknowledged commands lost or executed twice, and no aggregate forked, with the service outcome store on
19
chaos scenarios, among them servers killed and frozen, a region cut off, database failovers
3×3
contexts × replicas, with the lane holder killed mid-stream: every event applied once and in order
- About 3,900 commands a second
- Stored by one server on a 4-core machine, p99 about 60 ms.
- 7 ms median round trip
- A command on one machine, timed phase by phase from arrival to reply.
03 · Reactive Manifesto
Four traits, each one enforced.
The Manifesto tells you what to want. RECQ tells you how to build it, and Evento holds you to it.
Responsive
Every component declares what response time it can promise. Designed in, not hoped for.
Resilient
A failure stays inside its component. State can be rebuilt from the log. A bad event goes to a dead queue, and the consumer keeps going.
Elastic
Scale commands and reads separately. When a Projector or a Saga needs more throughput, split the stream into contexts.
Message Driven
Components never call each other. Everything goes through the Message Gateway or the System State Store.
The Evento GUI
Documentation that can't lie.
The catalog is generated from your code when your bundles start. There's no wiki to keep up to date, and no diagram to go stale.
Rename a command and the docs follow on the next deploy. Every handler links to its source line on GitHub.
Todo
Todo
User
User
Erp
Erp
Notification
Notification
Todo
Todo
User
User
Erp
Erp
Todo
Todo
Follow any message
Trace it through the whole system, or zoom out to the full application graph.
Find the bottleneck first
Give it a workload and it shows where the system will choke, before you buy the hardware. Move the slider: past its capacity, a station passes on only what it serves.
Todo
160 / 250 Bottleneck
Todo
160 / 333 Bottleneck
Todo
160 / 500 Bottleneck
Notification
160 / 250 Bottleneck
Erp
160 / 125 Bottleneck
Erp
125 / 200 Bottleneck
Requests per second against capacity, with one server per station. Service times are illustrative: the console reads yours.
Available now
Evento 3
From your laptop to three continents. Every feature is public, and the client libraries are Apache-2.0.
Seven RECQ components
Annotated Java classes that announce themselves to the server. No deployment descriptors.
Exactly-once commands
Idempotency keys make a retry safe on whichever server it lands. Sagas, Projectors and Observers derive theirs automatically.
Aggregates that never fork
Every event carries its aggregate version under a unique index. Two commands deciding from the same state can never both be stored.
One site to three continents
Several servers on one PostgreSQL event store. Bundles fail over and fail back on their own, and get answered close to the caller.
Highly available consumers
Run a consumer in as many replicas as you like, per context. One holds the lane, another takes over when it goes.
Consumers that wait, not poll
The server tells a bundle when its events arrive. Updates flow at once, and an idle system costs the event store almost nothing.
Queries at any size
Streamed and paged answers with flow control, and read-your-writes when a query must see a command.
Payloads that evolve
Rename or reshape a payload. Upcasters read the old history, and the event store is never rewritten.
Secure by default
Production requires a bus token, TLS, a payload allowlist and real passwords. Single sign-on and audit trail for the console.
Privacy built in
Personal data is encrypted per person. Delete the key and it's gone from the whole event history.
Observable
A console generated from your code, OpenTelemetry tracing and Prometheus metrics.
Any Java stack
Plain Java, Spring, Micronaut or Quarkus, with test fixtures and an in-process broker. Java 25.
Born from research, proven in production.
7
component types, the well-formed cells of a trigger × state classification
235 kLOC
production manufacturing-execution system on RECQ, 206 components, anonymized census
2023
first peer-reviewed publication (QualITA’23) and founding master’s thesis
Use it live.
A real RECQ app and its server, running in the open. Check a todo, then open the console and watch your command go through.
Open the live demo
Build one.
Free to run in production, every feature public, client libraries under Apache-2.0.