{"id":44266976,"url":"https://github.com/fireflyframework/fireflyframework-eventsourcing","last_synced_at":"2026-06-26T12:00:34.298Z","repository":{"id":336918929,"uuid":"1151407373","full_name":"fireflyframework/fireflyframework-eventsourcing","owner":"fireflyframework","description":"Reactive event sourcing for Spring Boot — R2DBC event store with optimistic concurrency, snapshots, transactional outbox \u0026 projections","archived":false,"fork":false,"pushed_at":"2026-06-19T13:37:38.000Z","size":502,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T15:28:09.361Z","etag":null,"topics":["cqrs","event-sourcing","event-store","firefly-framework","java","microservices","outbox","projections","r2dbc","reactive","spring-boot","webflux"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fireflyframework.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-06T12:33:12.000Z","updated_at":"2026-06-19T13:36:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/fireflyframework/fireflyframework-eventsourcing","commit_stats":null,"previous_names":["fireflyframework/fireflyframework-eventsourcing"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/fireflyframework/fireflyframework-eventsourcing","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fireflyframework%2Ffireflyframework-eventsourcing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fireflyframework%2Ffireflyframework-eventsourcing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fireflyframework%2Ffireflyframework-eventsourcing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fireflyframework%2Ffireflyframework-eventsourcing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fireflyframework","download_url":"https://codeload.github.com/fireflyframework/fireflyframework-eventsourcing/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fireflyframework%2Ffireflyframework-eventsourcing/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34815669,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-26T02:00:06.560Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cqrs","event-sourcing","event-store","firefly-framework","java","microservices","outbox","projections","r2dbc","reactive","spring-boot","webflux"],"created_at":"2026-02-10T18:25:41.970Z","updated_at":"2026-06-26T12:00:34.287Z","avatar_url":"https://github.com/fireflyframework.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Firefly Framework - Event Sourcing\n\n[![CI](https://github.com/fireflyframework/fireflyframework-eventsourcing/actions/workflows/ci.yml/badge.svg)](https://github.com/fireflyframework/fireflyframework-eventsourcing/actions/workflows/ci.yml)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)\n[![Java](https://img.shields.io/badge/Java-21%2B-orange.svg)](https://openjdk.org)\n[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.x-green.svg)](https://spring.io/projects/spring-boot)\n\n\u003e Reactive event sourcing for Spring Boot — store domain events as the source of truth with an R2DBC event store, optimistic concurrency, snapshots, transactional outbox, and projections.\n\n---\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Features](#features)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [How It Works](#how-it-works)\n- [Version Semantics](#version-semantics)\n- [Configuration](#configuration)\n- [Database Schema](#database-schema)\n- [Documentation](#documentation)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Overview\n\n`fireflyframework-eventsourcing` is a reactive Event Sourcing library for Spring Boot. Instead of persisting only the current state of an entity, an event-sourced application records every state change as an immutable domain event. The full history of events becomes the source of truth: the current state of any aggregate is derived by replaying its events. This unlocks complete audit trails, temporal (\"as-of\") queries, reliable integration via published events, and trivial debugging of how an entity reached its current state.\n\nThe library is built entirely on Project Reactor and R2DBC, so reads and writes are non-blocking end to end. At its core is the reactive `EventStore` SPI backed by an R2DBC implementation (`R2dbcEventStore`) that provides atomic appends, optimistic concurrency control via aggregate versioning, and global-order event streaming for projections. Around this core the module layers production concerns: a snapshot store to cap replay cost, a transactional outbox for reliable event publishing, read-model projections with checkpointing, and operational hooks for metrics, health, structured logging, resilience, and multi-tenancy.\n\nWithin the Firefly Framework, this module sits in the domain/persistence layer and complements the CQRS and orchestration modules. It depends on `fireflyframework-kernel` (shared exceptions and abstractions), `fireflyframework-r2dbc` (reactive database access and utilities), and `fireflyframework-eda` for outbound event publishing — the outbox relays committed events through the EDA abstraction, and the publisher transport is selected with `firefly.eventsourcing.publisher.type` (mapping to the EDA `PublisherType`, e.g. `AUTO`, `KAFKA`, `RABBITMQ`). `fireflyframework-observability` provides metrics, tracing, and health primitives, and `fireflyframework-cache` is an optional dependency used to cache snapshots. Everything is wired automatically through Spring Boot auto-configuration, so adding the dependency and pointing it at an R2DBC datasource is enough to start storing events.\n\n## Features\n\n**Core**\n- `AggregateRoot` base class with reflection-based event-handler dispatch (`on(...)` methods)\n- Reactive `EventStore` SPI backed by R2DBC (`R2dbcEventStore`); PostgreSQL, MySQL, and H2 supported\n- Optimistic concurrency control via per-aggregate versioning (`expectedVersion` on `appendEvents`)\n- `@DomainEvent` annotation for declarative event-type registration (bridges to Jackson `@JsonTypeName`)\n- `AbstractDomainEvent` with builder pattern and metadata helpers (correlationId, causationId, userId, source)\n- `StoredEventEnvelope` wraps domain events with storage metadata (global sequence, created timestamp)\n- `EventStream` with query helpers (`getEventsFromVersion`, `getEventsInRange`, `isEmpty`, `size`)\n- Rich global-order querying: `streamAllEvents`, `streamEventsByType`, `streamEventsByAggregateType`, `streamEventsByTimeRange`, `streamEventsByMetadata`\n\n**Persistence**\n- Flyway-managed schema with 8 migrations (V1–V8) for events, snapshots, outbox, and projection tables\n- `BIGSERIAL` global sequence assigned by the database — the INSERT excludes `global_sequence`\n- `TEXT` columns for `event_data` and `metadata` (database-agnostic, not JSONB)\n- Snapshot store with UPSERT semantics — PK is `(aggregate_id, aggregate_type)`, one snapshot per aggregate\n- Transactional Outbox pattern (`EventOutboxService` / `EventOutboxProcessor`) for reliable publishing with exponential-backoff retry\n\n**Projections \u0026 evolution**\n- `ProjectionService\u003cT\u003e` base class for read-model projections with batch or per-event processing\n- Projection checkpoint tracking (`projection_positions`) for resume-after-restart and lag-based health\n- Event upcasting for schema evolution via the `EventUpcaster` SPI (`canUpcast` / `upcast`, priority-ordered)\n\n**Operations**\n- `@EventSourcingTransactional` with configurable propagation, isolation, retry, and timeout\n- Auto-configuration chain (9 conditional configuration classes) — see the imports file below\n- Health indicators: EventStore, Outbox, Snapshot, Projection\n- Micrometer metrics via `EventStoreMetrics` and `ProjectionMetrics` (timers, counters, gauges)\n- Structured logging with MDC keys and reactive context propagation\n- Circuit breakers (eventStore, outbox, projection) via Resilience4j (off by default)\n- Multi-tenancy via `TenantContext` (off by default)\n\n## Requirements\n\n- Java 21+ (Java 25 recommended)\n- Spring Boot 3.x\n- Maven 3.9+\n- PostgreSQL (recommended) or any R2DBC-compatible database (MySQL, H2). The PostgreSQL, MySQL, and H2 R2DBC drivers are optional dependencies — add the one you use to the classpath.\n\n## Installation\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.fireflyframework\u003c/groupId\u003e\n    \u003cartifactId\u003efireflyframework-eventsourcing\u003c/artifactId\u003e\n    \u003c!-- Version is managed by the Firefly BOM / parent — omit when inheriting it --\u003e\n\u003c/dependency\u003e\n```\n\nIf you inherit the Firefly parent (or import the BOM), the version is managed for you:\n\n```xml\n\u003cparent\u003e\n    \u003cgroupId\u003eorg.fireflyframework\u003c/groupId\u003e\n    \u003cartifactId\u003efireflyframework-parent\u003c/artifactId\u003e\n    \u003cversion\u003e26.05.08\u003c/version\u003e\n\u003c/parent\u003e\n```\n\nAdd the R2DBC driver for your database, e.g. PostgreSQL:\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.postgresql\u003c/groupId\u003e\n    \u003cartifactId\u003er2dbc-postgresql\u003c/artifactId\u003e\n\u003c/dependency\u003e\n```\n\n## Quick Start\n\n### 1. Define domain events\n\n```java\n@DomainEvent(\"order.placed\")\n@SuperBuilder\n@Getter\n@NoArgsConstructor\n@AllArgsConstructor\npublic class OrderPlacedEvent extends AbstractDomainEvent {\n    private String productId;\n    private int quantity;\n    private BigDecimal totalPrice;\n}\n```\n\n### 2. Create an aggregate\n\n```java\npublic class Order extends AggregateRoot {\n\n    private String productId;\n    private int quantity;\n    private BigDecimal totalPrice;\n\n    // Constructor for loading from the event store\n    public Order(UUID id) {\n        super(id, \"Order\");\n    }\n\n    // Constructor for creating a new order (command)\n    public Order(UUID id, String productId, int quantity, BigDecimal totalPrice) {\n        super(id, \"Order\");\n        applyChange(OrderPlacedEvent.builder()\n                .aggregateId(id)\n                .productId(productId)\n                .quantity(quantity)\n                .totalPrice(totalPrice)\n                .build());\n    }\n\n    // Event handler — updates state only, no validation\n    private void on(OrderPlacedEvent event) {\n        this.productId = event.getProductId();\n        this.quantity = event.getQuantity();\n        this.totalPrice = event.getTotalPrice();\n    }\n}\n```\n\n### 3. Persist and load via the EventStore\n\n```java\n@Service\n@RequiredArgsConstructor\npublic class OrderService {\n    private final EventStore eventStore;\n\n    public Mono\u003cOrder\u003e placeOrder(String productId, int qty, BigDecimal price) {\n        UUID orderId = UUID.randomUUID();\n        Order order = new Order(orderId, productId, qty, price);\n\n        return eventStore.appendEvents(\n                orderId, \"Order\", order.getUncommittedEvents(), -1L) // -1 = new aggregate\n            .doOnSuccess(stream -\u003e order.markEventsAsCommitted())\n            .thenReturn(order);\n    }\n\n    public Mono\u003cOrder\u003e getOrder(UUID orderId) {\n        return eventStore.loadEventStream(orderId, \"Order\")\n            .map(stream -\u003e {\n                Order order = new Order(orderId);\n                order.loadFromHistory(stream.getEvents());\n                return order;\n            });\n    }\n}\n```\n\n### 4. Point it at a database\n\n```yaml\nspring:\n  r2dbc:\n    url: r2dbc:postgresql://localhost:5432/mydb\n    username: user\n    password: pass\n\nfirefly:\n  eventsourcing:\n    enabled: true\n    event-scan-packages: \"com.example.myapp\"  # where @DomainEvent classes live\n```\n\nFlyway runs the bundled V1–V8 migrations on startup to create the `events`, `snapshots`, `event_outbox`, and `projection_positions` tables.\n\n## How It Works\n\n### Command flow (write)\n\n```\nCommand --\u003e Aggregate --\u003e [validate] --\u003e Event(s) --\u003e EventStore.appendEvents()\n                                                         |\n                                              +----------+-----------+\n                                              |                      |\n                                       events table            event_outbox\n                                     (BIGSERIAL seq)          (if publisher\n                                                                configured)\n```\n\n### Read flow (query)\n\n```\nEventStore.loadEventStream() --\u003e StoredEventEnvelope[] --\u003e aggregate.loadFromHistory()\n                                                               |\n                                                        Aggregate (current state)\n```\n\n### Auto-configuration chain\n\nRegistered in `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`:\n\n```\nR2dbcBeansAutoConfiguration\nEventStoreAutoConfiguration\nSnapshotAutoConfiguration\nEventSourcingAutoConfiguration\nEventSourcingProjectionAutoConfiguration\nEventSourcingHealthAutoConfiguration\nEventSourcingMetricsAutoConfiguration\nCircuitBreakerAutoConfiguration        (resilience, off by default)\nMultiTenancyAutoConfiguration          (off by default)\n```\n\n## Version Semantics\n\n| State                  | `AggregateRoot.version` | `expectedVersion` for `appendEvents` |\n|------------------------|:-----------------------:|:------------------------------------:|\n| New (no events yet)    | -1                      | -1                                   |\n| After 1st event        | 0                       | 0 (for next append)                  |\n| After Nth event        | N-1                     | N-1 (for next append)                |\n\nThe aggregate version starts at **-1** and increments with each event applied via `applyChange()`. When calling `appendEvents`, pass the aggregate's current version as `expectedVersion` for optimistic concurrency control; a mismatch raises `ConcurrencyException`.\n\n## Configuration\n\nAll properties live under the `firefly.eventsourcing.*` prefix (bound by `EventSourcingProperties` and `EventSourcingProjectionProperties`). The block below shows the real keys with their defaults:\n\n```yaml\nfirefly:\n  eventsourcing:\n    enabled: true                         # master switch\n    event-scan-packages: org.fireflyframework  # packages scanned for @DomainEvent\n    store:\n      type: r2dbc                         # event store backend\n      batch-size: 100\n      connection-timeout: 30s\n      query-timeout: 30s\n      validate-schemas: true\n      max-events-per-load: 1000\n    snapshot:\n      enabled: true\n      threshold: 50                       # snapshot after N events\n      check-interval: 5m\n      keep-count: 3\n      max-age: 30d\n      store-type: same                    # same | cache | ...\n    publisher:\n      enabled: true\n      type: AUTO                          # AUTO | KAFKA | RABBITMQ ... (EDA PublisherType)\n      destination-prefix: events\n      async: true\n      batch-size: 10\n      publish-timeout: 10s\n      continue-on-failure: true\n      retry:\n        enabled: true\n        max-attempts: 3\n        initial-delay: 1s\n        max-delay: 10s\n        backoff-multiplier: 2.0\n    performance:\n      buffer-size: 1000\n      metrics-enabled: true\n      health-checks-enabled: true\n      tracing-enabled: true\n      statistics-interval: 1m\n      circuit-breaker:\n        enabled: false                    # Resilience4j, opt-in\n        failure-rate-threshold: 50.0\n        minimum-number-of-calls: 10\n        sliding-window-size: 60s\n        wait-duration-in-open-state: 30s\n    projection:\n      batch-processing:\n        default-batch-size: 100\n        default-interval: 5s\n        max-batch-size: 1000\n        min-interval: 100ms\n      health-check:\n        timeout: 5s\n        max-acceptable-lag: 1000          # events behind before \"unhealthy\"\n        include-details: true\n        fail-on-unhealthy-projection: true\n      retry:\n        default-max-attempts: 3\n        default-delay: 1s\n        max-delay: 5m\n        backoff-multiplier: 2.0\n      metrics:\n        enabled: true\n        include-projection-tags: true\n        track-event-processing-time: true\n        enable-export: true\n```\n\nKey properties:\n\n- `firefly.eventsourcing.enabled` — master switch for the whole library.\n- `firefly.eventsourcing.event-scan-packages` — packages scanned for `@DomainEvent` types so events deserialize to the correct class (default `org.fireflyframework`; set to your app's base package).\n- `firefly.eventsourcing.snapshot.threshold` — number of events after which a snapshot is written to cap replay cost.\n- `firefly.eventsourcing.publisher.type` — selects the EDA transport for outbox publishing (`AUTO` picks the configured EDA provider; e.g. `KAFKA`, `RABBITMQ`).\n- `firefly.eventsourcing.performance.circuit-breaker.enabled` and `multi-tenancy` are off by default; enable them only when you need the corresponding resilience or tenant-isolation behavior.\n\n## Database Schema\n\nThe library ships 8 Flyway migrations (`V1`–`V8`). Key tables:\n\n| Table                  | Purpose                                | Primary key                       |\n|------------------------|----------------------------------------|-----------------------------------|\n| `events`               | Append-only event log                  | `event_id` (UUID)                 |\n| `snapshots`            | Aggregate state cache                   | `(aggregate_id, aggregate_type)`  |\n| `event_outbox`         | Transactional outbox for publishing     | `outbox_id` (UUID)                |\n| `projection_positions` | Projection checkpoint tracking          | `projection_name`                 |\n\nThe `events` table uses `BIGSERIAL` for `global_sequence` — the database auto-assigns sequence numbers, so the INSERT statement omits that column. `event_data` and `metadata` are `TEXT` (database-agnostic), not JSONB. See [docs/database-schema.md](docs/database-schema.md) for full details.\n\n## Documentation\n\n- Framework docs hub \u0026 module catalog: [github.com/fireflyframework](https://github.com/fireflyframework)\n- In-repo guides under [`docs/`](docs/):\n  - [Event Sourcing Explained](docs/event-sourcing-explained.md) — concepts and comparison with CRUD\n  - [Quick Start](docs/quick-start.md) — step-by-step setup\n  - [Architecture](docs/architecture.md) — layers, auto-configuration, event flows\n  - [API Reference](docs/api-reference.md) — interfaces and classes\n  - [Configuration](docs/configuration.md) — every property with defaults\n  - [Database Schema](docs/database-schema.md) — migrations, tables, indexes, triggers\n  - [Testing](docs/testing.md) — unit, integration, and projection testing\n  - [Account Ledger Tutorial](docs/tutorial-account-ledger.md) — complete worked example\n  - [Optional Enhancements](docs/optional-enhancements.md) — circuit breakers, metrics, multi-tenancy, upcasting\n\n## Contributing\n\nContributions are welcome. Please read the [CONTRIBUTING.md](CONTRIBUTING.md) guide for details on our code of conduct, development process, and how to submit pull requests.\n\n## License\n\nCopyright 2024-2026 Firefly Software Foundation.\n\nLicensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffireflyframework%2Ffireflyframework-eventsourcing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffireflyframework%2Ffireflyframework-eventsourcing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffireflyframework%2Ffireflyframework-eventsourcing/lists"}