-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
The full tutorial path lives at docs.eventoframework.com → Quick Start and the TodoList RECQ tutorial. This page is the condensed version, with the API shapes that actually appear in the repository's integration-test bundles.
The quickest path is Docker Compose — server plus Postgres:
version: '3.3'
services:
database:
image: 'postgres:latest'
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=evento
volumes:
- ./data/postgres:/var/lib/postgresql/data/
ports:
- "5433:5432"
evento-server:
image: 'eventoframework/evento-server:latest'
depends_on:
- database
environment:
- evento_cluster_name=evento-server
- evento_performance_capture_rate=0.1
- evento_telemetry_ttl=365
# GUI/API login (HTTP Basic) — override in real deployments
- spring_security_user_name=evento
- spring_security_user_password=secret
- spring_datasource_url=jdbc:postgresql://database:5432/evento
- spring_datasource_username=postgres
- spring_datasource_password=secret
ports:
- '3000:3000'
- '3030:3030'(from docker/evento-docker-compose/docker-compose.yaml)
Two ports, and the distinction matters:
| Port | Purpose |
|---|---|
| 3000 | HTTP — the GUI and the REST API (server.port) |
| 3030 | The Netty message bus that bundles connect to (evento.server.bus.port) |
The GUI is then at http://localhost:3000, behind HTTP Basic with the credentials above.
Change
spring_security_user_passwordbefore exposing the server anywhere. See Security Model.
EventoBundle.Builder.builder() is the entry point. A realistic Spring wiring — this is
evento-lab-microservices/evento-lab-ms-query:
@Configuration
public class EventoConfiguration {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public EventoBundle eventoApplication(
@Value("${evento.server.host}") String host,
@Value("${evento.server.port}") int port,
@Value("${evento.bundle.id}") String bundleId,
@Value("${evento.bundle.version}") long version,
@Value("${evento.bundle.repository-url:}") String repositoryUrl,
@Value("${evento.bundle.line-prefix:L}") String linePrefix,
BeanFactory factory) throws Exception {
return EventoBundle.Builder.builder()
.setBasePackage(LabMsQueryApplication.class.getPackage())
.setBundleId(bundleId)
.setBundleVersion(version)
.setRepositoryUrl(repositoryUrl)
.setLinePrefix(linePrefix)
.setEventoServerMessageBusConfiguration(new EventoServerMessageBusConfiguration(
new ClusterNodeAddress(host, port)))
.setInjector(factory::getBean)
.setConsumerEngineConfigBuilder(ConsumerEngineConfig::inMemory)
.start();
}
}with:
evento.server.host=localhost
evento.server.port=3030
evento.bundle.id=lab-ms-query
evento.bundle.version=1Three things are required — start() throws IllegalArgumentException otherwise:
basePackage, bundleId, and eventoServerMessageBusConfiguration.
Note there is no setEventoServerHost / setEventoServerPort. Host and port go through
setEventoServerMessageBusConfiguration(new EventoServerMessageBusConfiguration(new ClusterNodeAddress(host, port))).
setInjector(factory::getBean) is what lets your components be Spring beans with their own
dependencies injected.
The Builder is @Accessors(chain = true), so every field has a chained setter.
| Setter | Default | Purpose |
|---|---|---|
setBasePackage(Package) |
— (required) | Package scanned for components |
setBundleId(String) |
— (required) | Logical bundle identity |
setEventoServerMessageBusConfiguration(...) |
— (required) | Server address(es) |
setInstanceId(String) |
generated | Distinguishes instances of the same bundle |
setBundleVersion(long) |
1 |
Reported at registration |
setInjector(Function<Class<?>, Object>) |
— | Component instantiation (e.g. factory::getBean) |
setConsumerEngineConfigBuilder(BiFunction<…>) |
ConsumerEngineConfig::inMemory |
Consumer persistence wiring |
setCommandGatewayBuilder / setQueryGatewayBuilder
|
CommandGatewayImpl::new / QueryGatewayImpl::new
|
Gateway implementations |
setPerformanceServiceBuilder(...) |
RemotePerformanceService |
Performance reporting |
setTracingAgent(TracingAgent) |
no-op | Distributed tracing |
setMessageHandlerInterceptor(...) |
— | Cross-cutting hooks (e.g. transactions) |
setSssFetchSize(int) / setSssFetchDelay(int)
|
1000 / 1000
|
System-state-store fetch tuning |
setRepositoryUrl(String) / setLinePrefix(String)
|
"" / "L"
|
Clickable source links in the GUI |
setDescription(String) / setDetail(String)
|
"" |
Human-readable bundle metadata |
setObjectMapper(ObjectMapper) |
payload mapper | Payload serialization |
setComponentContexts(Class<?>, String...) |
— | Restrict a component to named contexts |
setStrictConfinement(boolean) |
false |
Fail start-up on gateway leaks — see Bundle Client § 5 |
addConsumerExecutor(ConsumerExecutor) |
— | Register a parallel-consumption executor |
setCheckpointMode(CheckpointMode) |
ON_START |
Global checkpoint semantics |
setComponentCheckpointMode(Class<?>, CheckpointMode) |
— | Per-component override |
setConsumerStatsInterval(Duration) |
30 s | How often consumer counters are pushed |
setOnEventoStartedHook(Consumer<EventoBundle>) |
no-op | Post-start callback |
All four component kinds, taken from evento-lab.
@Aggregate(snapshotFrequency = 5)
public class LabAggregate {
@AggregateCommandHandler(init = true)
OrderCreatedEvent handle(CreateOrderCommand cmd, LabAggregateState state) {
if (cmd.getOrderId() == null || cmd.getOrderId().isBlank()) {
throw new IllegalArgumentException("orderId is required");
}
return new OrderCreatedEvent(cmd.getOrderId(), cmd.getDescription(), cmd.getQuantity());
}
@EventSourcingHandler
LabAggregateState on(OrderCreatedEvent e, LabAggregateState state) {
if (state == null) state = new LabAggregateState();
state.setDescription(e.getDescription());
state.setQuantity(e.getQuantity());
return state;
}
@AggregateCommandHandler
OrderUpdatedEvent handle(UpdateOrderCommand cmd, LabAggregateState state) {
return new OrderUpdatedEvent(cmd.getOrderId(), cmd.getDescription(), cmd.getQuantity());
}
@EventSourcingHandler
void on(OrderUpdatedEvent e, LabAggregateState state) {
state.setDescription(e.getDescription());
state.setQuantity(e.getQuantity());
state.setUpdateCount(state.getUpdateCount() + 1);
}
}A command handler decides and returns an event; an event-sourcing handler applies it to state.
The two never mix. init = true marks the handler that creates the aggregate.
@Projector(version = 1)
public class LabProjector {
@EventHandler
void on(OrderCreatedEvent e) {
LabStore.put(new OrderView(e.getOrderId(), e.getDescription(), e.getQuantity(), "CREATED", false));
}
@EventHandler(retry = 3)
void on(OrderUpdatedEvent e) {
var v = LabStore.get(e.getOrderId());
if (v != null) {
v.setDescription(e.getDescription());
v.setQuantity(e.getQuantity());
}
}
}@EventHandler also takes executor = "name" for parallel consumption — see
Parallel Consumers.
@Saga(version = 1)
public class LabSaga {
@SagaEventHandler(init = true, associationProperty = "orderId")
LabSagaState on(OrderCreatedEvent e, CommandGateway cg, QueryGateway qg) {
var state = new LabSagaState();
state.setAssociation("orderId", e.getOrderId());
state.setOrderId(e.getOrderId());
state.setStatus("CREATED");
return state;
}
@SagaEventHandler(associationProperty = "orderId")
LabSagaState on(OrderConfirmedEvent e, LabSagaState state, QueryGateway qg) throws Exception {
var rich = qg.query(new FindOrderRichByIdQuery(e.getOrderId())).get();
state.setStatus("CONFIRMED");
return state;
}
}The handler returns the new state; associationProperty is how the engine finds the right saga
instance for an incoming event. Gateways are injected as parameters.
@Projection
public class LabProjection {
@QueryHandler
Single<OrderView> query(FindOrderByIdQuery q) {
var v = LabStore.get(q.getOrderId());
if (v == null) throw new NoSuchElementException("order not found: " + q.getOrderId());
return Single.of(v);
}
@QueryHandler
Multiple<OrderView> query(ListOrdersQuery q) {
return Multiple.of(LabStore.getAll());
}
}Return Single.of(...) for one result, Multiple.of(...) for many.
ConsumerEngineConfig::inMemory is the default and is fine for demos and tests — but it forgets
every checkpoint on restart. For anything real, wire the JDBC store: see
Consumer State Store.
| Goal | Page |
|---|---|
| Understand what the server is doing with your messages | Architecture Overview |
| Persist consumer checkpoints | Consumer State Store |
| Speed up a projector | Parallel Consumers |
| Configure the server for production | Server Configuration · Security Model |
| Size it | Throughput and Capacity |
| Monitor it | Observability |
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