GH GambleHub

Event Sourcing: basics

What is Event Sourcing

Event Sourcing (ES) is a way of storing the state of domain objects not as a "current line," but as an unchangeable event log describing everything that happened. The current state of the aggregate is obtained by rolling up (replay) its events, and any read views are built as projections on top of this log.

Key implications:
  • History is the "primary source of truth," state is the projection of history.
  • Any state can be replicated, checked and explained (audit).
  • Adding new views and analytics does not require migrations of old "snapshots" - it is enough to lose events.

Basic terms

Aggregate - domain unit of consistency with clear invariants (Order, Payment, UserBalance).
Event - an unchanging fact that occurred in the past ('payment. authorized`, `order. shipped`).
Event Store is an appendix-only log that provides the order of events within the unit.
Aggregate version is the number of the last applied event (for optimistic concurrency).
Snapshot - a periodic impression of the state to speed up convolution.
Projection (read-model) - materialized view for reading/searching/reporting (often asynchronous).

How it works (thread → events → projections)

1. The client sends a command ('CapturePayment', 'PlaceOrder').
2. The aggregate validates the invariants and, if all are ok, generates events.
3. Events are atomically added to the Event Store with version verification (optimistic concurrency).
4. Projection processors subscribe to the event flow and update read models.
5. When the aggregate is loaded for the following command, the status is restored to snapshot (if any) → the event after the snapshot.

Event design

Required Attributes (Core)

json
{
"event_id": "uuid",
"event_type": "payment. authorized. v1",
"aggregate_type": "Payment",
"aggregate_id": "pay_123",
"aggregate_version": 5,
"occurred_at": "2025-10-31T10:42:03Z",
"payload": { "amount": 1000, "currency": "EUR", "method": "card" },
"meta": { "trace_id": "t-abc", "actor": "user_42" }
}
Recommendations:
  • Naming: 'domain. action. v{major}`.
  • Additivity: new fields are optional, without changing the meaning of the old ones.
  • Minimalism: only facts, without duplication of easily recoverable data.

Contracts and schemes

Fix schemas (Avro/JSON Schema/Protobuf) and check compatibility on CI.
For "breaking" changes - a new major version of the event and parallel publication 'v1 '/' v2' for the migration period.

Competitive access: optimistic concurrency

Rule: New events can only be written if 'expected _ version = = current_version'.

Pseudocode:
pseudo load: snapshot(state, version), then apply events > version new_events = aggregate. handle(command)
append_to_store(aggregate_id, expected_version=current_version, events=new_events)
//if someone has already written an event between load and append, the operation is rejected -> retray with reload

So we guarantee the integrity of invariants without distributed transactions.

Snapshots (convolution acceleration)

Take a snapshot every N events or timer.
Храните `snapshot_state`, `aggregate_id`, `version`, `created_at`.
Always check and catch up on events after the snapshot (don't just trust the cast).
Remove snapshots so that they can be recreated from the log (do not store "magic" fields).

Projections and CQRS

ES is naturally combined with CQRS:
  • Write-model = aggregates + Event Store.
  • Read models = projections updated by events (Redis cards, OpenSearch for search, ClickHouse/OLAP for reporting).
  • The projections are idempotent: reprocessing the same 'event _ id' does not change the result.

Circuit evolution and compatibility

Additive-first: add fields; do not change types/semantics.
For complex changes, release new event types and write projection migrators.
Maintain a double entry ('v1' + 'v2') for the transition period and shoot 'v1' when all projections are ready.

Safety, PII and the "right to be forgotten"

History often contains sensitive data. Approaches:
  • Minimize PII in events (identifiers instead of data, details in protected sides).
  • Crypto-erase: encrypt fields and, when prompted for deletion, destroy the key (the event remains, but data is not available).
  • Revision events: 'user. piiredacted. v1 'with the replacement of sensitive fields in the projections (history preserves the fact of editing).
  • Retention policies: for some domains, some events can be archived to WORM storage.

Performance and scale

Partitioning: the order is important inside the aggregate - partition by'agregate _ id '.
Cold start: snapshots + periodic "compacting" convolution.
Batch appendix - group events in one transaction.
Backpressure and DLQ for projection processors; measure lag (time and number of messages).
Event Store indexing: quick access by '(aggregate_type, aggregate_id)' and by time.

Testing

Specification tests for aggregates - "commands → expected events" scenario.
Projection tests: Feed the event flow and check the materialized state/indexes.
Replayability tests: rebuild the projections from scratch on the stand - make sure that the result matches.
Chaos/latency: inject delays and takes, check idempotence.

Examples of domains

1) Payments

Events: 'payment. initiated`, `payment. authorized`, `payment. captured`, `payment. refunded`.
Invariants: you cannot 'capture' without 'authorized'; amounts are non-negative; the currency is unchanged.
Projections: "payment card" (KV), transaction search (OpenSearch), reporting (OLAP).

2) Orders (e-commerce)

