Manual de Engenharia Fintech
(w.pitula.me)- Sistemas que tratam dinheiro como estado central devem ser projetados com os princípios de não criar dados, não perder dados e não confiar em nada
- A representação de valores deve evitar
floate combinarBigDecimal, inteiros na menor unidade e números racionais conforme a responsabilidade, e a serialização de números em JSON também pode recriar o problema do IEEE-754 double - O livro-razão deve manter partidas dobradas, trilha de auditoria imutável, separação entre value time, booking time e settlement time, e registros de correção e cancelamento para permitir reconstruir saldos e relatórios
- O fluxo real do dinheiro deve evitar gastos duplicados e omissões com reservas, idempotência, máquinas de estado reiniciáveis, validação de APIs externas e webhooks, outbox, CDC e reconciliação (reconciliation)
- Controle de acesso, aprovação four-eyes, rastreamento de mudanças no SDLC, testes baseados em propriedades e injeção de falhas fazem com que até operadores internos e mudanças de código sejam tratados como fronteiras de confiança
Princípios básicos dos sistemas fintech
- Em engenharia de software onde dinheiro é a principal preocupação do sistema, rastreabilidade, imutabilidade e verificabilidade são muito mais importantes do que CRUD genérico
- O público-alvo são pessoas que acabaram de entrar em fintech, pessoas que já trabalham em fintech e pessoas de fora do setor que querem entender como sistemas de dinheiro diferem de sistemas comuns
- Todos os padrões são meios para preservar três princípios
- No invented data: dinheiro não pode ser criado do nada, então não se permite processamento duplicado nem alteração arbitrária de saldo
- No lost data: tudo o que aconteceu com o dinheiro deve ser rastreado e persistido
- No trust: não se confia em provedores externos, componentes internos nem no mundo real sem validação
Como representar dinheiro
- A representação de valores é a decisão mais fundamental de um sistema financeiro e, se for escolhida de forma errada, toda a camada superior herdará erros
- float/double quase nunca é uma boa escolha, porque pode causar perda de precisão imprevisível
- Tem as vantagens de ser rápido, eficiente em memória e não exigir bibliotecas extras nem estruturas de dados adicionais
- Tipos de precisão arbitrária como
BigDecimalpermitem controlar explicitamente a precisão dos cálculos e o ponto de arredondamento- São adequados para cálculos intermediários em sequências de operações, como FX ou cálculo de preços
- Armazenar como inteiro na menor unidade é a abordagem usada pela maioria das moedas fiduciárias, com precisão fixa como nos sistemas de bancos centrais
- €12.34 é armazenado como
1234 - Deve-se seguir a quantidade de casas definida pela ISO 4217, sem assumir sempre 2 casas
- Criptomoedas também usam inteiros na menor unidade, como satoshi e wei, mas a precisão varia por ativo e é definida pelo token, como em
decimalsdo ERC-20 - Valores em criptomoedas podem ultrapassar inteiros de 64 bits, então pode ser necessário usar inteiros de largura arbitrária
- €12.34 é armazenado como
- Números racionais são a opção mais poderosa quando não se pode permitir perda de precisão, mas são lentos, difíceis de converter para outros formatos sem perder exatidão e normalmente exigem tipos ou bibliotecas customizadas
- A forma de armazenamento e a forma de cálculo são decisões separadas, e um mesmo sistema pode usar armazenamento em inteiro com cálculos intermediários em
BigDecimal - O tratamento de borda também é importante na serialização de valores
- Números JSON comuns são IEEE-754 double na maioria dos parsers, então, mesmo com cuidado na representação interna, o problema de
floatpode reaparecer na fronteira - Dinheiro deve ser enviado como string, como
"12.34", ou como inteiro na menor unidade
- Números JSON comuns são IEEE-754 double na maioria dos parsers, então, mesmo com cuidado na representação interna, o problema de
Arredondamento e tratamento de moedas
- Arredondamento é inevitável em divisões, conversão de moeda, taxas, juros, aplicação de proporções e mudança de precisão, então não deve ficar implícito
- A estratégia de arredondamento é uma decisão de negócio
- Em alguns casos é preciso arredondar para baixo de forma conservadora, e em outros pode-se usar half-even por efeito estatístico
- Definir quem fica com a fração residual pode ter impacto jurídico e tributário
- Deve-se manter a precisão total pelo maior tempo possível e, em geral, arredondar apenas nas fronteiras, como antes de persistir ou exibir ao usuário
- Se um valor for dividido em várias partes e depois arredondado, a soma das partes pode diferir do valor original
- Dependendo do caso, pode ser necessária uma conta de arredondamento explícita
- Dinheiro não pode ser representado apenas por um número; ele sempre deve ser tratado junto com a moeda
- Agrupar valor e moeda em um newtype, struct, class ou record como
Moneyreduz a chance de erro - Soma entre moedas diferentes deve ser proibida, e a conversão deve ser feita explicitamente com taxa cambial rigidamente controlada
- Não se deve aceitar códigos de moeda arbitrários; eles devem ser validados contra um conjunto controlado nas fronteiras do sistema
- Códigos de moedas fiduciárias podem servir como identificadores, mas criptoativos exigem identificações mais complexas, como
(network, contract address) - Criptomoedas pegged, bridged e wrapped não são equivalentes ao ativo subjacente
- Agrupar valor e moeda em um newtype, struct, class ou record como
Taxas de câmbio FX
- Uma FX rate sempre tem direção
- A taxa EUR/USD não é o simples inverso de USD/EUR
- Em exchanges, compra e venda são ordens com preços diferentes por causa do bid/ask spread
- O momento da taxa também muda o resultado
- A taxa do momento atual é usada para calcular o valor atual de posições ou de transações assumidas como ocorrendo agora
- A taxa da value date é usada para cálculo de variação de valor ou de imposto
- Em conversão, dois tipos de taxa são importantes
- Transactional rate é a taxa em que a conversão realmente ocorreu, derivada do valor original e do valor resultante
- Reference rate é usada para avaliação e julgamento de equivalência, como valor de posição ou base tributária, e não é o preço real de negociação
- Não existe uma única taxa padrão
- A taxa vem do mercado e varia conforme o local de negociação ou o método de cálculo
- A taxa do banco central é a mais próxima de um padrão, mas só pode ser usada como reference rate, e outras fontes também podem ser válidas
- É preciso armazenar junto o valor e a origem da reference rate para poder validar depois
Livro-razão e partidas dobradas
- A movimentação do dinheiro deve ser registrada de forma auditável e reconstruível mesmo anos depois
- Partidas dobradas são uma forma amplamente usada de armazenar transações financeiras como uma lista de entries no formato
(credit account, debit account, amount)- Na forma clássica, cada movimentação tem uma linha de débito e uma linha de crédito separadas
- Como toda entry move o mesmo valor de uma conta para outra, o livro-razão permanece sempre balanceado
- Dinheiro sempre tem origem e destino
- Provedores externos também devem ter contas dedicadas para que o dinheiro que entra e sai do sistema possa ser rastreado
- O saldo não deve ser armazenado; ele deve ser derivado da movimentação do dinheiro
- Contas têm tipos como asset, liability e equity
- A accounting equation
assets = liabilities + equityé mantida - Na prática, também são necessárias contas de revenue e expense para registrar receita de tarifas ou perdas por write-off
- A forma expandida é
assets = liabilities + equity + revenue - expenses
- A accounting equation
- Uma única transação geralmente cria várias movimentações
- A movimentação líquida e a movimentação de taxa podem ser registradas separadamente
- Uma entry lançada é, por convenção, imutável, e correções são feitas adicionando uma nova entry que compensa a original
Modelo de tempo: value, booking, settlement
- Uma transação normalmente tem dois ou mais timestamps, e às vezes três
- Value time: quando a transação realmente aconteceu
- Booking time: quando foi registrada no sistema
- Settlement time: quando o dinheiro foi de fato transferido ou efetivado
- Settlement time não existe em todas as transações e geralmente é expresso como T+X
- T+2 significa que o settlement acontece dois dias após a value date
- Value time e booking time quase sempre diferem
- Se booking > value, é backdated, o que é especialmente importante quando o período de relatório muda
- Se booking < value, é forward-dated, como em pagamentos agendados ou pagamentos com data futura
- Um exemplo de pagamento com cartão é: o pagamento acontece em T1, o sistema registra em T2 e o provedor de pagamento transfere o dinheiro para a conta em T3
- Relatórios de negócio geralmente olham para value time ou settlement time, enquanto booking time é útil para rastreabilidade
- Se vários momentos forem condensados em um único
created_at, perde-se informação que depois não pode ser reconstruída
Trilha de auditoria, event sourcing e imutabilidade
- Sistemas financeiros estão sujeitos a auditoria regulatória e podem precisar provar se houve mistura entre fundos dos usuários e da empresa, se a receita é explicável, se o que foi informado a terceiros corresponde à realidade e se os fundos estão protegidos
- audit trail é o histórico completo não só do estado atual, mas também de como esse estado foi produzido
- o que aconteceu
- quando aconteceu
- quem ou o que disparou isso
- por que aconteceu
- Além da movimentação de dinheiro, intervenções manuais, alterações de configuração como fee schedule, rate source e limites, além de mudanças de permissão, também precisam de trilha de auditoria
- Para decisões como compliance check ou risk score, pode não ser suficiente armazenar apenas o resultado
- se isso estiver em uma decision table ou rules engine como DMN, Drools ou Decisions4s, a estrutura se torna reproduzível, permitindo ver quais regras foram executadas com quais entradas e quais resultados produziram
- Event sourcing é uma abordagem sistemática para criar uma audit trail
- em vez de armazenar separadamente o estado atual e o log, armazena-se apenas os eventos e o estado é derivado deles
- o ledger de partidas dobradas é um exemplo desse padrão, pois não armazena o balance, mas o calcula a partir dos entries
- Event sourcing também tem grandes restrições práticas
- não é necessário em todo lugar e, se o ledger já cobre o dinheiro, os domínios ao redor podem ser atendidos por um modelo comum e um change log confiável
- por desempenho, é possível fazer cache ou snapshot de balance e projections
- consultar a fonte original dos eventos com eficiência pode ser difícil, o que pode aumentar o trabalho de projection
- como os eventos permanecem por anos, o código de hoje precisa conseguir ler eventos de muito tempo atrás
- Uma trilha de auditoria não serve como evidência se puder ser alterada, então ela precisa ser append-only
- tabela append-only, remoção de
UPDATEeDELETEdas permissões do banco, bloqueio de operações mutáveis na camada de aplicação e tamper evidence com checksum ou hash chain são ferramentas úteis
- tabela append-only, remoção de
- Em sistemas reais, às vezes é preciso corrigir o event log ou a audit trail por causa de bugs
- em geral, os dados devem ser fixados depois de terem sido reportados externamente; antes disso, se o problema for encontrado antes de sair do sistema, pode ser possível corrigir no próprio lugar
Cancelamento e correção
- Erros como posting com valor incorreto ou posting na conta errada acontecem
- A imutabilidade exige uma forma de corrigir dali para frente
- faz-se um novo compensating entry e liga-se esse lançamento ao registro original em ambas as direções
- Reversal compensa totalmente o original do ponto de vista econômico, como se nunca tivesse existido, mas tanto o original quanto o reversal permanecem no histórico
- Correction ou adjustment registra a diferença entre o valor lançado e o valor correto, ou então reverte e depois faz um novo posting com o valor certo
- A correção pode cair em um período de reporte diferente do original
- é preciso haver informação de vínculo para que os relatórios atribuam a correção corretamente e distingam a atividade real da limpeza posterior
- Como normalmente não se permite backdate em períodos de reporte já fechados, definir um value time passado em uma correção depende do calendário de reporte
Execução do fluxo de dinheiro: invariantes e reserva de fundos
- Invariant é uma propriedade que deve ser sempre verdadeira no sistema
- a equação contábil é um exemplo, e stakeholders do negócio podem definir várias outras condições
- As formas de impor invariantes se complementam
- projetar a criação para que apenas objetos válidos possam ser criados
- verificar em runtime com assertions, testes e property-based testing
- analisar os dados armazenados depois do fato com jobs de reconciliation ou verificações noturnas
- Transações que interagem com o mundo externo precisam evitar race conditions
- é preciso impedir situações em que a insuficiência de saldo só seja descoberta após a chamada externa ou em que o mesmo dinheiro seja gasto duas vezes
- Funds reservation ou hold-and-release é um padrão que reserva fundos para uma transação específica antes da interação externa
- em caso de sucesso, a reservation é settled e a transação prossegue
- em caso de falha, ela é released e volta para o available balance
- Esse padrão distingue total balance de available balance
available = total - reserved- a verificação de saldo e o registro de uma nova reservation são feitos com base no available balance
- O valor final pode ser diferente da estimativa inicial
- se taxas ou câmbio mudarem, reserva-se o valor estimado, faz-se o settle do valor real e depois libera-se o restante
- Toda reservation deve necessariamente ser settled ou released
- uma reservation órfã bloqueia fundos do usuário, mas não perde nem cria dinheiro
- expiry ou timeout podem servir como rede de segurança, mas não são obrigatórios
- A verificação de saldo e o registro da reservation precisam ser linearizáveis
- com stale read, duas transações podem passar na verificação e acabar sendo suportadas pelos mesmos fundos
Overdraft e idempotência
- Overdraft é a situação em que o saldo de uma conta fica negativo
- Um overdraft intencional é um produto de crédito com limite e juros, normalmente modelado como uma conta separada de overdraft
- Um overdraft não intencional pode acontecer mesmo que a política o proíba
- o settlement pode vir maior que a estimativa da reserva, ou um reversal pode chegar depois de os fundos já terem saído
- funds reservation reduz a janela, mas não a elimina
- “Proibido” e “impossível de representar” são coisas diferentes
- impedir a representação de saldo negativo com unsigned integer ou
CHECK (balance >= 0)pode levar, quando a realidade exigir saldo negativo, a crash, clamp para zero ou tratamento incorreto - fazer clamp do saldo negativo para 0 cria dinheiro
- impedir a representação de saldo negativo com unsigned integer ou
- Quando um overdraft é detectado, ele deve ser tratado como um sinal para investigação, e depois recuperado ou tratado explicitamente por meios como depósitos futuros e compensação, pedido de quitação ou write-off em uma conta de despesa/perda
- Em sistemas distribuídos, não é possível garantir exactly-once delivery, então retries são necessários, e retries podem gerar entregas duplicadas
- Idempotency é a propriedade de fazer com que o processamento só produza efeito uma vez, mesmo que a mesma mensagem seja entregue duas vezes
- uma idempotency key explícita costuma ser mais simples e segura do que deduplicação baseada no payload
- a key deve ser limitada a uma operação específica e ao escopo do cliente
- é preciso decidir se erros serão reproduzidos ou reprocessados, e para erros permanentes normalmente é mais simples reproduzi-los como estão
- validar se o payload de uma chamada duplicada é igual ao original é uma boa prática, mas traz custo de complexidade de implementação e de flexibilidade
- em grande escala, é preciso deduplicar bilhões de requisições e ter uma barreira atômica para acessos concorrentes
- uma janela de idempotência de 24 horas, por exemplo, simplifica a implementação, mas tem custo de corretude
- também é necessário testar retries e o tratamento de retries fora de ordem
Fluxos retomáveis
- O fluxo de dinheiro passa por várias etapas, e é preciso assumir que ele pode morrer em qualquer ponto entre elas
- Full resumability é um design que garante que um fluxo parcialmente concluído sempre fique em um estado recuperável
- O estado de progresso deve ser armazenado em armazenamento persistente, não em memória
- o fluxo deve ser modelado como uma state machine explícita, e a conclusão de cada etapa deve ser commitada antes do início da próxima
- É necessário um driver independente para voltar a empurrar fluxos interrompidos
- scheduler, worker ou poller devem conseguir processar fluxos incompletos mesmo após um crash do orquestrador
- Ao retomar, etapas que já ocorreram parcialmente podem ser executadas de novo, então cada etapa precisa ser idempotente
- Efeitos externos não podem ser revertidos
- depois de chamar o mundo externo, não é possível voltar ao estado de não ter chamado
- é preciso seguir em frente até concluir ou, se uma etapa posterior falhar de forma permanente, registrar uma ação compensatória como no padrão saga
- Pode-se usar um mecanismo de execução durável como Temporal, Camunda, Workflows4s ou AWS Step Functions, ou construir uma state machine persistente por conta própria
Consumo de APIs externas
- APIs externas, como provedores de pagamento, custodians, nós de blockchain e fornecedores de KYC, devem ser tratadas de forma defensiva, pois não é possível controlar seu código, qualidade nem tempo de atividade
- Não se deve confiar no schema
- Pode haver campos ausentes, mudanças de tipo e
nullinesperado - Partes importantes devem ser validadas na fronteira, e dados inesperados devem falhar de forma explícita
- Validar até o que não é necessário pode causar incidentes desnecessários por violações de contrato de terceiros
- Pode haver campos ausentes, mudanças de tipo e
- Em APIs externas, é comum acontecer de token ser passado na URL, haver perda de precisão, códigos HTTP com significado diferente, corpo de erro dentro de
200, paginação inconsistente e formatos de data customizados - Toda chamada pode falhar, então timeout e retry são necessários
- Circuit breaker é, em geral, uma cortesia para servidores sobrecarregados e aumenta a complexidade do cliente
- Ainda assim, pode ser necessário para proteger recursos finitos como latência, threads e conexões
- Rate limit e quota devem ser avaliados antecipadamente para comparar o volume esperado de chamadas com os limites do provedor
- Se todas as requests e responses forem armazenadas de forma estruturada e consultável, isso serve para investigação, audit trail, evidência em disputas sobre o comportamento do provedor e material para reprocessamento após bugs
- Em áreas centrais, pode-se considerar redundância de provedores
- Isso pode incluir validar dados com dois nós de blockchain, ter parceiro bancário de backup, custodiante de cripto ou fornecedor de KYC alternativo
- O custo de desenvolvimento, taxas e complexidade é muito alto
- O sandbox pode ser muito diferente da produção, então é preciso preparar testes em produção com canary release ou uso controlado de baixo impacto
Processamento de webhooks
- Webhooks são uma forma comum de receber sinais de sistemas externos, mas tratá-los com segurança não é simples
- Não se deve assumir ordem
- As mensagens podem chegar fora de ordem ou conter dados desatualizados
- Não se deve sobrescrever o estado assumindo que o webhook recém-recebido é a verdade mais atual
- Não se deve assumir validade
- Webhooks vêm de outros subsistemas do emissor e podem conter dados desatualizados ou convertidos incorretamente
- É melhor usar o corpo do webhook apenas como gatilho e consultar a API para verificar o estado autoritativo
- A API também pode ser eventually consistent, então uma consulta imediata pode retornar o estado anterior, exigindo retry
- Não se deve assumir entrega
- Mesmo que o emissor prometa uma política forte de reentrega, webhooks eventualmente se perdem
- Um processo independente, como reconciliação, deve compensar a completude dos dados
- Também não se deve assumir entrega única
- O mesmo webhook será entregue várias vezes, e o processamento deve ser idempotente
- Deve-se reconhecer rapidamente e processar de forma assíncrona
- O evento bruto é salvo em armazenamento durável, retorna-se 2xx imediatamente e o trabalho real é feito de forma assíncrona
- O payload bruto deve ser armazenado como está
- Isso é necessário para processamento confiável, audit trail e reprocessamento após bugs
- O chamador deve ser verificado
- Em geral, o emissor anexa uma assinatura ao payload, e o receptor a valida com HMAC usando segredo compartilhado ou com assinatura assimétrica baseada em chave pública
- A validação da assinatura deve ser feita sobre os raw bytes recebidos, e não sobre o payload reserializado
- Webhooks devem ser tratados não como a verdade do que aconteceu, mas como um indício de que algo aconteceu
Notificações confiáveis: Outbox e CDC
- Quando é preciso notificar de forma confiável mudanças no sistema por canais externos, como evento no Kafka ou chamada de webhook, a transactionalidade se torna um problema
- Pode acontecer de o publish ter sucesso, mas não se receber a resposta por problema de rede e o estado do sistema ser revertido, ou de a mudança de estado ter sido commitada, mas o publish falhar
- A resposta de livro-texto é 2-phase commit ou transação distribuída, mas isso raramente é usado por causa da complexidade e da dificuldade de padronização reutilizável
- Há várias opções práticas
- Outbox pattern: registrar de forma transacional a intenção de publicação em um armazenamento dedicado junto com a mudança de estado e processá-la depois até ter sucesso
- Change Data Capture: ler o write-ahead log ou replication log do banco de dados e transformar mudanças commitadas em um stream de eventos
- Debezium e AWS DMS oferecem CDC
- CDC exporta eventos brutos no formato de linhas de tabela, então é necessário pós-processamento para evitar vazamento do schema interno
- Listen-to-yourself publica primeiro o evento e depois reconstrói o próprio estado a partir desse evento
- Em event sourcing, como o log de eventos já está no banco, basta publicar a partir dele
- Independentemente do mecanismo escolhido, a entrega é at-least-once
- Se o relay ou connector cair depois de publicar e antes de registrar, pode reenviar na reinicialização
- O consumer deve deduplicar com um ID de evento estável e operar de forma idempotente
Reconciliação
- Sistemas que dependem de dados externos são vulneráveis a data drift, em que o estado de dois sistemas diverge
- Pode-se perder um webhook, ou uma transação pode ter sido lançada no ledger sem refletir no sistema do provedor externo
- Reconciliação é o processo de alinhar dois sistemas
- Na prática, podem ser três ou mais, como ledger, processador de pagamentos e banco
- A cadência pode ser horária, diária, mensal ou anual, dependendo do contexto e das restrições
- O drift pode ser ausência de dados ou diferenças mais complexas, como valores distintos para a mesma transação
- O timing também é importante
- Se a liquidação é T+3, um registro pode permanecer unreconciled por 3 dias, então isso deve ser refletido no processo para evitar alertas desnecessários
- O algoritmo de matching é a principal dificuldade
- Em geral, armazenar internamente o ID do provedor externo simplifica o matching
- Sem isso, pode ser necessário usar heurísticas baseadas em valor e tempo
- Também pode ser necessária reconciliação one-to-many
- Uma única transferência de liquidação pode abranger várias transações
- Não se deve simplesmente sobrescrever discrepâncias para que a reconciliação bata
- É preciso suporte de primeira classe para entender e corrigir a causa, como registros de correção e reprocessamento de dados de webhook
Controles e acesso
- Sistemas de dinheiro precisam controlar não só os dados, mas também quem pode realizar quais ações, e depois provar que os procedimentos foram seguidos
- Segregation of duties é um controle para impedir que uma única pessoa detenha todo o processo
- Four-eyes, maker-checker e dual control são formas de exigir aprovação de uma segunda pessoa antes que uma ação específica seja aplicada
- Isso se aplica a ações que podem mover fundos ou representá-los incorretamente, como saques grandes ou manuais, correções manuais no ledger, movimentações de tesouraria e de cold wallet, ou mudanças em tabela de tarifas e limites
- Os mesmos controles também se aplicam à engenharia
- Merge de código, deploy em produção e mudanças de infraestrutura são ações sensíveis em sistemas de dinheiro, então exigem revisão e aprovação
- A própria aprovação também faz parte do trail
- É preciso registrar quem solicitou, quem aprovou e se eram pessoas diferentes para poder provar o controle
- É necessário um caminho de break-glass explícito e fortemente auditado para emergências
- O controle de acesso faz parte do estado do sistema e muda com o tempo
- Tanto humanos quanto serviços devem receber privilégio mínimo
- Preferir RBAC a grants por pessoa facilita a revisão
- Concessões e revogações de capacidades também são eventos sensíveis, então é preciso registrar o que mudou, quem mudou e por quê
- Revisões programadas de acesso devem identificar permission drift antigo ou incorreto
Rastreamento de mudanças no SDLC
- Em ambientes regulados, é preciso ser capaz de auditar o processo pelo qual o código chega à production
- O source control é o histórico de mudanças
- O commit history atribui todas as mudanças ao author e as conecta ao motivo por meio de review e ticket vinculado
- Deve ser protegido com signed commit, protected branch e proibição de force-push em shared history
- Review e pipeline devem ser obrigatórios
- Required review, status check e proibição de direct push na main branch são importantes
- O deployment deve ser rastreável
- Deve ser possível reconstruir qual version está em execução e quem fez o release e quando, para conectar um incident à mudança que o causou
Estratégia de testes
- Em sistemas de dinheiro, o espaço de sequências de operation é grande e as falhas interessantes surgem das combinações, por isso testes são especialmente importantes
- Property-based testing verifica propriedades que devem valer para qualquer entrada, em vez de uma saída específica
- Combina bem com invariants e money math
- Ao gerar sequências de operation, é preciso verificar os invariants não só no final, mas após cada etapa
- Como é difícil executar isso manualmente em grande escala, é necessário um testing harness que injete assertions automaticamente
- Generative idempotency testing verifica se toda operation que toca o mundo externo não tem efeito na segunda chamada
- Crash and resume injection verifica se um fluxo longo consegue se recuperar mesmo que morra entre quaisquer etapas
- Round-trip testing confirma se, após encode/decode, serialize/deserialize, convert/convert back, o sistema retorna ao ponto inicial ou permanece dentro de uma tolerance conhecida
- É uma forma rápida de detectar perda de precisão em boundaries de tipos de money e currency, além de bugs de serialization
- Golden testing compara resultados de cálculo, como fee breakdown, statement e report, com resultados esperados armazenados para revelar diffs não intencionais
- Backward-compatibility testing mantém um corpus de payloads reais em formato antigo e verifica se o código atual ainda faz deserialize e project corretamente
- Production testing pode ser necessário quando o sandbox difere muito da production
- Exemplos incluem canary release, controlled rollout com blast radius pequeno e synthetic transactions que fazem pequenos valores reais circularem continuamente
- Como o production test movimenta dinheiro real, ele deve passar igualmente por ledger, reconciliation e audit trail, e ser encerrado pelos fluxos normais de correction/reversal
Terminologia de domínio e materiais de referência
- Ao começar em fintech, vocabulary e concept podem ser mais difíceis do que o código, por isso os termos principais são organizados separadamente
- A área de contabilidade e ledger inclui ledger, general ledger e sub-ledger, debit/credit, posting, chart of accounts, receivable/payable, IOU, accrual vs cash basis, trial balance, suspense/clearing account, write-off, commingling e reconciliation break
- A área de money e FX inclui Money type, minor units, basis point, notional, fiat vs crypto, stablecoin, pegged/wrapped/bridged, bid/ask/spread, mid-market rate, reference rate e mark-to-market
- A área de transações e settlement inclui value date, booking date, settlement date, T+X, clearing vs settlement, cut-off time, float, netting, backdating e reversal/correction
- Termos de payments, cards, markets, crypto e compliance também são organizados separadamente
- PSP, omnibus account, FBO account, chargeback, issuer/acquirer, authorization vs capture
- order book, market vs limit order, maker/taker, slippage, liquidity, derivative, futures, perpetual, liquidation
- custody, hot/cold wallet, private key, multisig, MPC, gas, confirmation/finality, reorg, UTXO vs account model
- KYC, AML/CFT, sanctions screening, PEP, SoF/SoW, Travel Rule, VASP, MiCA, least privilege, RBAC, audit trail
- Os materiais de referência são divididos em contabilidade e ledger, payments e cards, markets e trading, crypto, engenharia, KYC e AML
- Accounting for Computer Scientists: texto para engenheiros que explica partidas dobradas com grafos e modelos de dados
- Modern Treasury, How to Scale a Ledger: série de textos que aborda production ledger sob a perspectiva de software engineering
- Designing Data-Intensive Applications: aborda idempotency, log, consistency e failure modes sob a perspectiva de sistemas
Três exemplos end-to-end
-
Saque de cripto
- É o fluxo em que o usuário saca 0,5 ETH para um endereço externo
- A solicitação inclui uma idempotency key para que envios duplicados gerem apenas um saque
- 0,5 ETH e a network fee estimada são reservados do available balance
- O compliance gate verifica sanções, AML e o endereço de destino, e pode ficar em sleep por vários dias por causa de chamadas externas e revisão manual
- O broadcast on-chain deve ser idempotente e, após um crash, deve-se verificar a chain novamente em vez de fazer um segundo broadcast
- Após confirmações suficientes, faz-se o posting no ledger com debit na conta do usuário, credit na conta externa on-chain, network fee expense e service fee revenue
- Um job noturno reconcilia o ledger com a realidade da chain
-
Depósito com cartão
- É o fluxo de recarga via cartão por meio de um PSP
- O usuário envia o valor e os dados do cartão, e abre uma transaction de depósito no PSP com uma idempotency key
- A authorization cria apenas um hold; como o dinheiro ainda não pertence à empresa, não se faz credit no balance do usuário
- O webhook
capturedfaz a verificação da assinatura dos raw bytes, armazena o raw payload e, após uma resposta 2xx rápida, processa de forma assíncrona - O webhook é apenas um trigger, então o estado autoritativo é consultado na API do PSP
- O estado captured but not settled é registrado por meio de uma clearing account, e o settlement pode chegar em lote após T+X
- O chargeback é tratado com uma linked compensating entry, sem alterar o original
-
Conversão no app com cashback
- É o fluxo de converter 1.000 EUR em USDC e conceder um cashback promocional
- A quote EUR→USDC não é o inverso de USDC→EUR; é uma directional rate
- EUR e USDC não são somados entre si, USDC é identificado por
(network, contract address)e não é o mesmo que fiat pareada - O cálculo mantém a precisão total e faz arredondamento com uma estratégia explícita apenas uma vez, no boundary
- O spread deve ser booked explicitamente em uma revenue account e não pode desaparecer como rounding residual
- O cashback não é um aumento gratuito de balance, mas dinheiro real movido da promotional/expense account da empresa para o balance do usuário
- A publicação do resultado garante reliable delivery com mecanismos como outbox, CDC e event log, e os consumers downstream fazem dedupe com um event id estável
1 comentários
Opiniões no Hacker News
Dei uma olhada por alto e acho que este handbook é superficial e, em algumas áreas, chega perto de ser mau conselho
Por exemplo, ao ver valores monetários armazenados em uma forma que não é inteira, eu sairia correndo gritando. Por coisas como o
decimaldo Rust ser representado como ponto flutuante em JSON. A menos que haja um motivo muito forte, deve ser sempre inteiro, e a visualização exportada pode ser qualquer coisa, até algum formato estranho de codificação de bitsTaxa de câmbio também não é um problema que se resolve com um único ponto no tempo. A taxa no momento do comprador, a taxa no momento do vendedor, o acordo, a tolerância do acordo e o timestamp definitivo acordado, tudo isso influencia
Por causa da imutabilidade, dá vontade de ter event sourcing em todo lugar que lida com dinheiro. O stream final consolidado parece
A -> B -> E, mas o stream real pode serA0 -> Edit(A0, A) -> B -> C -> D -> Rollback(B) -> ENo fim das contas, Fintech não é tudo a mesma Fintech. Em alguns lugares, o dinheiro era tratado como carga; em outros, o dinheiro era o centro de tudo
Sobre câmbio, isso também parece reforçar a afirmação do handbook de que “não existe uma taxa canônica”. Além disso, o texto trata de registros depois da confirmação, e tenho a impressão de que você está falando do método de confirmação. É uma nuance válida para um objetivo separado, mas não parece prova de que algo esteja faltando ou errado
Na parte de imutabilidade, o texto também parece dizer a mesma coisa. Não entendo qual é a diferença
Se você está calculando o preço de opções por Monte Carlo em trajetórias de taxas de juros e se importa com métricas de risco como duration, convexidade e vega, ninguém se importa com qual é a regra de arredondamento. double basta. Como você vai forçar
exp(-rt)cashflowou a função de distribuição acumulada normal a serem inteiros?Existem áreas em que inteiros fazem sentido. Mas isso não é um princípio universal; basta fazer a escolha correta de engenharia
Se o ambiente que você usa der suporte, também dá para usar ponto fixo, mas tecnicamente ainda é inteiro
Para exibição, é seguro retornar valores decimal
Fugir de um sistema que armazena valores monetários como inteiros? Ótimo. Assim provavelmente não vamos acabar trabalhando no mesmo sistema. Hoje em dia, muitas vezes tenho vontade é de fugir de sistemas que tratam valores monetários como inteiros. Em uma codebase ideal, tocada apenas por programadores financeiros experientes, isso pode funcionar bem, mas esses sistemas geralmente correm o risco de se tornar excessivamente excludentes ou frágeis
Para quem está considerando uma estratégia de precisão em minor units para representar valores monetários, meu conselho é: melhor não. Pelo menos não use isso como formato de dados de intercâmbio/API.
Parece esperto — operações inteiras rápidas, sem problemas de arredondamento em somas e subtrações —, mas, no momento em que você trabalha com um parceiro que assume implicitamente um número de casas diferente para uma determinada moeda, pode se complicar muito. Isso é especialmente importante com stablecoins, que muitas vezes têm casas decimais implícitas diferentes da moeda fiduciária que representam.
Em APIs baseadas em JSON, também vale considerar representar valores monetários como strings. JSON não especifica precisão decimal, então você sempre precisa verificar se você e todos os seus usuários/fornecedores não estão perdendo precisão porque o parser/serializador passa internamente por ponto flutuante. Isso pode ficar bagunçado rapidamente, e strings parecem conceitualmente menos elegantes, mas contornam completamente o problema. Algumas pessoas chamariam isso de antipadrão [1], mas eu não gostaria de travar essa batalha por pureza ideológica às custas dos usuários ou acionistas.
[1] https://blog.json-everything.net/posts/numbers-are-numbers-n...
Em trading de alta frequência, se for possível fixar previamente um expoente consistente para algum {slice}, dá para economizar espaço de transmissão. Por exemplo, dentro de um escopo como produto/tamanho do tick/classe de ativo/bolsa/feed/servidor, envia-se apenas a mantissa e o cliente usa um expoente hardcoded.
Mas, mesmo em áreas parecidas, muitas vezes vale a pena enviar também um expoente
uint32nos dados transmitidos. Assim é possível mudar depois, sem ficar preso a um desenho inicial do tipo “por enquanto só precisamos de centavos”. Por exemplo, talvez seja necessário de repente dar suporte ao preço do bitcoin com precisão total. Os usuários vão agradecer se você não precisar coordenar uma alteração incompatível quando quiser ajustar o expoente fixo.O critério de “qualquer” é irracional. É um padrão inalcançável que pode exigir um orçamento de engenharia ilimitado sem valor comprovável na prática.
Identificar a ausência de padrão, falar sobre como parsers reais se comportam e discutir lacunas e casos de uso não atendidos é bom. Sugerir que precisamos de um padrão mais razoável também é bom. Mas exigir que todo mundo dê suporte a “todas as possibilidades” — algo de que ninguém realmente precisa, cujo significado é pouco claro e que na prática nem é alcançável — não é uma boa ideia.
float/double; aritmética de ponto fixo em milésimos de minor unit ou unidades menores; decimal de precisão arbitrária; ou uma abordagem totalmente diferente?Como programador, ao ver programadores de Fintech falando a partir de experiências e perspectivas diferentes, fico me perguntando o que significa, afinal, programar bem.
Quando xlii disse para não armazenar valores monetários em ponto flutuante, isso é o problema comum do IEEE 754. Rastreamento financeiro deve mesmo ser feito com logs imutáveis ou registros baseados em eventos, mas não acho que todos os serviços ao redor precisem ser feitos com event sourcing. Acho que aplicar isso só à lógica central — ledger, liquidação, ordens, execuções — já é suficiente. Lendo o texto do xlii, parece uma técnica que só se torna viável quando a modelagem deu certo.
O comentário de lxgr aponta o problema das minor units. Se números JSON forem parseados por uma linguagem ou parser como ponto flutuante, pode haver perda de precisão. Normalmente se envia o valor junto com um campo separado para o número de casas decimais. Mas ouvi dizer que, em trading de alta frequência, esse overhead em si é caro demais, então não fazem isso.
O que antonymoose diz se conecta ao que muitos livros falam. Por isso, em contextos de câmbio ou APIs, esse tipo de desenho é comum. Também parece um pouco com design de protocolo.
No conjunto, todos estão certos dentro de seus próprios domínios. Acho que seria bom ter alguém como xlii como programador sênior, mas, ao mesmo tempo, acho que eu não conseguiria projetar um sistema tão complexo. Nesse sentido, o que cada um diz é válido, e é interessante ver como as opiniões divergem conforme o domínio. Fico pensando se isso é expertise.
Ao ver essas coisas, dá para inferir mais ou menos de que experiência um programador veio. Às vezes, programar parece não ser encontrar a resposta certa, mas escolher uma visão de mundo.
É sempre interessante ver no HN como programadores modelam seus próprios domínios. Às vezes clico nos perfis e adiciono o conhecimento de domínio deles ao meu wiki pessoal, pensando que talvez eu use algum dia.
Bom. Este livro já contém muitas informações boas que também podem ser encontradas em outros lugares, mas só o fato de reuni-las já o torna bastante prático. Recomendo fortemente Designing Data-Intensive Applications, de Kleppmann. A primeira edição já era excelente, e a segunda saiu recentemente.
Já trabalhei como CTO em FinTech e construí toda a pilha de software do zero; as lições do livro estão, em geral, corretas. Digo “em geral” porque, como sempre, em projetos específicos é preciso considerar muito o “depende”. Por exemplo, eu não usei event sourcing porque queria evitar o problema de calcular o estado inteiro. Um trilho de auditoria append-only padrão pode ser suficiente.
Não é possível garantir entrega exatamente uma vez, mas dá para construir processamento efetivamente uma vez, e isso é o que você realmente quer.
A recomendação de armazenar todas as requisições e respostas está absolutamente certa. Isso vale não só ao consumir APIs, mas também ao coletar qualquer informação do mundo externo; e, se possível, todos os estágios intermediários de transformação dentro da sua fronteira também deveriam ser registrados. Uma combinação de buckets endereçados por conteúdo e tabelas relacionais funciona bem.
Além disso, o texto não diz nada sobre linhagem de dados. O que fazer se um fornecedor atualiza algum dado no meio do dia e você precisa obrigatoriamente saber disso? Você precisa conseguir explicar essa mudança e, ao mesmo tempo, permitir que cálculos feitos com valores antigos sejam reexecutados produzindo o mesmo resultado. Não é um problema particularmente difícil de resolver, mas exige reflexão.
“Conselho ruim” até foi uma forma bem educada de dizer. Sinceramente, parece que a maior parte deste “manual” foi escrita por um LLM
Por exemplo, na seção sobre imutabilidade há esta frase: “Ao separar PII de dados financeiros, é possível respeitar o direito à exclusão sem perder o histórico financeiro que precisa ser mantido”
Em instituições financeiras, os dois andam juntos por motivos óbvios de KYC/AML
Se você apagar imediatamente, mediante solicitação, o nome, endereço etc. do cliente antes do fim do período aplicável, mantendo apenas os dados financeiros, a organização inteira terá um dia muito ruim quando uma autoridade legítima vier exigir os dados para rastrear um crime
Quem quer trabalhar em Fintech não deve depender de um “manual” aleatório escrito por uma pessoa desconhecida, em uma jurisdição desconhecida
Quem trabalha em Fintech deve seguir apenas o manual/diretrizes internas do empregador etc. Esse tipo de documento provavelmente foi elaborado em conjunto pelos advogados e responsáveis de compliance da empresa, para atender às leis e aos requisitos de reporte das jurisdições em que o empregador opera
Pelo que vejo, ele recomenda separar as PII que eventualmente precisam ser apagadas dos dados que, por fazerem parte da equação contábil/das invariantes, na prática você quer manter permanentemente. Assim, depois que passar o período aplicável de retenção de registros, você pode apagar as primeiras
Concordo que não se deve depender de um “manual” escrito por uma pessoa desconhecida, em uma jurisdição desconhecida. Mas também não se deve ignorar cegamente as ideias e práticas dele nem deixar de olhar para fora da própria organização. O ideal é lê-lo e depois confrontar e ajustar com seu próprio conhecimento e com as regras locais
Em um mundo em que só existissem organizações perfeitas e sem erros, a abordagem de seguir apenas as diretrizes internas do empregador pareceria razoável. Mas, sem conversas como esta, como chegaríamos a esse nível?
Acho que a maior parte disto se aplica não só a Fintech, mas à engenharia de software em geral
Por exemplo, as partes que tratam de retentativas, idempotência, ordem de eventos etc. se aplicam a qualquer sistema que precise de algum nível de correção, mesmo quando dinheiro não está diretamente envolvido. Já vi sistemas demais construídos com a premissa de que “é só tentar de novo a qualquer momento”, mas, para que a retentativa seja possível, primeiro é preciso falhar de forma limpa, e os subsistemas precisam oferecer o nível de idempotência que eu imagino. Essas coisas muitas vezes não são de fato verificadas
Eu preferiria ler um texto defendendo uma abordagem mais radical, como bancos de dados por conta. Algo que tenha trade-offs próprios dentro de Fintech
O principal conselho que eu daria a um engenheiro ou fundador de Fintech é levar risco e compliance a sério desde o primeiro dia
Sistemas financeiros são baseados em confiança. Se você não consegue mitigar riscos de forma demonstrável, perde a confiança e, no fim, perde o negócio inteiro
A recomendação de evitar ponto flutuante não é verdadeira. Trabalhei 20 anos em Fintech e usamos principalmente double. O Excel também usa double, o front-end vai usar double, e todos os bancos de dados dão suporte a double. A biblioteca padrão sabe fazer parsing de double, e JSON, mesmo que não em teoria, na prática usa double. Muitos sistemas ERP também usam double
O ponto central ao lidar com moeda usando double é ter em mente que ele consegue armazenar 15 dígitos de precisão no total. Desde que os números não usem mais dígitos do que isso, como
123456789.01ou123.456789, é possível ter precisão decimal perfeita em cálculos financeiros. Basta sempre arredondar o resultado para dentro de 15 dígitos de precisão após cada cálculo e antes de cada comparação. É isso que o Excel fazA maior vantagem do double é o amplo suporte e a possibilidade de misturar diferentes precisões dentro do sistema. Isso acontece quando se lida com finanças internacionais ou produtos financeiros avançados. Algumas contabilizações precisam de precisão até milésimos, outras precisam ser arredondadas em múltiplos de 0,25. No fim, você provavelmente não usará aritmética básica, mas uma biblioteca especializada de matemática contábil, e essa biblioteca pode perfeitamente usar ponto flutuante como backend
Verificação de saldo pela Plaid não é garantia de que um débito ACH prestes a ser enviado será bem-sucedido
Não importa se o saldo é de um milhão de dólares. Antes que o ACH seja processado, todo o dinheiro pode (a) sair por transferência wire, (b) ser compensado por ACHs de ontem, como contas, débitos automáticos etc., e cheques, ou (c) ser gasto no cartão de débito/ATM
Talvez seja melhor eu não dizer como sei que algumas Fintechs não lidam com isso
Só a seção sobre chaves de idempotência já vale a leitura. A maioria dos desenvolvedores aprende essa lição do jeito difícil
Muitos deles são de antes de o conhecimento sobre idempotência se espalhar amplamente, então muitas vezes se acaba forçando uma chave de idempotência ao concatenar vários campos que parecem globalmente únicos. O problema é que isso nunca é totalmente único. Às vezes dá para espiar por trás da cortina; por exemplo, quando um banco impede que uma transferência de mesmo valor seja feita para a mesma conta destinatária na mesma data
Passei muito tempo explicando como a idempotência deve funcionar e por que ela é importante. A maioria das equipes entende a necessidade, mas quase nenhuma pensou nisso desde o início
“Fintech” é um termo muito amplo, e boa parte do que é chamado de Fintech é, na verdade, comunicação. Comunicação entre empresas, entre traders, entre sistemas, entre ledgers. Não existe uma forma “correta” de programar que valha para o setor inteiro. No fim, a forma correta é aquela que a contraparte com quem eu me comunico consegue entender
Se a contraparte rastreia moeda em centavos, rastrear com precisão maior que essa gera divergências de arredondamento. O contrário também vale se eu uso centavos e a contraparte trabalha com décimos de centavo. Todos os outros conselhos deste documento devem ser vistos dessa forma