ArquiteturaIntermediário8 min de leitura6 seções1494, Data conhecida

Ledger (livro-razão)

Em vez de um saldo que muda, um histórico imutável de lançamentos — e o saldo é a soma. Com partidas dobradas, todo valor sai de uma conta e entra em outra, e a soma do sistema é sempre zero: dinheiro não aparece nem some sem rastro.

Em 10 segundos

Saldo não é uma coluna que muda — é a soma de lançamentos imutáveis. Cada lançamento debita uma conta e credita outra (partidas dobradas), então a soma do sistema é sempre zero e qualquer bug que 'cria' ou 'some' dinheiro é detectado na hora.

Antes de ter nome

Mercadores de Gênova e Veneza já escrituravam em partidas dobradas desde o século XIV; Pacioli documentou a prática, não a inventou.

  • #pagamentos
  • #partidas-dobradas
  • #auditoria
  • #saldo
  • #contabilidade
Analogia

O caderno da mercearia

O dono da mercearia não apaga a linha de ontem quando você paga a conta — escreve uma linha nova: 'pagou 50'. Se ele rasurasse o caderno, uma discussão sobre o total não teria como ser resolvida. O caderno inteiro é a prova; o total é só a soma das linhas. Bancos funcionam assim desde Veneza no século XV — e o seu extrato é exatamente isso.

01Conceito

O problema: o saldo que não sabe se explicar

Um saldo guardado como coluna é um número sem memória. Funciona até o dia em que o financeiro pergunta por que a conta da Ana tem R$ 37,50 a mais — e a resposta honesta é 'não sabemos, o UPDATE sobrescreveu o passado'.

Pior: a coluna mutável convida a corrida. Dois saques simultâneos leem o mesmo saldo, ambos passam na checagem, ambos gravam — e o dinheiro nasce do nada, sem deixar pista de quando.

Partidas dobradas resolvem o segundo problema estrutural: com uma perna só ('creditei 100 pra Ana'), um bug que esquece a contrapartida cria dinheiro silenciosamente. Com duas pernas obrigatórias, o dinheiro sempre vem de algum lugar — nem que seja de uma conta explícita 'mundo externo' — e a invariante soma-zero vira um alarme barato que roda em qualquer conciliação.

O saldo derivado não precisa ser lento: materialize-o como projeção (uma linha por conta, atualizada na mesma transação do lançamento) e reconcilie periodicamente contra a soma real. O ledger continua sendo a fonte da verdade; a projeção é só cache com auditoria.

Sem o padrão × com o padrão

Ledger de lançamentos

  • INSERT lançamento (débito A, crédito B, 100)
  • Saldo = SUM(créditos) − SUM(débitos)
  • Estorno é um lançamento inverso — nada se apaga

Todo número tem extrato: dá para recontar, auditar e provar. A soma do sistema fecha em zero ou há um bug — e você fica sabendo.

A mesma operação de negócio, com e sem memória: o ledger troca 'confie no número' por 'reconte quando quiser'.
02Anatomia

Anatomia de um ledger

Camada 1/3Lançamentos

Camada 1

Lançamentos

a fonte da verdade — append-only, partidas dobradas

Uma tabela imutável: id, conta de débito, conta de crédito, valor em centavos inteiros, motivo, timestamp. Sem UPDATE nem DELETE — o banco pode até revogar esses privilégios do usuário da aplicação. Estorno, ajuste e correção são lançamentos novos.

No código

REVOKE UPDATE, DELETE ON lancamentos FROM app;
-- corrigir = lançar no sentido contrário

Se violar: um único UPDATE 'só desta vez' quebra a confiança do livro inteiro: se uma linha pôde mudar, nenhuma soma prova mais nada.

03Implementação

Código

// Saldo não é uma coluna: é a soma de lançamentos imutáveis.
// Partidas dobradas: todo valor SAI de uma conta e ENTRA em outra.
interface Lancamento {
  id: string;
  debito: string;   // conta de onde sai
  credito: string;  // conta onde entra
  centavos: number; // dinheiro em inteiro — nunca float
  motivo: string;
}

const livro: Lancamento[] = []; // append-only: sem UPDATE, sem DELETE

function lancar(l: Omit<Lancamento, "id">): Lancamento {
  if (l.centavos <= 0) throw new Error("valor deve ser positivo");
  if (l.debito === l.credito) throw new Error("contas distintas");
  const registro = { id: crypto.randomUUID(), ...l };
  livro.push(registro); // a única escrita permitida
  return registro;
}

// O saldo é DERIVADO — recontável, auditável, à prova de "de onde veio isso?"
function saldo(conta: string): number {
  return livro.reduce(
    (s, l) => s + (l.credito === conta ? l.centavos : 0) - (l.debito === conta ? l.centavos : 0),
    0
  );
}

