Blog

Partidas dobradas explicadas para desenvolvedor.

A primeira vez que me mandaram usar partidas dobradas num sistema, achei burocracia. Por que dois lançamentos pra registrar uma coisa que aconteceu uma vez? Some no lugar certo e pronto.

Levei um tempo pra entender que não é notação contábil. É uma invariante que o sistema consegue verificar sozinho, e ela pega uma classe de bug que nenhum teste unitário pega.

A ideia inteira em uma frase

Dinheiro não aparece nem desaparece. Ele muda de lugar.

Toda vez que algo acontece, o valor sai de uma conta e entra em outra. Você registra as duas pontas. A soma das pontas de uma operação é zero.

Cliente paga R$ 250 de assinatura:

  caixa_gateway          +25000
  receita_assinatura     -25000
                         ------
                              0

Se a soma não der zero, aconteceu um bug. Não “talvez”: aconteceu. Isso é raro e valioso em software, ter uma verificação que aponta erro com certeza e sem falso positivo.

Débito e crédito, sem a parte que confunde

O jargão contábil aqui atrapalha mais do que ajuda. “Débito” não quer dizer saída e “crédito” não quer dizer entrada. Depende do tipo da conta, e é exatamente isso que faz todo dev desistir na primeira página.

Se você está construindo o sistema do zero, use sinal. Positivo entra, negativo sai, e cada conta tem um tipo que diz o que saldo positivo significa nela.

create table ledger_entry (
  id             bigserial primary key,
  transaction_id uuid not null,
  account_id     uuid not null,
  amount_minor   bigint not null,  -- em centavos, inteiro, sempre
  created_at     timestamptz not null default now()
);

Se depois for preciso conversar com o contador ou exportar pra ERP, você mapeia sinal para débito e crédito na camada de relatório. A conversa fica com quem fala a língua, e o núcleo continua simples de raciocinar.

As contas que não são de gente

O truque mental que mais ajuda: contas não são só de clientes.

Um sistema de pagamento tem conta do cliente, mas também conta do gateway, conta de receita da plataforma, conta de taxa, conta de repasse pendente, conta de imposto retido. Cada perna de uma operação encosta em alguma dessas.

Uma venda com taxa vira isso:

Venda de R$ 100 com taxa de 5%:

  caixa_gateway            +10000
  conta_vendedor            -9500
  receita_taxa               -500
                           ------
                                0

Repare que o vendedor recebeu 95 e a plataforma reconheceu 5 de receita, num movimento só, e a soma continua zero. Sem as contas internas, essa taxa vira uma subtração escondida numa função e ninguém consegue auditar quanto a empresa ganhou no mês sem reprocessar pedido por pedido.

Onde a invariante te salva

O ganho aparece numa query que roda toda madrugada:

select transaction_id, sum(amount_minor) as desbalanceio
from ledger_entry
group by transaction_id
having sum(amount_minor) <> 0;

Vazio significa que a contabilidade fecha. Qualquer linha é um incidente detectado no mesmo dia, não em março quando o contador reclamar.

Já vi bug de arredondamento em split de pagamento aparecer exatamente assim: dividir R$ 100 em três partes gera 33,33 três vezes e sobra um centavo. Sem a verificação, esse centavo some silenciosamente por meses e ninguém sabe quantos milhares ele virou. Com a verificação, aparece no primeiro dia, com o ID da transação.

A regra prática: quando não dividir exato, o resto vai explicitamente para uma conta de ajuste. Nunca desaparece por arredondamento.

O que não fazer

Não permita UPDATE em lançamento. Errou, registra o inverso e depois o correto. O histórico mostrando o erro e a correção é uma qualidade, não um constrangimento.

Não guarde saldo como coluna que você incrementa. Saldo é derivado da soma. Se quiser cache por performance, deixe claro que é cache e reconstruível.

Não misture moedas na mesma conta. Uma conta, uma moeda. Conversão é uma transação com duas pernas em contas diferentes e uma taxa registrada.

E não use float. Nem double, nem real. Inteiro em centavos. Todo sistema financeiro que usa ponto flutuante tem um bug de centavo, ele só ainda não foi encontrado.

Vale a pena para o meu caso?

Se o seu produto movimenta dinheiro de terceiro, sim, sem discussão.

Se movimenta crédito, cota, ponto ou qualquer unidade que o usuário acumula e gasta, provavelmente também. A pergunta que decide é: alguém um dia vai pedir o extrato disso? Se a resposta é sim, você precisa da história, e a história é o ledger.

Escrevi sobre o desenho completo em o que é um core ledger.

← Voltar pro blog