When Regular CRUD Stops Working: Getting to Know CQRS and Event Sourcing in C++
Have you ever encountered a situation where your C++ program, working perfectly on test data, starts to "lag" in production? Or when business analysts ask to see the change history, but you only have the latest state? Sound familiar? Then welcome to the world of CQRS (Command Query Responsibility Segregation) and Event Sourcing.
These two patterns are not just buzzwords from the microservices world. They are powerful tools that allow you to build scalable, auditable, and flexible systems. And yes, they fit perfectly into C++ — a language where performance and memory control are critical.
In this article, we won't give boring lectures. We'll break down how to implement CQRS and Event Sourcing in pure C++ without framework magic, with real examples and ready-to-use templates.
CQRS: Separating Reads and Writes
In the classic CRUD (Create, Read, Update, Delete) architecture, we use the same model for both reading and writing. This is convenient as long as the system doesn't start growing. Problems arise when:
- The object in memory is too complex to serialize quickly for reading.
- The write business logic requires validation that slows down reading.
- Different parts of the system want to see the same data in different ways.
CQRS offers a simple but ingenious solution: split the model into two parts — Command for changing state and Query for reading. They can use different storage, different data structures, and even be implemented on different machines.
Basic CQRS Implementation in C++
Let's start simple. Imagine we are writing an order management system. Instead of storing everything in one Order class, we will separate responsibilities.
First, let's define interfaces for commands and queries:
#include <memory>#include <vector>#include <string>
// Base class for all commandsclass Command {public: virtual ~Command() = default; virtual void execute() = 0;};
// Base class for all queriestemplate <typename TResult>class Query {public: virtual ~Query() = default; virtual TResult execute() = 0;};Now let's create a specific command for adding an item to an order:
class AddItemCommand : public Command {private: std::string orderId_; std::string productId_; int quantity_; // Reference to the repository (or command bus) OrderRepository& repository_;
public: AddItemCommand(const std::string& orderId, const std::string& productId, int quantity, OrderRepository& repo) : orderId_(orderId), productId_(productId), quantity_(quantity), repository_(repo) {}
void execute() override { // Business logic: check if the product is in stock if (quantity_ <= 0) { throw std::invalid_argument("Quantity must be positive"); } // Call the domain service or aggregate auto& order = repository_.getById(orderId_); order.addItem(productId_, quantity_); repository_.save(order); }};And here's what a query for getting an order summary looks like:
class OrderSummaryQuery : public Query<OrderSummary> {private: std::string orderId_; ReadModelDatabase& db_;
public: OrderSummaryQuery(const std::string& orderId, ReadModelDatabase& db) : orderId_(orderId), db_(db) {}
OrderSummary execute() override { // Read from an optimized read-only database return db_.getOrderSummary(ord