Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

02 - Sistema Bancário (Transações, Event Sourcing)

#auditoria#banking#event-sourcing#ledger#saga
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Sistema Bancário: Transações e Event Sourcing

Este caso acompanha um banco digital com 3,5 milhões de contas ativas e 180 milhões de lançamentos por mês, com pico de 4.500 transações por segundo na janela do almoço, concentrado em PIX e cartão. Os objetivos arquiteturais derivam do regulador tanto quanto do cliente: cada lançamento é fato imutável e auditável, o saldo permanece correto sob concorrência e nenhum dinheiro desaparece quando um serviço externo falha no meio da operação. O desenho adota o ledger como fonte de verdade, com event sourcing no agregado conta e saldo derivado dos eventos. A consistência é forte dentro do agregado e eventual entre contextos, com sagas de compensação nas fronteiras. O resultado é uma plataforma onde a auditoria é subproduto do desenho, e não um projeto separado.

Requisitos e pressupostos

A carga e a regulação definem o desenho. O volume anual projetado passa de 2 bilhões de lançamentos, e a retenção regulatória é de 10 anos com consulta eficiente por conta e por período. As garantias exigidas do sistema são:

  • RPO zero para lançamento confirmado: uma vez aceito pelo cliente, o fato sobrevive a qualquer falha de infraestrutura.
  • RTO de 15 minutos para retomada de comandos após desastre, com teste trimestral do procedimento de restauração.
  • Concorrência real por conta: o mesmo cliente com o app em dois aparelhos não pode produzir duplo saque aceito com o mesmo saldo.
  • Falha externa sem perda de movimento: PIX, bandeiras e boleto podem cair, e o sistema precisa compensar ou reter de forma visível.
  • Conciliação diária que bate centavo a centavo com cada contraparte, com divergência apontada por lançamento.
  • Dados de cartão isolados em escopo próprio, com tokenização nos demais contextos, para reduzir a superfície de conformidade PCI.

Os pressupostos delimitam a modelagem. A conta é agregado com fronteira clara e titular único, e contas conjuntas são tratadas como permissões de acesso, e não como co-titularidade dentro do agregado. Os meios de pagamento externos aceitam idempotência por chave, o que permite repetir chamadas com segurança. O extrato na tela pode apresentar defasagem de segundos, enquanto o saldo usado para decisão de débito é sempre calculado sobre o estado real do agregado. Nada é editado ou apagado: toda correção entra como novo evento, inclusive estornos.

Segurança atravessa todos os requisitos. O acesso ao store de eventos é append-only por permissão de banco, nenhuma credencial de aplicação aceita update ou delete na tabela de fatos, e o acesso de auditoria é read-only com trilha própria. A tokenização de cartão nasce no gateway, então os demais contextos nunca tocam no número aberto.

O perfil de volume orienta o dimensionamento. PIX responde por 70% dos lançamentos, cartão por 20% e boleto por 10%, com concentração de 38% do movimento diário entre 12h e 14h. O tamanho do agregado também importa: 92% das contas acumulam menos de 200 eventos por ano, e o 1% mais movimentado justifica snapshot por conta, e não uma política global única.

Arquitetura proposta

O núcleo é um ledger append-only implementado sobre PostgreSQL, com o agregado conta persistido por event sourcing e reidratado por snapshot mais cauda de eventos. Projeções alimentam as leituras de saldo, extrato e bloqueios, seguindo o par CQRS, e sagas coreografadas coordenam a integração com PIX, bandeiras e boleto. Um fluxo de CDC replica os eventos para a plataforma de dados, servindo analítica e auditoria sem tocar na origem. A tabela resume as garantias e o mecanismo que sustenta cada uma.