Events: 'order. placed`, `order. paid`, `order. packed`, `order. shipped`, `order. delivered`.
Invariants: state chart status transitions; cancellation is possible before 'shipped'.
Projections: list of user orders, SLA boards by status.

3) Balance sheets (Finance/iGaming)

Events: 'balance. deposited`, `balance. debited`, `balance. credited`, `balance. adjusted`.
Hard invariant: balance does not go away <0; commands are'operation _ id '.
Critical operations are read directly from the aggregate (strict consistency), UI - from the projection (eventual).

Typical Event Store structure (DB variant)

events

`event_id (PK)`, `aggregate_type`, `aggregate_id`, `version`, `occurred_at`, `event_type`, `payload`, `meta`

Index: '(aggregate_type, aggregate_id, version)'.

snapshots

`aggregate_type`, `aggregate_id`, `version`, `state`, `created_at`

Index: '(aggregate_type, aggregate_id)'.

consumers_offsets

'consumer _ id ',' event _ id '/' position ',' updated _ at '(for projections and retail).

Frequently Asked Questions (FAQs)

Is it mandatory to use ES everywhere?
No, it isn't. ES is useful when auditing, complex invariants, reproducibility, and different representations of data are important. For simple CRUD, this is redundant.

What about "current state" requests?
Either read from the projection (quickly, eventual), or from the unit (more expensive, but strictly). Critical operations typically use the second path.

Do I need a Kafka/stream broker?
Event Store - source of truth; broker is convenient for distributing events to projectors and external systems.

What to do with the "right to be forgotten"?
Minimize PII, encrypt sensitive fields and apply crypto erasure/redaction in projections.

How do I migrate old data?
Write a script for retrospective event generation ("re-highstory") or start with "state-as-is" and publish events only for new changes.

Antipatterns

Event Sourcing "out of habit": complicates the system without domain benefit.
Fat events: bloated payloads with PII and doubles - brakes and compliance issues.
Lack of optimistic concurrency: loss of invariants when racing.
Non-reproducible projections: no replay/snapshots → manual fixes.
Raw CDCs as domain events: leaked DB schemas and hard connectivity.
Mixing internal and integration events: publish a stabilized "showcase" outside.

Production checklist

  • Aggregates, invariants and events (titles, versions, schemas) are defined.
  • Event Store provides order within the aggregate and optimistic concurrency.
  • Snapshots and their rebuild plan are included.
  • The projections are idempotent, there are DLQs and lag metrics.
  • Schemes are validated at CI, version policy is documented.
  • PII is minimized, fields are encrypted, there is a "forgetting" strategy.
  • Projection replay checked on bench; has a disaster recovery plan.
  • Dashboards: app speed, projection lag, application errors, proportion of retrays.

Total

Event Sourcing makes the history of the system a first-class artifact: we capture facts, reproduce the state from them and freely build any representations. This gives audit, resistance to change and flexibility of analytics - subject to discipline in schemes, competitive control and competent work with sensitive data.

Contact

Get in Touch

Reach out with any questions or support needs.We are always ready to help!

Start Integration

Email is required. Telegram or WhatsApp — optional.

Your Name optional
Email optional
Subject optional
Message optional
Telegram optional
@
If you include Telegram — we will reply there as well, in addition to Email.
WhatsApp optional
Format: +country code and number (e.g., +380XXXXXXXXX).

By clicking this button, you agree to data processing.