DEV Community

Fred Amaral for Lerian

Posted on

Construindo um Financial Ledger com CQRS e EDA

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;
Enter fullscreen mode Exit fullscreen mode

O Desafio dos Sistemas Financeiros

Sistemas financeiros apresentam desafios únicos que arquiteturas monolíticas tradicionais têm dificuldade em resolver:

  1. Alto volume de transações: Sistemas financeiros devem processar milhares, senão milhões/bilhões, de transações diariamente.
  2. Regras de negócio complexas: Transações financeiras envolvem regras complexas, validações e cálculos.
  3. Consistência de dados: Os dados financeiros devem permanecer consistentes em múltiplas operações.
  4. Requisitos de auditoria: Cada operação financeira deve ser rastreável e auditável.
  5. 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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Benefícios do CQRS em Sistemas Financeiros

  1. 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.

  2. 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.

  3. 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).

  4. 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;
Enter fullscreen mode Exit fullscreen mode

Conceitos Principais de EDA no Contexto Financeiro

  1. Events as facts: Cada evento financeiro (transação criada, saldo atualizado, etc.) é registrado como um fato imutável.

  2. 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.

  3. 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,
    )
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Padrões Chave de EDA em Sistemas Financeiros

  1. 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.

  2. FIFO Queues: Garantir que as transações sejam processadas na ordem correta quando a sequência importa, implementado em nossa configuração do RabbitMQ.

  3. Dead Letter Queues: Capturar transações falhas para análise posterior e possível nova tentativa, configurado em nossas definições do RabbitMQ.

  4. 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
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Command Initiation: Um command é recebido para criar uma transação (por exemplo, transferir fundos entre contas).

  2. Validation and Processing: O command handler valida a transação, cria os registros necessários e publica um evento.

  3. 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
  4. 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:

  1. API Layer: Endpoints HTTP recebem commands e queries, documentados em nossa API Swagger.

  2. Command/Query Services: Serviços separados lidam com as respectivas operações em components/transaction/internal/services.

  3. Event Bus: RabbitMQ fornece reliable message delivery entre serviços.

  4. 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:

  1. Eventual Consistency: A maioria das operações não requer consistência imediata, permitindo-nos usar processamento assíncrono via RabbitMQ.

  2. Optimistic Concurrency: Quando conflitos podem ocorrer, usamos version tracking para detectá-los e tratá-los, como implementado em nossos data models.

  3. 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

  1. 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
  2. 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.

  1. 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
  2. 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
  3. 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

  1. 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.
  2. 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
  3. 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
  4. Estratégias de teste: Testar sistemas orientados a eventos requer abordagens diferentes das aplicações monolíticas tradicionais. Nosso aprendizado incluiu:

  5. 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:

  1. 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.

  2. 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.

  3. 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.

Image of Quadratic

Free AI chart generator

Upload data, describe your vision, and get Python-powered, AI-generated charts instantly.

Try Quadratic free

Top comments (0)