When Regular CRUD Is No Longer Enough: Introduction to CQRS and Event Sourcing
\nImagine 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?
\nIn 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\nWhat is CQRS in Simple Terms?
\nCQRS 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
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\nSimple CQRS Example in Go
\nLet's create a basic system for managing orders. We'll start with commands:
\n\ntype 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\nNow let's create a separate model for reading:
\n\ntype 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\nNotice: the OrderView already contains denormalized data (user name, product name) gathered from different sources. This speeds up reading by tens of times.
Event Sourcing: Storing Events, Not State
\nEvent 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\nImplementing Event Sourcing in Go
\nLet's start with basic event types:
\n\ntype 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\nNow let's implement an aggregate — an object that restores its state from events:
\n\ntype 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