← todos os artigos

Payments

Como eu projetaria um sistema de pagamentos como .NET developer júnior

Se me entregassem uma feature de pagamento amanhã, as partes difíceis não seriam o design da API. Seriam idempotency, o fato de que problemas com dinheiro não aparecem em testing, e saber o que não construir.

Como eu projetaria um sistema de pagamentos como .NET developer júnior

Se alguém me entregasse uma feature de pagamento amanhã, o design da API seria a parte fácil. O que eu de fato me preocuparia é uma lista mais curta e menos óbvia do que eu teria imaginado há um ano.

Eu não construiria a parte de pagamento

A primeira decisão é o que não construir. Dados de cartão têm obrigações de compliance atreladas a eles, e um desenvolvedor júnior decidindo lidar com números de cartão crus para economizar uma taxa de integração está tomando uma decisão que não é dele para tomar. Use um provider, deixe os detalhes do cartão irem para ele, e guarde a referência que ele devolve.

Isso parece que está evitando o problema. Na verdade é a resposta correta, e saber disso vale mais do que ser capaz de implementar a coisa que você não deveria implementar.

Assuma que todo request chega duas vezes

Essa é a que eu erraria se já não tivesse aprendido isso do jeito difícil. Redes dão timeout, usuários clicam duas vezes, clientes mobile fazem retry automaticamente, e nada disso é incomum. Um endpoint de pagamento que cobra duas vezes quando recebe o mesmo request duas vezes está quebrado, mesmo que passe em todo teste que você escreveu para ele.

O conserto é uma idempotency key vinda do cliente e uma constraint única nela no database. Checar se a key já existe antes de inserir parece que funciona, mas existe uma janela entre a leitura e a escrita onde dois requests concorrentes podem passar pela checagem ao mesmo tempo. Deixe o database aplicar isso, capture a falha, e devolva o resultado que você já armazenou.

Dinheiro nunca deveria ser um float

decimal, sempre. Aritmética de ponto flutuante perde precisão de formas que são aceitáveis para uma simulação de física e inaceitáveis para um saldo. Esse é fácil de acertar e genuinamente ruim de errar.

Guarde a moeda junto com o valor também. Um valor sem moeda é um número, não dinheiro, e no momento em que uma segunda moeda aparece no sistema, todo valor sem uma se torna ambíguo.

Projete para a pergunta "o que aconteceu?"

A coisa que eu mais subestimei: o trabalho de um sistema de pagamentos não é só mover dinheiro, é conseguir explicar depois o que ele fez e por quê. Quando um cliente contesta uma cobrança ou um provider pergunta sobre uma transação, a resposta tem que vir de registros armazenados, não de ler o código e deduzir o que ele provavelmente fez.

Isso significa que todo passo relevante é persistido, incluindo as falhas. Também significa que registros de pagamento não são deletados, porque um registro de pagamento deletado é evidência destruída, não dado organizado.

Torne os estados explícitos e as transições protegidas

Um pagamento é uma state machine quer você o modele como uma ou não. Ele está pending, depois authorised ou declined, depois talvez captured, depois talvez refunded. Se esses estados são só um campo string em que qualquer coisa pode escrever, eventualmente algo escreve uma combinação impossível e não existe registro de como.

Colocar as transições dentro da entity, para que uma inválida lance um erro em vez de salvar silenciosamente, custa muito pouco e transforma uma classe de bug numa exception que você consegue de fato ver.

Falhas não são todas iguais

Saldo insuficiente e um cartão reportado como roubado são as duas declines, e tratá-las da mesma forma é um erro. Uma é temporária e vale a pena tentar de novo, a outra é final e tentar de novo em outro lugar é só fraude com passos extras. Qualquer retry logic precisa saber com qual tipo de falha está lidando antes de decidir tentar de novo.

Com o que eu pediria ajuda

Reconciliation, o processo de confirmar que o que o seu sistema acha que aconteceu bate com o que o provider diz que aconteceu. Eu entendo que isso importa e não tenho experiência real com isso, e é o tipo de coisa em que estar errado é caro e silencioso.

Também qualquer coisa envolvendo múltiplas moedas com conversão de verdade, e qualquer coisa tocando payouts em vez de collections. Sei o suficiente para saber que essas coisas são mais profundas do que parecem.

O formato geral disso

Use um provider para dados de cartão. Assuma requests duplicados. Use decimal com uma moeda explícita. Persista o suficiente para explicar qualquer transação depois. Modele estados explicitamente. Distinga falhas que podem ter retry das finais. E seja honesto sobre quais partes você gostaria que um sênior revisasse antes de chegar perto de dinheiro de verdade.