// A invariante que dedura bugs: a soma do sistema inteiro é sempre zero.
function somaDoSistema(): number {
  return livro.reduce((s, l) => s + l.centavos - l.centavos, 0);
}

lancar({ debito: "banco:externo", credito: "cliente:ana", centavos: 10_000, motivo: "pix_in" });
lancar({ debito: "cliente:ana", credito: "cliente:bia", centavos: 2_500, motivo: "transferencia" });
// estorno NÃO apaga nada — é um lançamento no sentido contrário:
lancar({ debito: "cliente:bia", credito: "cliente:ana", centavos: 2_500, motivo: "estorno" });

console.log(saldo("cliente:ana")); // 10000
console.log(somaDoSistema());      // 0 — sempre
04No mundo real

Casos de uso

Carteira digital num PaaS de pagamentos

Cenário

Clientes têm saldo, recebem PIX, transferem entre si e sacam. O BACEN e a auditoria interna exigem provar cada centavo, e a conciliação com o extrato do banco liquidante roda todo dia.

Como o padrão entra

Cada pix_in debita 'banco:liquidante' e credita a carteira; cada saque faz o inverso. O saldo exibido é projeção; a conciliação soma o ledger e bate com o extrato externo. Estorno de PIX é lançamento inverso referenciando o original — o histórico do cliente mostra os dois.

Trade-off: Escrever duas pernas e atualizar projeção na mesma transação custa mais por operação do que um UPDATE — e o time precisa aprender a pensar em contas e contrapartidas, o que não é intuitivo no primeiro contato.

Créditos de uso de uma API

Cenário

Uma plataforma vende pacotes de créditos consumidos por chamada. Clientes contestam cobranças ('não usei isso tudo') e o comercial dá cortesias que precisam de rastro.

Como o padrão entra

Compra credita a conta do cliente contra 'receita'; cada chamada debita 1 crédito contra 'consumo'; cortesia vem de uma conta própria 'marketing'. A resposta para toda contestação é o extrato — linha a linha, com timestamp e motivo.

Trade-off: Volume alto de débitos minúsculos infla a tabela rápido; agregação em lote (um lançamento por minuto de uso) alivia, ao custo de granularidade no extrato.

05Cuidado

Armadilhas

  1. Ledger com UPDATE liberado

    Se a tabela de lançamentos aceita UPDATE ou DELETE, você não tem um ledger — tem uma tabela com nome pomposo. A imutabilidade é o que dá valor de prova; imponha no banco (REVOKE, triggers), não só no combinado do time.

  2. Uma perna só (single-entry)

    Registrar 'creditei 100 na Ana' sem a contrapartida deixa a pergunta 'de onde veio?' sem resposta — e um bug que credita sem debitar cria dinheiro invisível. A segunda perna é o que faz a soma-zero funcionar como alarme.

  3. Float para dinheiro

    0.1 + 0.2 !== 0.3 em ponto flutuante, e num ledger cada erro de arredondamento é uma divergência de conciliação. Centavos em inteiro (ou tipo decimal do banco) — sempre.

  4. Saldo recalculado do zero a cada consulta

    SUM sobre o histórico inteiro funciona no protótipo e morre com um milhão de linhas. Materialize o saldo por conta e use snapshots periódicos como ponto de partida da recontagem — o ledger é fonte de verdade, não índice de leitura.

06Decisão

Quando usar × quando evitar

Use quando

3
  • Dinheiro ou valor de terceiros sob sua guarda — carteira, saldo, cotas.
  • Créditos e pontos que geram contestação e precisam de extrato.
  • Sistemas sujeitos a auditoria ou regulação financeira.

Evite quando

3
  • Contadores descartáveis sem valor probatório (likes, views).
  • Estado que não é soma de movimentos — perfil, configuração, catálogo.
  • Protótipos em que a modelagem contábil atrasaria a validação da ideia.

Testar o que ficou

4 armadilhas de Ledger (livro-razão) viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • extrato bancárioCada linha é um lançamento que nunca se apaga; o saldo não é um campo editável, e sim a soma de todos os lançamentos até ali.
  • balance transactions do StripeO Stripe expõe o razão como uma lista de movimentos imutáveis, e o saldo disponível é derivado deles — nada é sobrescrito.
  • blockchainUm livro-razão distribuído em que blocos só são anexados e cada um referencia o anterior, tornando a reescrita do passado detectável.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Partidas dobradas: débito e crédito se equilibram.
lancar({ debita: caixa, credita: receita, valor });

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • O saldo deixa de ser uma coluna e passa a ser derivado da soma dos lançamentos, o que pede cache ou snapshot para ler rápido
  • Corrigir um erro vira um lançamento de estorno, nunca um UPDATE — o modelo mental muda

Não vale se: não há exigência de auditoria nem histórico de movimentações — uma coluna de saldo é mais simples de ler e manter.

Onde isto já custou caro

Incidentes reais que este conceito explica.

Onde isto aparece

Continue por aqui