Garantia Mecanismo Alcance Custo aceito
Nenhum débito sem saldo Invariante no agregado com append condicional por versão Por conta Contenção baixa, concorrência por conta é rara
Transferência interna atômica Débito e crédito em dois streams no mesmo commit do store Entre contas Transação única cobre os dois streams
Trilha auditável íntegra Append-only com hash encadeado por evento Global Verificação periódica da cadeia
Integração sem perda Saga com compensação e idempotência por chave Entre contextos Estado em progresso visível ao cliente
Leitura rápida Projeções alimentadas pelo log de eventos Por contexto Defasagem de segundos nas telas
Histórico em 10 anos Particionamento por ano com retenção fria em object storage Global Consulta antiga exige restauração parcial

Cada contexto carrega desenho próprio sobre o mesmo fundamento. Contas concentra o event sourcing e a invariante de saldo; meios de pagamento orquestra sagas e idempotência; cartão autoriza com exigência de frescor alto na projeção de saldo; auditoria consome o log por replay e expõe extratos regulatórios. Os quatro compartilham o store de eventos como fronteira, e nenhum escreve na tabela do outro.

Fluxo principal: débito, crédito e transferência

O caminho de comando segue sempre o mesmo formato. O serviço carrega a conta por snapshot mais cauda, valida a invariante sobre o estado reidratado, e anexa o evento com a versão esperada do stream. O append condicional recusa a escrita se outro comando avançou a versão nesse intervalo, e o cliente recebe conflito para repetir sobre o estado novo. A projeção de saldo reage ao evento confirmado e atualiza a leitura, e a saga de integração assume quando há contraparte externa.

O vocabulário de fatos cobre o ciclo completo: AberturaDeConta, DepositoRealizado, SaqueRealizado, TransferenciaEnviada, TransferenciaRecebida, EstornoAutomatico e BloqueioAplicado. Cada evento carrega valor, contraparte quando existe e a chave de idempotência que originou o comando. O extrato na tela é a leitura desses fatos em ordem, e o mesmo arquivo alimenta a conciliação, o que elimina divergência entre o que o cliente vê e o que o regulador audita.

A transferência interna entre contas do próprio banco usa um truque importante: os dois fatos, débito na conta origem e crédito na conta destino, são gravados no mesmo commit do event store, em streams distintos. A atomicidade vem da transação relacional do store, sem coordenador distribuído. Se a segunda parte falha, o commit inteiro falha, e nenhum saldo fica pendente. Em números de produção, 120 mil transferências internas por dia seguem esse caminho sem coordenação adicional.

-- Event store: append-only, sem update e sem delete por permissão de banco
CREATE TABLE eventos (
    id             BIGSERIAL PRIMARY KEY,
    stream_id      VARCHAR(64)  NOT NULL,          -- "conta-{uuid}"
    versao         BIGINT       NOT NULL,          -- posição do evento no stream
    tipo           VARCHAR(64)  NOT NULL,          -- DepositoRealizado, SaqueRealizado
    dados          JSONB        NOT NULL,
    hash_anterior  CHAR(64)     NOT NULL,          -- encadeamento para auditoria
    hash_proprio   CHAR(64)     NOT NULL,          -- sha256(id + hash_anterior + dados)
    ocorrido_em    TIMESTAMPTZ  NOT NULL DEFAULT now(),
    UNIQUE (stream_id, versao)                     -- concorrência otimista nativa
);

-- Snapshot por conta: descartável e reconstruível por replay
CREATE TABLE snapshots (
    stream_id   VARCHAR(64) PRIMARY KEY,
    versao      BIGINT      NOT NULL,
    estado      JSONB      NOT NULL,                -- saldo, status e bloqueios
    criado_em   TIMESTAMPTZ NOT NULL DEFAULT now()
);
def debitar(conta_id, valor, chave_idempotencia):
    foto, cauda = store.carregar(f"conta-{conta_id}")  # snapshot + eventos após a foto
    conta = Conta.reidratar(foto, cauda)
    if conta.saldo < valor:
        raise SaldoInsuficiente(conta_id)              # invariante sobre o estado real
    if chave_idempotencia in conta.chaves_usadas:
        return conta.ultimo_lancamento(chave_idempotencia)  # retry devolve o mesmo fato

    evento = SaqueRealizado(conta_id, valor, chave_idempotencia)
    # append condicional: só grava se a versão no banco for a esperada
    store.acrescentar(
        f"conta-{conta_id}",
        versao_esperada=foto.versao + len(cauda),
        eventos=[evento])
    # projeções e sagas reagem ao fato confirmado a partir daqui

