Evento
Evento 3 is out: exactly-once, cluster-ready, tested to break

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.

1/6 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.

Aggregate Service Projector Saga Observer

TodoListAggregate

6 handlers · TodoListCreateCommand

TodoListAggregate.java:21

UserAggregate

1 handler · UserRegisterCommand

UserAggregate.java:15

ErpService

1 handler · ErpUserActivityRegisterCommand

ErpService.java:14

NotificationService

1 handler · NotificationSendCommand

NotificationService.java:13

TodoListProjector

6 handlers · TodoListDeletedEvent

TodoListProjector.java:60

UserDirectoryProjector

2 handlers · UserRegisteredEvent

UserDirectoryProjector.java:30

ErpTodoListActivityRegistrationSaga

6 handlers · TodoListDeletedEvent

ErpTodoListActivityRegistrationSaga.java:28

TodoListNotificationObserver

1 handler · TodoListTodoCheckedEvent

TodoListNotificationObserver.java:26

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.

TodoListAggregate

160 / 250 Bottleneck

TodoListProjector

160 / 333 Bottleneck

TodoListNotificationObserver

160 / 500 Bottleneck

NotificationService

160 / 250 Bottleneck

ErpTodoListActivityRegistrationSaga

160 / 125 Bottleneck

ErpService

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

Read the research

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
The live todo demo: a populated list with checked todos, running on demo.eventoframework.com

Build one.

Free to run in production, every feature public, client libraries under Apache-2.0.