Skip to content

Event Sourcing & CQRS in Go

CoreConceptAugust 4, 20269 min read

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.

Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read projections
Event Sourcing and CQRS architecture in Go featuring append-only PostgreSQL event stores, optimistic concurrency, and async read 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.

Event Sourcing command-to-projection pipeline from Command validation to PostgreSQL event append and async read model projection
Event Sourcing command-to-projection pipeline from Command validation to PostgreSQL event append and async read model projection

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.

Event Sourcing command-to-projection pipeline from Command validation to PostgreSQL event append and async read model projection
Event Sourcing command-to-projection pipeline from Command validation to PostgreSQL event append and async read model projection

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.

Share:

Related Articles

A customer sees “payment pending” after checkout, retries, and is charged twice. The design question is not whether the

Read

As relational databases grow beyond millions to billions of rows, single-table query performance degrades due to massive

Read

Relational databases like PostgreSQL excel at Online Transaction Processing (OLTP)—handling frequent single-row reads, u

Read

Keep learning

Follow a structured path or browse all courses to go deeper.