O conflito de versão é caso previsto, e não erro. O cliente recebe código 409 com a versão corrente, e o app repete o comando após reidratar o estado; no p99 a segunda tentativa passa, porque a disputa por uma mesma conta é rara. O conflito persiste apenas na disputa legítima de saldo, exatamente o cenário que a versão esperada existe para interromper.

O checkpoint da projeção grava a posição do log junto com o efeito, na mesma transação do banco de leitura, o que torna a reentrega inofensiva.

-- Checkpoint da projeção: posição do log e efeito avançam juntos
CREATE TABLE checkpoint_projecao (
    nome          VARCHAR(64) PRIMARY KEY,   -- "saldo", "extrato", "bloqueios"
    posicao       BIGINT      NOT NULL,      -- último id de evento processado
    atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);

A integração externa é uma saga coreografada. O evento TransferenciaIniciada dispara o débito com status de reserva, o adaptador chama o meio de pagamento externo usando a chave de idempotência como deduplicador, e a resposta produz TransferenciaConfirmada ou aciona o estorno automático como compensação. Cada passo é um fato no log, e o cliente acompanha o estado em progresso na tela, o que reduz chamados porque a operação nunca parece sumida.

Exemplos concretam o vocabulário no dia a dia. Um PIX recebido é um DepositoRealizado com origem externa; uma compra no cartão gera BloqueioAplicado no limite, e a captura converte o bloqueio em SaqueRealizado; um estorno parcial entra como DepositoRealizado com referência ao fato original, mantendo a cadeia intacta. Nenhum desses movimentos precisa de tabela de status: a leitura dos fatos em ordem já é o estado.

Decisões e trade-offs

Decisão Alternativa descartada Trade-off aceito
Event store sobre PostgreSQL EventStoreDB dedicado Menor throughput bruto por nó; time já domina backup, failover e tuning do PG
Snapshot a cada 100 eventos Replay completo sempre Gestão de fotos descartáveis; reidratação de 3 ms contra 120 ms de replay total
Saga coreografada Orquestrador central Rastro distribuído mais difícil de seguir; nenhum ponto único de falha na coordenação
Saldo derivado com projeção Saldo gravado na conta Projeção pode atrasar segundos; mudança de regra de saldo não exige migração de dado
Forte no agregado, eventual entre contextos Consistência forte global Coordenador distribuído seria gargalo; cada contexto evolui no próprio ritmo
Retenção fria em object storage Histórico sempre quente no banco Consulta antiga exige restauração; custo de armazenamento cai cerca de 80%

O fio condutor é concentrar a consistência forte onde o dinheiro decide, e relaxar onde a leitura só informa. O agregado conta é pequeno, com fronteira nítida, então a concorrência otimista resolve o caso raro de disputa sem lock de banco. Entre contextos, a saga assume: o custo é estado em progresso e a necessidade de projetar compensação para cada passo, preço pequeno perto da perda de movimento silenciosa.

Um custo menos visível merece registro: modelar por eventos exige disciplina de nomenclatura e de compatibilidade, porque o log de 10 anos não pode ser reescrito a cada refatoração. O time adotou versionamento de eventos com conversores, eventos antigos nunca mudam de significado, e correção entra como fato novo. A alternativa, editar histórico, foi descartada por quebrar auditoria e conciliação de uma vez.

Escalabilidade e falhas

