Quando o CRUD comum deixa de funcionar: conhecendo CQRS e Event Sourcing em C++
Você já se deparou com uma situação em que seu programa C++, funcionando perfeitamente com dados de teste, começa a "travar" em produção? Ou quando os analistas de negócios pedem para ver o histórico de alterações, mas você tem apenas o estado mais recente? Familiar? Então bem-vindo ao mundo do CQRS (Command Query Responsibility Segregation) e Event Sourcing.
Esses dois padrões não são apenas palavras da moda do mundo dos microsserviços. São ferramentas poderosas que permitem construir sistemas escaláveis, auditáveis e flexíveis. E sim, eles se encaixam perfeitamente em C++ — uma linguagem onde desempenho e controle de memória são cruciais.
Neste artigo, não vamos ler palestras chatas. Vamos analisar como implementar CQRS e Event Sourcing em C++ puro, sem a mágica dos frameworks, com exemplos reais e modelos prontos para uso.
CQRS: Separando Leitura e Escrita
Na arquitetura clássica CRUD (Create, Read, Update, Delete), usamos o mesmo modelo tanto para leitura quanto para escrita. Isso é conveniente até o sistema começar a crescer. Os problemas surgem quando:
- O objeto na memória é muito complexo para ser serializado rapidamente para leitura.
- A lógica de negócios da escrita requer validação que torna a leitura mais lenta.
- Diferentes partes do sistema desejam ver os mesmos dados de maneiras diferentes.
CQRS oferece uma solução simples, mas genial: dividir o modelo em duas partes — Command (comando) para alterar o estado e Query (consulta) para leitura. Eles podem usar armazenamentos diferentes, estruturas de dados diferentes e até mesmo ser implementados em máquinas diferentes.
Implementação básica de CQRS em C++
Vamos começar com algo simples. Imagine que estamos escrevendo um sistema de gerenciamento de pedidos. Em vez de armazenar tudo em uma única classe Order, vamos separar as responsabilidades.
Primeiro, definimos as interfaces para comandos e consultas:
#include <memory>#include <vector>#include <string>
// Classe base para todos os comandosclass Command {public: virtual ~Command() = default; virtual void execute() = 0;};
// Classe base para todas as consultastemplate <typename TResult>class Query {public: virtual ~Query() = default; virtual TResult execute() = 0;};Agora, vamos criar um comando específico para adicionar um item ao pedido:
class AddItemCommand : public Command {private: std::string orderId_; std::string productId_; int quantity_; // Referência ao repositório (ou barramento de comandos) 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 { // Lógica de negócios: verifica se o item está em estoque if (quantity_ <= 0) { throw std::invalid_argument("Quantity must be positive"); } // Chama o serviço de domínio ou agregado auto& order = repository_.getById(orderId_); order.addItem(productId_, quantity_); repository_.save(order); }};E é assim que se parece uma consulta para obter um resumo do pedido:
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 { // Lê do banco de dados otimizado para leitura return db_.getOrderSummary(ord