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.
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.
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.
Anatomia de um ledger
Camada 1/3Lançamentos
use ← →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árioSe 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.
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 — sempreCasos 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.
Armadilhas
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.
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.
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.
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.
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
- Append-onlyUma estrutura onde a única escrita permitida é acrescentar no fim: nada se edita, nada se apaga. Corrigir é gravar um registro novo que supera o anterior — e é assim que funcionam o WAL do seu banco, o Kafka, o git e o seu extrato.
- Event SourcingEm vez de guardar o estado atual, o sistema guarda a sequência imutável de eventos que o produziu — o estado é sempre derivado por replay, e o histórico é a fonte da verdade.
- Race conditionDuas execuções leem o mesmo estado, decidem com base nele e gravam — e o resultado passa a depender de quem chega primeiro. É o bug que nunca aparece no teste e vende o mesmo ingresso duas vezes na sexta à noite.