CQRS and Event Sourcing in Go: Architecture for Complex Systems

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

When Regular CRUD Is No Longer Enough: Introduction to CQRS and Event Sourcing

\n

Imagine you are building a ticket booking system. Everything is simple while there are ten users. But when there are a thousand, and then a million, problems start: databases lock up, queries conflict, and the change history cannot be restored. Sound familiar?

\n

In the world of Go development, where performance and concurrency are everything, two architectural patterns help solve these problems: CQRS (Command Query Responsibility Segregation) and Event Sourcing. Today we'll break them down in practice — with code, examples, and real-world use cases.

\n\n

What is CQRS in Simple Terms?

\n

CQRS is the separation of responsibility between commands (write) and queries (read). In classic CRUD, one data model handles both creation and reading. In CQRS, we create two separate models:

\n
    \n
  • Command model — responsible for changing state (create, update, delete)
  • \n
  • Query model — responsible only for reading data
  • \n
\n

This allows each side to be optimized independently: writing can be strictly validated, while reading can be denormalized and fast. In Go, this is especially effective thanks to goroutines and channels.

\n\n

Simple CQRS Example in Go

\n

Let's create a basic system for managing orders. We'll start with commands:

\n\n
type CreateOrderCommand struct {\n    OrderID   string\n    UserID    string\n    ProductID string\n    Amount    float64\n}\n\ntype CommandHandler struct {\n    repo *OrderRepository\n}\n\nfunc (h *CommandHandler) HandleCreateOrder(ctx context.Context, cmd CreateOrderCommand) error {\n    order := Order{\n        ID:        cmd.OrderID,\n        UserID:    cmd.UserID,\n        ProductID: cmd.ProductID,\n        Amount:    cmd.Amount,\n        Status:    "pending",\n        CreatedAt: time.Now(),\n    }\n    return h.repo.Save(ctx, order)\n}
\n\n

Now let's create a separate model for reading:

\n\n
type OrderQuery struct {\n    repo *ReadRepository\n}\n\ntype OrderView struct {\n    OrderID    string\n    UserName   string\n    ProductName string\n    Total      float64\n    Status     string\n}\n\nfunc (q *OrderQuery) GetOrderSummary(ctx context.Context, orderID string) (*OrderView, error) {\n    return q.repo.GetOrderView(ctx, orderID)\n}
\n\n

Notice: the OrderView already contains denormalized data (user name, product name) gathered from different sources. This speeds up reading by tens of times.

\n\n

Event Sourcing: Storing Events, Not State

\n

Event Sourcing changes the approach to data storage: instead of storing the current state of an object, we store the sequence of events that led to that state. This provides a superpower: a complete history of changes and the ability to "rewind" the state back.

\n\n

Implementing Event Sourcing in Go

\n

Let's start with basic event types:

\n\n
type Event struct {\n    ID        string\n    AggregateID string\n    Type      string\n    Data      []byte\n    Timestamp time.Time\n    Version   int\n}\n\ntype EventStore interface {\n    Save(ctx context.Context, events []Event) error\n    Load(ctx context.Context, aggregateID string) ([]Event, error)\n}
\n\n

Now let's implement an aggregate — an object that restores its state from events:

\n\n
type OrderAggregate struct {\n    ID     string\n    Status string\n    Items  []OrderItem\n    Version int\n}\n\nfunc (a *OrderAggregate) Apply(event Event) {\n    switch event.Type {\n    case "OrderCreated":\n        var data OrderCreatedData\n        json.Unmarshal(event.Data, &data)\n        a.ID = data.OrderID\n        a.Status = "created"\n    case "OrderConfirmed":\n        a.Status = "confirmed"\n    case "OrderShipped":\n        a.Stat                

Blogs

Book Recommendations