Event Sourcing & CQRS · Event Sourcing Concepts
Event Store Design
Streams, global ordering, subscriptions and implementing a store on Postgres.
An event store provides: append-only streams with expected-version appends, reading a stream in order, a global ordered log (all events, with a position) for projections, and subscriptions (catch-up then live). Options: purpose-built stores (EventStoreDB/KurrentDB, Axon Server), libraries on relational databases (Marten on Postgres, Eventuous, custom tables), or managed options. Kafka is great for distributing events but weak as an event store (no per-stream optimistic concurrency, retention, per-entity reads).
Event store options
| Option | Strengths | Watch out for |
|---|---|---|
| EventStoreDB / KurrentDB | Purpose-built streams, subscriptions, projections | New infrastructure to run |
| Marten (Postgres, .NET) | Event store + document DB on Postgres | .NET only |
| Custom Postgres tables | Simple, transactional, familiar | Global ordering gaps with concurrent inserts |
| Kafka | Distribution, replay | No per-stream concurrency; retention; per-entity reads slow |
-- Minimal event store on PostgreSQL
CREATE TABLE events (
global_position bigserial PRIMARY KEY,
stream_id text NOT NULL,
stream_version int NOT NULL,
event_type text NOT NULL,
data jsonb NOT NULL,
metadata jsonb NOT NULL DEFAULT '{}',
recorded_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (stream_id, stream_version) -- optimistic concurrency
);
-- Append (expected version 4 → new events get 5, 6); unique violation = concurrency conflict
INSERT INTO events (stream_id, stream_version, event_type, data)
VALUES ('account-42', 5, 'MoneyWithdrawn', '{"amount": 1000}');
-- Projections read the global log
SELECT * FROM events WHERE global_position > $last_checkpoint ORDER BY global_position LIMIT 500;
Common pitfalls
- Using bigserial order naively (concurrent transactions commit out of order → projections can skip events; use commit-ordered approaches or transaction-id watermarks).
- Kafka as the only event store.
- Mutable events (editing history).
Key points
- Streams + expected version + global log
- Purpose-built or Postgres-based
- Kafka distributes, doesn't replace a store
Further reading: Kurrent (EventStoreDB): Event sourcing
Practice questions
Why is Kafka usually not a good event store?
Answer
It lacks optimistic concurrency per entity stream, efficient reads of one entity's events, and indefinite retention by default; it's better for distributing events.
What problem can a simple bigserial global position cause for projections?
Answer
Concurrent transactions can commit out of sequence-number order, so a projection reading 'position > checkpoint' may skip an event committed later with a lower position.
Open in the app to mark lessons complete, take notes and track your progress.