Bastam dois tipos de transação

acerto de contas

Uma versão antiga do HiBill registrava dinheiro trocando de mãos de quatro jeitos diferentes: uma despesa, um empréstimo, uma devolução e um acerto. Cada um tinha sua própria tabela, sua própria tela, suas próprias regras.

Hoje são dois. Veja por quê, e o que a gente aprendeu desmontando esse modelo.

Três objetos que, no fundo, eram um só

Olhe o que três dos quatro guardavam de verdade.

Um empréstimo era: o dinheiro foi de A para B, e agora B deve a A.

Uma devolução era: o dinheiro foi de B para A, abatendo parte do que B devia.

Um acerto era: o dinheiro foi de B para A, abatendo parte do que B devia — e a gente marcava como definitivo.

Três nomes. Um acontecimento. O dinheiro foi de exatamente uma pessoa para exatamente uma pessoa. As diferenças eram todas de narrativa — o que o usuário quis dizer com aquilo —, não de estrutura. E a gente tinha codificado essa narrativa no schema, onde ela se cristalizou em três caminhos de código que precisavam ficar sincronizados para sempre.

A pista estava no campo de status do acerto. Os acertos tinham os estados pending, paid e cancelled. A interface gravava paid toda santa vez. A gente tinha construído uma máquina de estados e nunca usou, porque, na vida real, você registra um acerto depois que o dinheiro já mudou de mãos. Estado morto num schema é um sinal forte de que aquele objeto não deveria existir.

O que é diferente de verdade

Só uma distinção sobreviveu ao contato com o uso real: quantas pessoas há em cada ponta.

Uma despesa é de um para muitos. Alguém (ou várias pessoas) pagou, e um grupo de pessoas divide o custo. A parte interessante é a divisão.

O cabeçalho e o detalhamento de uma transação: €186.40 pagos por você, divididos entre 4, e €139.80 que devem a você

Uma transferência é de um para um. O dinheiro saiu de uma pessoa e chegou a outra, por inteiro. Não há divisão para calcular — quem recebe fica com o valor todo.

Essa é uma diferença estrutural de verdade, e é a única. Então esses são os dois tipos.

Tudo o que o antigo modelo de quatro objetos conseguia expressar, este também expressa:

O que você quer registrar

Como

Jantar dividido entre cinco

Despesa

Emprestar €50 a um amigo

Transferência

Devolver esses €50

Transferência, no sentido contrário

Zerar um saldo

Transferência

Acertar as contas deixou de ser uma cerimônia especial. Não tem tela própria nem status separado. É uma transferência que, por acaso, leva um saldo a zero, e aparece na linha do tempo junto com todo o resto, porque é isso que ela é. Registrar uma é igual a registrar qualquer outra coisa: escreva dei 15 pra anna, e a revisão lê como uma transferência — Você deu €15.00 para Anna Lopes — sem divisão nenhuma para conferir.

“dei 15 pra Anna Lopes” lido como você deu €15.00 para Anna Lopes

As invariantes foram para um lugar só

Juntar os tipos fez com que as regras pudessem ser escritas uma vez e aplicadas uma vez. Existe um único arquivo que responde “esta transação é válida?”, e ele diz mais ou menos isto:

Para uma despesa: quando há pagadores, os valores deles precisam somar o total — uma despesa também pode começar vazia, com as pessoas incluídas depois ou chegando por um compartilhamento, ao reivindicar a sua parte. Os participantes devem partes dela.

Para uma transferência: exatamente um pagador, exatamente um recebedor, e a parte de quem recebe é o valor inteiro.

Esse é todo o regulamento específico de cada tipo. Quando a gente tinha quatro objetos, regras equivalentes ficavam espalhadas por quatro serviços e foram se distanciando — o caminho com vários pagadores tinha verificações que o caminho do acerto não tinha. Agora existe uma porta só, e tudo passa por ela.

A única exceção

Existe um terceiro tipo no banco de dados que você nunca cria: Simplificação.

Quando o sistema encontra um ciclo de dívidas — você deve a Marco, Marco deve a Aya, Aya deve a você —, ele pode encolher esse ciclo sem mudar quanto cada um realmente tirou do bolso. Esses ajustes precisam ser registrados como lançamentos de verdade, senão os saldos mudariam sem explicação.

Então eles são transações, com o rótulo Simplificação, na linha do tempo, onde você pode vê-las. A regra que a gente se impôs: nenhum saldo muda sem um motivo visível. Um número que muda em silêncio é pior que um número um pouco alto demais.

Valeu a pena?

A reescrita apagou mais código do que acrescentou, o que costuma ser bom sinal. Mas o ganho de verdade estava no que ficou mais fácil depois.

Divisões abertas, atribuição dos itens do recibo, saldos em várias moedas, adicionar pessoas que ainda não se cadastraram — cada uma dessas coisas antes precisava de quatro implementações, uma por objeto, e por isso cada uma tinha sido adiada sem alarde. Com dois tipos e invariantes compartilhadas, elas ficaram pequenas.

Esse é o argumento para enxugar um modelo, e ele não tem muito a ver com elegância. Cada objeto a mais num schema é um multiplicador no custo de cada recurso que você ainda vai criar. A gente estava pagando 4× em tudo para preservar distinções que só existiam na nossa cabeça.


Os dois tipos já estão no HiBill hoje. Se você quer saber em detalhe como os saldos funcionam em cima deles, o complemento deste texto é Entendendo os saldos.

Continue lendo