Traduzido do artigo original em inglês. Ler em inglês

A maioria dos sistemas financeiros começa do mesmo jeito. Você está construindo uma carteira, um usuário tem $100, envia $30 para um amigo, então você escreve balance = balance - 30 em uma linha e balance = balance + 30 na outra. O saldo vira só um número que você guarda na carteira e atualiza a cada vez, como o total de um gift card. Funciona, e você sobe isso pra prod desse jeito.
Aí a empresa cresce, e agora você tem cartões de débito/crédito, rails de cripto, pagamentos em fiat, taxas, reembolsos, alguns provedores de pagamento, um provedor de carteira. O sistema financeiro se espalhou por serviços e tabelas de times diferentes. E num belo trimestre, o financeiro te avisa que $40.000 estão sem explicação, ninguém encontra.
O dinheiro tecnicamente não sumiu. Ele está na sua conta bancária, cada centavo está contabilizado. O que sumiu foi a atribuição, a referência, o registro de a quem ele pertence. Uma coluna balance no seu db guarda um número, mas não a história toda por trás dele, então quando o número e a realidade discordam, não tem como refazer o caminho de como ele chegou ali. Todo engenheiro que já trabalhou em algum sistema financeiro importante tem alguma versão dessa história de terror, pode perguntar!
A solução não é só uma coluna balance mais bem pensada. É um modelo que bancos e sistemas financeiros tradicionais usam há anos, e no qual todo ledger sério ainda roda hoje: partidas dobradas.
Guardar dinheiro como um número mutável, em partida simples, falha por três motivos principais, e eles costumam aparecer às 2h45 da madrugada.
E você não consegue rastrear para entender o que aconteceu, porque não existe um histórico claro. balance = 70 não diz nada sobre as movimentações que levaram a ele, é só um número. Quando ele está errado, não tem nada para auditar e refazer o caminho de volta.
Não existe um segundo lado. Subtrair $30 de uma carteira não registra para onde os $30 foram. A origem e o destino do dinheiro só existem na linha de código da aplicação que por acaso rodou, e não nos dados do sistema central.
Mais do que isso, não existe proteção nenhuma contra as race conditions que vão/podem acontecer. Duas requisições leem balance = 100 ao mesmo tempo, cada uma subtrai 30, e cada uma grava 70. O dinheiro se moveu duas vezes e a coluna ainda vai parecer correta.
Um ledger de partidas dobradas troca esse número mutável por um registro append only de movimentações, construído a partir de três objetos.
Uma Conta (Account): É simplesmente um pool independente de valor. A carteira de um usuário, o caixa da empresa, as taxas que você cobrou, cada um deles é uma conta.
Um Lançamento (Entry): Agora é um único débito ou crédito contra uma única conta. Um lançamento SEMPRE DEVE ser escrito uma vez e nunca alterado ou apagado. Cada um é um registro permanente de valor entrando ou saindo de uma única conta "imutável".
Uma Transação (Transaction): É um grupo de lançamentos que se movem como um só, tudo ou nada. É esse o objeto que mantém o seu sistema inteiro de pé.
Você não escreve lançamentos diretamente. Você escreve uma transação, e uma transação carrega os seus lançamentos. A própria escrita roda dentro de uma única txn do db, então é tudo ou nada, tx, _ := db.Begin(); tx.Exec(...); tx.Commit(). Por exemplo, uma transferência de $30 do Alfred para o Tobi é uma transação com dois lançamentos, um que tira do Alfred e outro que vai dar ao Tobi, e ou os dois são commitados ou nenhum deles é. Nunca deveria existir um estado válido em que o Alfred foi debitado e o Tobi nunca foi creditado. Esse agrupamento atômico é a garantia.
Agora, essa é a próxima parte que confunde bastante gente. Um débito não é uma subtração e um crédito não é uma soma. Eles são simplesmente as duas únicas direções que uma movimentação pode tomar, e se uma determinada direção aumenta ↑ ou diminui ↓ uma conta depende do tipo de conta em que ela cai. Pra ficar mais claro, você pode ler de novo a seção "Os três princípios centrais"
Toda conta é de um entre poucos tipos possíveis, como falamos acima, e é o tipo que define a sua natureza, o lado que faz o saldo subir. Então você só lida com dois. Primeiro, as contas de natureza devedora, ativos e despesas. Um débito é o que aumenta essas contas, um crédito é o que vai diminuí-las. Depois, as contas de natureza credora, passivos, patrimônio líquido e receitas. Um crédito é o que aumenta essas contas, um débito é o que vai diminuí-las.
A diferença fica clara no momento em que você aplica isso a um exemplo de carteira. Os $100 que um usuário guarda com você não são dinheiro seu, é dinheiro que você deve a ele, então nos seus livros isso é um passivo, de natureza credora. Os dólares de verdade que você guarda em nome dele são um ativo, que é de natureza devedora.
Agora um depósito passa a fazer sentido. Por exemplo, um usuário deposita 100 USDC, e duas coisas passam a ser verdade ao mesmo tempo: você está guardando 100 USDC a mais, um ativo aumentando, que é um débito, e você deve 100 USDC a mais ao usuário, um passivo aumentando, que é um crédito. Aconteceu uma movimentação, dois lançamentos, e eles se equilibram.