A escala de escrita vem do sharding por hash de conta em 16 shards lógicos, cada um mapeado a um cluster de banco. Como a concorrência dentro de um stream é naturalmente baixa, o append condicional raramente recusa, e o throughput cresce com o número de shards sem reescrever o modelo. As projeções rodam sobre réplicas de leitura, com lag monitorado por contexto e frescor exigido apenas nas telas de decisão, como autorização de cartão.

Reconstruir projeções do zero é operação rotineira, e não emergência. A reconstrução das projeções de saldo e extrato para as 3,5 milhões de contas roda em 50 minutos em lote paralelo por shard, e é o teste padrão depois de qualquer mudança de regra de projeção. Como o log é a única verdade, a operação é idempotente e repetível sem janela de manutenção.

O monitoramento segue o log como referência. Lag de projeção por contexto, tempo de reidratação, taxa de conflito de versão e profundidade da DLQ de sagas são as quatro séries de primeira linha, com alerta em desvio de 2 desvios padrão da média horária. Taxa de conflito acima de 0,5% sinaliza hot spot de conta, e o shard problemático é isolado no painel antes que clientes sintam.

Os números medidos em produção sustentam o desenho: comando de débito com p99 de 120 ms, reidratação média de 3 ms com snapshot, e replay completo de um ano de eventos executado em lote noturno para auditoria sem afetar comandos. As falhas têm caminho previsto: o event store fora do ar bloqueia comandos por ser a fonte de verdade, mas as leituras continuam pelas projeções até a retomada. Sagas com timeout externo de 20 segundos acionam estorno automático, e a conciliação diária compara lançamento a lançamento com cada contraparte, apontando divergência por item. O lote mensal processa os 180 milhões de lançamentos em janelas de 40 minutos por contraparte, e a divergência mediana detectada no último ano foi de zero itens por dia útil.

A recuperação de desastre é ensaiada, e não presumida. Os commits usam standby síncrono na mesma região, o que sustenta RPO zero para lançamento confirmado, e uma réplica assíncrona cross-region serve leitura e contingência. O drill trimestral restaura o store em ambiente isolado, roda o replay até o último evento e mediu RTO real de 11 minutos nas três últimas execuções, dentro da meta de 15.

A auditoria depende do hash encadeado: cada evento registra o hash do anterior, e qualquer alteração retroativa quebra a cadeia e é detectada na verificação periódica. O regulador recebe extrato reconstruído por replay, e o mesmo replay reconstrói qualquer saldo histórico, na data exata, porque o log é a única verdade.

Lições aplicáveis

  • Modele o dinheiro como log de fatos: o ledger imutável entregou auditoria, conciliação e depuração sem produto separado, porque a trilha é o próprio dado.
  • Concentre consistência forte onde o estado decide: o agregado conta é o único lugar com append condicional, e tudo ao redor aceita eventualidade com compensação.
  • Snapshot é otimização descartável: fotos podem ser perdidas sem dano, pois o replay recria o estado, e a cadência de 100 eventos foi calibrada por medição.
  • Concorrência otimista por versão evita lock distribuído: a disputa real por conta é rara, e o conflito legível é preferível a serialização global.
  • Compensação precisa ser projetada junto: todo passo externo da saga nasce com o caminho de desfazer e com idempotência por chave.
  • Audite a integridade do log com hash encadeado: a cadeia transforma qualquer tentativa de alteração retroativa em evidência, e não em risco silencioso.
  • Separe frescor por decisão: autorização de cartão lê projeção com exigência de frescor, extrato aceita segundos de defasagem, e a diferença está escrita no desenho.
  • Ensine o negócio a ler o log: extrato por replay e saldo histórico por data viraram argumentos comerciais, e não apenas requisitos regulatórios.
  • Projete o regulador como usuário: acesso read-only ao log e extrato por replay transformaram auditorias que levavam semanas em consultas de minutos.