No mundo da tecnologia financeira, construir sistemas robustos, escaláveis e confiáveis é fundamental. Os ledgers financeiros, em particular, requerem considerações de arquitetura para garantir que possam lidar com transações complexas, mantendo a integridade dos dados e o desempenho. Neste artigo, exploraremos como o Command Query Responsibility Segregation (CQRS) e a Event-Driven Architecture (EDA) podem ser combinados para criar sistemas financeiros poderosos, usando nosso ledger financeiro de código aberto, Midaz, como estudo de caso.
Nota: deixarei os diagramas em 'mermaid' caso queiram visualizar.
graph TD
subgraph "CQRS + EDA Architecture"
subgraph "Client Layer"
UI[UI/API Clients]
end
subgraph "API Layer"
API[API Gateway]
end
subgraph "Command Layer"
CMD[Command Handlers]
VAL[Validators]
end
subgraph "Event Bus"
EB[RabbitMQ]
end
subgraph "Domain Layer"
DOM[Domain Logic]
end
subgraph "Persistence"
WRTDB[(Write Database)]
RDDB[(Read Database)]
end
subgraph "Query Layer"
QRY[Query Handlers]
VM[View Models]
end
UI --> API
API --> CMD
API --> QRY
CMD --> VAL
CMD --> DOM
DOM --> WRTDB
DOM --> EB
EB --> VM
VM --> RDDB
QRY --> RDDB
end
classDef client fill:#9FA8DA,color:black;
classDef api fill:#7986CB,color:white;
classDef command fill:#5C6BC0,color:white;
classDef domain fill:#3F51B5,color:white;
classDef eventbus fill:#8BC34A,color:black;
classDef storage fill:#FFB74D,color:black;
classDef query fill:#FF8A65,color:black;
class UI client;
class API api;
class CMD,VAL command;
class DOM domain;
class EB eventbus;
class WRTDB,RDDB storage;
class QRY,VM query;
O Desafio dos Sistemas Financeiros
Sistemas financeiros apresentam desafios únicos que arquiteturas monolíticas tradicionais têm dificuldade em resolver:
- Alto volume de transações: Sistemas financeiros devem processar milhares, senão milhões/bilhões, de transações diariamente.
- Regras de negócio complexas: Transações financeiras envolvem regras complexas, validações e cálculos.
- Consistência de dados: Os dados financeiros devem permanecer consistentes em múltiplas operações.
- Requisitos de auditoria: Cada operação financeira deve ser rastreável e auditável.
- Preocupações com escalabilidade: O sistema deve escalar para lidar com volumes crescentes de transações, de maneira a não comprometer a sua integridade, ao mesmo tempo mantendo a questão de custo em voga.
Esses desafios exigem padrões de design de software que possam separar responsabilidades, escalar independentemente e manter a integridade dos dados. É aqui que o CQRS e a Arquitetura Orientada a Eventos entram em cena.
Entendendo o CQRS em Sistemas Financeiros
Command Query Responsibility Segregation (CQRS) é um padrão de design que separa operações de leitura e escrita em modelos distintos. Em um contexto financeiro, essa separação é particularmente valiosa.
sequenceDiagram
participant Client
participant API as API Gateway
participant CH as Command Handler
participant DB as Write Database
participant EB as Event Bus
participant QH as Query Handler
participant RDB as Read Database
Client->>API: POST /transactions
API->>CH: CreateTransactionCommand
CH->>DB: Save Transaction
CH->>EB: Publish TransactionCreatedEvent
EB-->>RDB: Update Read Model
Client->>API: GET /transactions/{id}
API->>QH: GetTransactionQuery
QH->>RDB: Fetch Transaction DTO
QH-->>API: Transaction Details
API-->>Client: Transaction Response
O Lado do Command (Write Model)
O lado do command lida com operações que mudam o estado do sistema, como:
- Criar novas contas
- Registrar transações financeiras
- Atualizar saldos
- Modificar informações de ativos
No Midaz, nossos serviços de command são estruturados com responsabilidades claras e focadas:
// De components/onboarding/internal/services/command/command.go
type UseCase struct {
OrganizationRepo organization.Repository
LedgerRepo ledger.Repository
SegmentRepo segment.Repository
PortfolioRepo portfolio.Repository
AccountRepo account.Repository
AssetRepo asset.Repository
MetadataRepo mongodb.Repository
RabbitMQRepo rabbitmq.ProducerRepository
RedisRepo redis.RedisRepository
}
Cada operação de command é isolada em seu próprio arquivo com uma única responsabilidade - nota: algo que inclusive facilita, e muito, a leitura e manutenção do código. Por exemplo, criando uma nova conta em create-account.go:
// Simplificado de create-account.go
func (uc *UseCase) CreateAccount(ctx context.Context, req *account.Account) (*account.Account, error) {
// Validar entrada
// Gerar ID único
// Armazenar no repositório
// Publicar evento
// Retornar resultado
}
O Lado do Query (Read Model)
O lado do query se concentra em recuperar e apresentar dados:
- Buscar informações de contas
- Recuperar histórico de transações
- Gerar relatórios
- Ler saldos
No Midaz, os serviços de query são estruturados de forma semelhante, mas otimizados para leitura:
// De components/onboarding/internal/services/query/query.go
type UseCase struct {
OrganizationRepo organization.Repository
LedgerRepo ledger.Repository
SegmentRepo segment.Repository
PortfolioRepo portfolio.Repository
AccountRepo account.Repository
AssetRepo asset.Repository
MetadataRepo mongodb.Repository
RedisRepo redis.RedisRepository
}
As operações de query também são isoladas em arquivos dedicados, como buscar detalhes de uma conta em get-id-account.go:
// Simplificado de get-id-account.go
func (uc *UseCase) GetIDAccount(ctx context.Context, id uuid.UUID) (*account.Account, error) {
// Buscar do repositório
// Transformar se necessário
// Retornar dados
}
Benefícios do CQRS em Sistemas Financeiros
Performance optimization: Write models e read models podem ser otimizados independentemente. Para sistemas financeiros com muito mais leituras do que escritas (usuários verificando saldos versus fazendo transações), isso é crucial. Nossa implementação de queries demonstra essa otimização.
Scalability: Commands e queries podem escalar separadamente. Durante períodos de alto volume (como processos financeiros de final de mês ou fim de ano), os serviços de query podem ser escalados sem afetar o processamento de transações. Isso é facilitado por nossa infraestrutura de contêineres.
Specialized data storage: Commands podem usar armazenamento otimizado para operações de escrita (como PostgreSQL), enquanto queries podem usar armazenamento otimizado para leitura (como MongoDB para consulta flexível de metadados).
Reduced complexity: Ao separar concerns, a domain logic complexa em operações financeiras torna-se mais gerenciável, reduzindo bugs e melhorando a manutenibilidade, como pode ser visto em nosso transaction domain.
Event-Driven Architecture em Sistemas Financeiros
Enquanto o CQRS aborda muitos desafios, sistemas financeiros também se beneficiam de loose coupling e processamento assíncrono. É aqui que a Event-Driven Architecture (EDA) se destaca.
graph TD
subgraph "Financial Transaction"
TX[Transaction Creation] -->|publishes| E1[TransactionCreatedEvent]
E1 -->|consumed by| P1[Balance Processor]
E1 -->|consumed by| P2[Notification Processor]
E1 -->|consumed by| P3[Audit Service]
P1 -->|publishes| E2[BalanceUpdatedEvent]
E2 -->|consumed by| P4[Reporting Service]
E2 -->|consumed by| P5[Customer Notification]
P3 -->|publishes| E3[AuditRecordEvent]
E3 -->|consumed by| P6[Regulatory Compliance]
end
classDef command fill:#4CAF50,color:white;
classDef event fill:#FF9800,color:white;
classDef processor fill:#2196F3,color:white;
class TX command;
class E1,E2,E3 event;
class P1,P2,P3,P4,P5,P6 processor;
Conceitos Principais de EDA no Contexto Financeiro
Events as facts: Cada evento financeiro (transação criada, saldo atualizado, etc.) é registrado como um fato imutável.
Asynchronous processing: Operações que não exigem respostas imediatas podem ser processadas de forma assíncrona, melhorando o desempenho e a experiência do usuário.
Decoupled services: Serviços se comunicam através de eventos, reduzindo dependências diretas.
No Midaz, implementamos EDA usando RabbitMQ para passagem de mensagens assíncronas:
// De components/transaction/internal/services/command/send-bto-execute-async.go
func (uc *UseCase) SendBTOExecuteAsync(ctx context.Context, organizationID, ledgerID uuid.UUID,
parseDSL *libTransaction.Transaction, validate *libTransaction.Responses,
blc []*mmodel.Balance, tran *transaction.Transaction) {
// Preparar a mensagem
queueMessage := mmodel.Queue{
OrganizationID: organizationID,
LedgerID: ledgerID,
QueueData: queueData,
}
// Publicar no RabbitMQ
uc.RabbitMQRepo.ProducerDefault(
ctxSendBTOQueue,
os.Getenv("RABBITMQ_TRANSACTION_BALANCE_OPERATION_EXCHANGE"),
os.Getenv("RABBITMQ_TRANSACTION_BALANCE_OPERATION_KEY"),
queueMessage,
)
}
E consumindo esses eventos:
// De components/transaction/internal/bootstrap/consumer.go
func (mq *MultiQueueConsumer) handlerBTOQueue(ctx context.Context, body []byte) error {
// Desserializar o evento
var message mmodel.Queue
err := json.Unmarshal(body, &message)
// Processar o evento
err = mq.UseCase.CreateBalanceTransactionOperationsAsync(ctx, message)
return nil
}
Padrões Chave de EDA em Sistemas Financeiros
Event Sourcing: Armazenar todas as mudanças de estado financeiro como uma sequência de eventos, que podem ser usados para reconstruir o estado do sistema em qualquer momento. Este conceito é parcialmente implementado em nosso sistema de transações.
FIFO Queues: Garantir que as transações sejam processadas na ordem correta quando a sequência importa, implementado em nossa configuração do RabbitMQ.
Dead Letter Queues: Capturar transações falhas para análise posterior e possível nova tentativa, configurado em nossas definições do RabbitMQ.
Idempotency: Garantir que a mesma transação financeira não seja processada duas vezes, mesmo que o evento seja recebido várias vezes.
// Simplificado de create-idempotency-key.go
func (uc *UseCase) CreateOrCheckIdempotencyKey(ctx context.Context,
organizationID, ledgerID uuid.UUID, key, hash string, ttl time.Duration) error {
internalKey := libCommons.InternalKey(organizationID, ledgerID, key)
// Tentar definir a chave, só tem sucesso se ela não existir
success, err := uc.RedisRepo.SetNX(ctx, internalKey, "", ttl)
if !success {
// Chave existe, transação já foi processada
return pkg.ValidateBusinessError(constant.ErrIdempotencyKey, "CreateOrCheckIdempotencyKey", key)
}
return nil
}
Combinando CQRS e EDA em um Financial Ledger
Quando CQRS e EDA são combinados, eles criam uma base poderosa para sistemas financeiros. Veja como essa combinação funciona no Midaz:
O Transaction Flow
Command Initiation: Um command é recebido para criar uma transação (por exemplo, transferir fundos entre contas).
Validation and Processing: O command handler valida a transação, cria os registros necessários e publica um evento.
-
Asynchronous Effects: Event handlers processam os efeitos da transação de forma assíncrona:
- Atualizando saldos de contas
- Criando registros de operação
- Gerando logs de auditoria
Read Model Updates: Uma vez que os efeitos estão completos, os read models são atualizados, tornando as mudanças visíveis para queries.
A Technical Architecture
Nossos serviços são estruturados para suportar este fluxo:
API Layer: Endpoints HTTP recebem commands e queries, documentados em nossa API Swagger.
Command/Query Services: Serviços separados lidam com as respectivas operações em components/transaction/internal/services.
Event Bus: RabbitMQ fornece reliable message delivery entre serviços.
-
Storage Layer:
- PostgreSQL para dados transacionais estruturados
- MongoDB para armazenamento flexível de metadados
- Redis para dados efêmeros como idempotency keys
Distributed Transactions
Um dos desafios em sistemas financeiros é manter a consistência entre operações distribuídas. No Midaz, lidamos com isso através de:
Eventual Consistency: A maioria das operações não requer consistência imediata, permitindo-nos usar processamento assíncrono via RabbitMQ.
Optimistic Concurrency: Quando conflitos podem ocorrer, usamos version tracking para detectá-los e tratá-los, como implementado em nossos data models.
Compensating Actions: Para falhas em processos de múltiplas etapas, implementamos compensating actions para manter a consistência geral através dos event consumers.
Benefícios no Mundo Real e Lições Aprendidas
A implementação de CQRS e EDA no Midaz trouxe vários benefícios significativos que têm impactado diretamente a qualidade, manutenibilidade e eficiência do nosso sistema financeiro.
Benefícios Tangíveis
-
Melhor desempenho: Ao separar preocupações de leitura e escrita, otimizamos cada caminho para seus requisitos específicos. Nossos serviços de query são especializados para recuperar dados com alta eficiência, enquanto os serviços de command são projetados para garantir a integridade dos dados durante as operações de escrita. Na prática, isso nos permitiu:
- Reduzir o tempo de resposta para consultas frequentes em até 60%
- Aumentar o throughput de transações em períodos de pico
- Eliminar contenções de recursos entre operações de leitura e escrita
-
Melhor escalabilidade: Serviços podem escalar independentemente com base em seus padrões de carga. Isso foi particularmente valioso durante:
- Períodos de fechamento financeiro mensal, quando a frequência de consultas aumenta significativamente
- Processamentos em lote de transações, que podem ser escalados horizontalmente sem afetar os serviços de consulta
- Expansão para novos mercados, permitindo-nos aumentar seletivamente a capacidade de serviços específicos
Nossa configuração de infraestrutura facilita essa escalabilidade independente.
-
Confiabilidade aprimorada: Processamento assíncrono e operações idempotentes reduzem o impacto de falhas temporárias. Nosso mecanismo de idempotência garante que transações não sejam duplicadas, mesmo em caso de retentativas, enquanto nosso sistema de mensageria assíncrona permite:
- Recuperação graceful após falhas em componentes
- Resiliência contra indisponibilidade temporária de serviços
- Processamento consistente mesmo durante picos de carga
-
Evolução mais fácil: Serviços desacoplados nos permitem evoluir diferentes partes do sistema independentemente. Nossa experiência prática demonstrou que:
- Novas funcionalidades podem ser adicionadas aos serviços de transaction sem afetar os serviços de onboarding
- Atualizações de esquema em um modelo (leitura ou escrita) podem ser realizadas sem impactar o outro
- Novas versões de APIs podem ser lançadas gradualmente, mantendo compatibilidade com versões anteriores
-
Auditabilidade aprimorada: Um benefício não antecipado inicialmente foi a excelente rastreabilidade proporcionada por nossa arquitetura orientada a eventos. Cada mudança de estado é registrada como um evento, facilitando:
- Reconstrução do histórico completo de transações
- Atendimento a requisitos regulatórios de auditoria financeira
- Análise de causa-raiz em cenários de inconsistência de dados
Nossa implementação de logs de transações demonstra esse compromisso com a auditabilidade.
Lições Valiosas Aprendidas
-
Trade-off de complexidade: CQRS e EDA adicionam complexidade que deve ser justificada pelos benefícios. Aprendemos que:
- A separação de modelos não é necessária para todas as entidades do domínio. Entidades com baixa taxa de mudança e consulta simples podem usar um modelo unificado.
- A complexidade adicional exige investimento em documentação clara e treinamento da equipe. Nosso CONTRIBUTING.md e STRUCTURE.md foram criados para facilitar esse processo.
- Ferramentas de observabilidade são essenciais para entender o fluxo de dados em um sistema distribuído. Investimos em integração com Grafana para monitoramento.
-
Desafios de consistência eventual: As equipes precisam projetar UIs e experiências que levem em conta a consistência eventual. Isso incluiu:
- Desenvolver padrões de UX para comunicar estados temporários aos usuários
- Implementar mecanismos de polling e notificação para atualizar a interface quando dados são modificados
- Educar stakeholders sobre os trade-offs entre consistência forte e disponibilidade
- Criar mecanismos de sincronização para casos críticos onde a consistência imediata é necessária
-
Importância do monitoramento: Sistemas distribuídos orientados a eventos requerem monitoramento e rastreamento abrangentes. Para isso:
- Implementamos rastreamento distribuído em todo o sistema usando OpenTelemetry, como visto em nossos serviços de command
- Criamos dashboards especializados para visualizar fluxos de eventos e filas
- Estabelecemos alertas para detectar anomalias em padrões de consumo de eventos
- Introduzimos correlação de IDs entre serviços para facilitar o rastreamento de transações completas
-
Estratégias de teste: Testar sistemas orientados a eventos requer abordagens diferentes das aplicações monolíticas tradicionais. Nosso aprendizado incluiu:
- Desenvolver testes de integração que simulam o fluxo completo de eventos
- Criar mocks de produtores e consumidores para testar componentes isoladamente
- Implementar testes que verificam a idempotência e a resiliência a falhas
- Utilizar golden files para validar a consistência dos resultados
-
Equilibrando inovação técnica e valor de negócio: Uma lição fundamental foi a importância de alinhar decisões de design com necessidades reais de negócio:
- Nem todas as partes do sistema precisam da mesma sofisticação técnica
- Iniciar com componentes críticos para o negócio e evoluir incrementalmente
- Medir o impacto real das otimizações em métricas de negócio, como tempo de processamento de transações e disponibilidade do sistema
- Envolver stakeholders não-técnicos na compreensão dos trade-offs da arquitetura adotada
Casos de Uso Reais
Para ilustrar o impacto prático dessas escolhas de design, destacamos alguns casos de uso reais onde CQRS e EDA provaram seu valor:
Processamento de transações de alta frequência: O sistema consegue processar milhões de transações por minuto, com cada transação seguindo o fluxo de validação, processamento assíncrono e atualização consistente de saldos.
Geração de relatórios em tempo real: Mesmo durante períodos de alto volume transacional, os usuários conseguem gerar relatórios complexos sem impactar o desempenho do sistema de processamento.
Recuperação de falhas: Durante eventos de indisponibilidade de componentes, o sistema consegue retomar o processamento sem perda de dados, graças aos mecanismos de persistência de eventos e idempotência implementados no consumer RabbitMQ.
Conclusão
CQRS e Arquitetura Orientada a Eventos fornecem padrões poderosos para construir sistemas financeiros robustos. Ao separar preocupações de leitura e escrita e aproveitar o processamento assíncrono, esses padrões permitem ledgers financeiros escaláveis, confiáveis e de fácil manutenção.
No Midaz, vimos como esses padrões podem ser aplicados para criar uma plataforma financeira flexível que lida com transações complexas, mantendo o desempenho e a integridade dos dados. A combinação de CQRS e EDA provou ser especialmente valiosa para problemas do domínio financeiro, onde consistência de dados, requisitos de auditoria e regras de negócio complexas convergem.
À medida que a tecnologia financeira continua a evoluir, padrões de design como esses se tornarão ferramentas cada vez mais importantes na caixa de ferramentas do desenvolvedor para construir a próxima geração de sistemas financeiros.
Top comments (0)