Event Sourcing & CQRS in Go
Traditional CRUD (Create, Read, Update, Delete) database architectures mutate entity records in place using SQL UPDATE queries. Destructively overwriting current record values discards historical context: database tables only store the latest state of an application, requiring complex audit log tables and historical triggers to track past state changes.
Event Sourcing stores all changes to application state as an immutable, append-only sequence of domain events (AccountOpened, MoneyDeposited, MoneyWithdrawn). CQRS (Command Query Responsibility Segregation) splits the data model into a Command write model (optimizing high-throughput event appends) and a Query read model (optimizing fast user queries). This guide details Go aggregate roots, PostgreSQL event store design with optimistic concurrency locking, and read-side projections.
Mental Model: CRUD Database Mutations vs Append-Only Immutable Event Streams
CRUD architectures execute UPDATE accounts SET balance = balance - 100 WHERE id = 1, losing the exact time, cause, and context of the financial transaction.
Event Sourcing & CQRS Architecture models business operations as immutable domain events:
1. Write Side (Commands): Accepts commands (WithdrawMoney), validates against aggregate root state, and appends MoneyWithdrawn events to an event store table.
2. Read Side (Queries): Asynchronously projects event streams into denormalized SQL or Elasticsearch tables tailored for fast user queries. For transaction pattern comparisons, review implementing saga pattern microservices distributed transactions and fencing tokens distributed transactions.
Quick reference
- Stores the complete historical timeline of business events as an immutable append-only ledger.
- Eliminates destructive UPDATE and DELETE queries to guarantee full auditability and compliance.
- CQRS separates write-optimized event stores from read-optimized denormalized database views.
- Allows rebuilding entire application states from zero by replaying historical event streams.
- Powers core banking ledgers and trading systems at Stripe, Monzo, Block, and CoreConcept.
Remember this
Implement Event Sourcing in Go to replace destructive CRUD mutations with immutable append-only event logs.
Designing an Event Store in Go (PostgreSQL JSONB & ExpectedVersion Optimistic Locking)
An Event Store in PostgreSQL uses an append-only events table with Optimistic Concurrency Control:
1CREATE TABLE events (2 aggregate_id UUID NOT NULL,3 version INT NOT NULL,4 event_type VARCHAR(100) NOT NULL,5 payload JSONB NOT NULL,6 created_at TIMESTAMPTZ DEFAULT NOW(),7 PRIMARY KEY (aggregate_id, version)8);1// Optimistic Concurrency Check in Go2func AppendEvents(ctx context.Context, tx pgx.Tx, aggID uuid.UUID, expectedVer int, events []Event) error {3 for i, evt := range events {4 ver := expectedVer + i + 15 _, err := tx.Exec(ctx, `6 INSERT INTO events (aggregate_id, version, event_type, payload) 7 VALUES ($1, $2, $3, $4)`, aggID, ver, evt.Type, evt.Payload)8 if err != nil {9 return fmt.Errorf("concurrency conflict: expected version %d", ver)10 }11 }12 return nil13}Quick reference
- Compound primary key (aggregate_id, version) enforces strict version ordering in PostgreSQL.
- Optimistic Concurrency Control rejects concurrent writes if version numbers conflict.
- JSONB column stores typed event payloads flexibility without requiring schema migrations per event.
- Append-only design eliminates row lock contention during concurrent write operations.
- PostgreSQL WAL (Write-Ahead Log) enables Change Data Capture (CDC) streaming via Debezium.
Remember this
Use compound primary keys (aggregate_id, version) in PostgreSQL to enforce optimistic concurrency locking.
Command Handlers, Aggregate Root Replay, & Snapshotting
In Go, an Aggregate Root evaluates business invariants before emitting new events:
1type BankAccountAggregate struct {2 ID uuid.UUID3 Version int4 Balance int645}6 7func (a *BankAccountAggregate) Apply(evt Event) {8 switch e := evt.(type) {9 case MoneyDepositedEvent:10 a.Balance += e.Amount11 case MoneyWithdrawnEvent:12 a.Balance -= e.Amount13 }14 a.Version++15}### Performance Snapshotting
Replaying 10,000 historical events to hydrate a long-lived aggregate root slows command execution. Snapshotting saves the current aggregate state every 100 events (snapshots table). Hydration loads the latest snapshot and replays only subsequent events.
Quick reference
- Aggregate roots encapsulate business rules and apply events to update in-memory state.
- Hydration replays past events sequentially to restore aggregate state before command execution.
- Snapshotting saves compressed aggregate states every N events to eliminate replay latency.
- Decouples domain business logic cleanly from underlying database storage frameworks.
- Guarantees deterministic aggregate state reconstruction across distributed Go services.
Remember this
Combine Go aggregate roots with periodic snapshotting to maintain instant hydration performance.
Asynchronous Projections to Read-Side Search & SQL View Engines
Command handlers append events to the event store and publish events to a message bus (Kafka/NATS).
Projection Background Workers listen to the event bus and update denormalized read-side database views (e.g. account_summaries SQL table or Elasticsearch search index):
- Eventual Consistency: Read models update asynchronously within milliseconds of event appends.
- Custom Read Schema: Easily add new UI features (e.g. monthly_spending_analytics) by replaying all historical events through a new projection worker without altering existing write models.
Quick reference
- Background projection workers listen to event streams and update read-side database tables.
- Eventual consistency delivers sub-10ms read view updates across distributed services.
- Allows creating new specialized read models retroactively by replaying historical event logs.
- Separates write concerns (high throughput appends) from read concerns (complex SQL joins).
- Provides unlimited horizontal read scaling without placing load on write event stores.
Remember this
Build asynchronous event projections to decouple read-side query views from write event stores.
Key takeaway
To test CQRS event sourcing in Go, implement a simple BankAccount aggregate root with Deposit and Withdraw commands. Append events to PostgreSQL and verify version conflict errors.
Related Articles
Explore this topic