Skip to content

Getting Started

Gabor Galazzo edited this page Jul 25, 2026 · 1 revision

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.


1. Run a server

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_password before exposing the server anywhere. See Security Model.


2. Bootstrap a bundle

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=1

Three 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.

Builder reference

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

3. Write components

All four component kinds, taken from evento-lab.

Aggregate — the write model

@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 — the read model

@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 — long-running process

@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 — query handling

@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.


4. Move off in-memory consumer state

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.


5. Next steps

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

Clone this wiki locally