Débito e crédito não são opostos em conflito. São duas visões dos mesmos $100, registradas ao mesmo tempo, uma única transação.
Fazendo uma analogia com a física, existe outra forma de olhar para isso e entender tudo, que é a famosa terceira lei do movimento de Newton "Para toda ação existe uma reação igual e oposta"
Em qualquer transação, os débitos e os créditos têm que dar o mesmo valor. Toda santa vez, zero exceções. Σ débitos − Σ créditos = 0
Uma força nunca aparece sozinha, ela sempre chega com a sua gêmea igual e oposta. O valor, nesse cenário de ledger, funciona exatamente do mesmo jeito. Ele nunca cai em uma conta sem um lançamento igual e oposto em outra, e os dois mantêm os livros equilibrados.
Os 100 USDC que você debita na sua custódia e os 100 USDC que você credita ao usuário têm o mesmo tamanho, mas não são a mesma coisa, um é um ativo que agora você tem, o outro é um passivo que agora você deve. Então nada se move aqui, em vez disso os dois lados crescem ao mesmo tempo, um novo ativo e um novo passivo igual passam a existir juntos, e é exatamente por isso que os livros continuam equilibrados.
Em toda transação, o total de débitos tem que ser igual ao total de créditos. Uma transação que não fecha não pode existir em lugar nenhum.
Essa regra é o que transforma "nunca perder dinheiro" de uma esperança em um atributo central do sistema em que você está trabalhando. O valor não consegue sair de uma conta sem chegar em outra, porque senão a transação não fecha. A sua custódia não deveria se distanciar do que você deve aos seus usuários, porque toda movimentação registra os dois lados de uma vez. Quando algo está errado, o ledger não te entrega um saldo silenciosamente incorreto, ele rejeita a escrita na hora.
Na prática, o núcleo de um ledger é essa única checagem. Neste caso os valores são guardados como inteiros não negativos em unidades menores (centavos, ou wei), e a direção vem de um campo side em vez de um sinal:
type Entry = { accountId: string; side: "DR" | "CR"; amountMinor: bigint; assetId: string };
function assertBalanced(entries: Entry[]) {
if (entries.length === 0) throw new Error("empty txn");
const net = new Map<string, bigint>();
for (const e of entries) {
if (e.amountMinor <= 0n) throw new Error(`bad amount: ${e.accountId} ${e.amountMinor}`);
const delta = e.side === "DR" ? e.amountMinor : -e.amountMinor;
net.set(e.assetId, (net.get(e.assetId) ?? 0n) + delta);
}
for (const [asset, balance] of net) {
if (balance !== 0n) throw new Error(`unbalanced: ${asset} off by ${balance}`);
}
}Duas coisas ali importam mais do que parecem. O saldo é checado por ativo, então uma transação que mexe com dólares e bitcoin tem que fechar em cada um de forma independente, uma moeda nunca pode liquidar outra silenciosamente. E os valores são bigint, não ponto flutuante, porque ponto flutuante e dinheiro é outra história de terror para outro dia.
Mais uma coisa para reparar: em nenhum lugar daquele trecho uma coluna balance é tocada. O código só checa se os lançamentos fecham, ele nunca grava um saldo em lugar nenhum. E esse é o ponto de partida certo, um saldo nunca é um fato que você define diretamente. Você não escreve balance = 500. Você soma os lançamentos, debits - credits, lidos de acordo com a natureza da conta. Os lançamentos são a verdade. O saldo é só uma pergunta que você faz a eles.
Agora tem outra coisa que dá para errar aqui. Ser definido pelos lançamentos não significa que você soma tudo de novo a cada leitura. Se você fizer isso, vai simplesmente somar o histórico inteiro de uma conta toda vez, isso é O(n), e o n só cresce. A conta que toda transação toca, a sua tesouraria e por aí vai, acabaria sendo a leitura mais lenta. Então você não soma de novo. Você guarda o saldo, atualiza ele no momento em que cada lançamento entra, e mantém ele em sincronia com o log. O log continua sendo a fonte da verdade. O número guardado é na verdade só um cache que tem que concordar com ele. Derivado no significado, armazenado na prática, os dois, não um ou outro.
A mesma ideia em uma imagem, uma transação se abrindo em dois lançamentos equilibrados que caem em duas contas:

Um sistema de partida simples pede que você confie em um número. Um sistema de partidas dobradas se recusa a deixar um número se sustentar sozinho, um saldo não é algo que você afirma, é algo que os lançamentos obrigam a ser verdade. Do mesmo jeito que um índice único torna duplicatas impossíveis, e não só algo que você não deveria fazer, o conjunto de regras de equilíbrio faz com que perder dinheiro seja uma escrita que não comita, em vez de uma diferença que você descobre anos ou meses depois, com a sua licença sendo questionada numa audiência. É por isso que quase todo sistema financeiro é construído assim.
Esta primeira parte também é só o esqueleto. Um ledger de verdade tem que lidar com fundos que ainda não foram liquidados, um depósito cripto esperando confirmações de bloco, um saque que pode falhar e precisar ser revertido para o usuário que o iniciou. Na Parte II, vamos olhar esse modelo em movimentações reais, e ver um único saldo virar três.
Receba os artigos mais recentes no seu e-mail.