O que é um ledger?
Considere um Ledger como um "livro da verdade", por exemplo, uma planilha contábil de uma empresa, onde são registradas todas as transações que aconteceram. Assim como em uma planilha contábil, não editamos ou removemos registros, apenas adicionamos novas entradas. Isso nos garante a imutabilidade, que é um ótimo princípio em sistemas financeiros. A forma mais comum de tratar isso é por meio de eventos de entrada e saída, junto ao montante de valor correspondente para cada transação.
Como fazer?
Primeiramente, é uma prática essencial manter um Ledger distinto para cada tipo de moeda, como BRL ou USD, para simplificar radicalmente a lógica de negócio e os cálculos. Além disso, para prevenir o processamento duplicado em um sistema distribuído, cada operação deve ser acompanhada de uma chave de idempotência única.
O gerenciamento da concorrência é outro pilar para evitar inconsistências no saldo. Uma abordagem é o lock otimista, que utiliza um campo de versionamento como version
ou last_sequence
. A alternativa é o lock pessimista, que garante acesso exclusivo ao Ledger através de um serviço externo como Redis.
Persistência e Concorrência
A persistência do Ledger pode ser implementada em um banco de dados como o MongoDB. Para lidar com bloqueios otimistas, utilizamos um campo como last_sequence
no próprio documento do Ledger. Para o bloqueio pessimista, a recomendação é um serviço de cache distribuído onde a latência seja a menor possível, como o Redis.
A chave de idempotência, por sua vez, depende da estrutura do sistema, mas geralmente é composta por uma combinação de dados como timestamp + from + to + amount
. Essa chave pode ser armazenada em um cache distribuído com um TTL (Time To Live).
Lidando com Dinheiro em Sistemas Distribuídos
Por que usar int
e não float
?
Nunca use tipos de ponto flutuante (float
) para armazenar dinheiro. A solução padrão é trabalhar com inteiros, armazenando o valor na menor unidade da moeda (ex: R$ 123,45 é armazenado como o inteiro 12345
). Todas as operações matemáticas são feitas com esses inteiros, eliminando erros de precisão.
Estrutura para Múltiplas Moedas
Valores monetários devem ser representados por uma estrutura que combine o montante e a moeda, seguindo o padrão ISO 4217. Essa estrutura deve conter o amount
(inteiro) e a currency
(ex: "BRL"), prevenindo erros como somar diretamente dólares com reais.
Aplicando o Lock Otimista e Transações
Para garantir a consistência, o processo de registrar uma transação deve ser atômico. Aqui está um passo a passo mais detalhado de como implementar o fluxo completo:
Verificação de Idempotência (Fail Fast): Antes de iniciar a transação no banco de dados, verifique a chave de idempotência em um cache rápido como o Redis. Se a chave já existir, significa que a operação já foi processada, e você pode retornar o sucesso imediatamente, evitando trabalho desnecessário.
Início do Loop de Tentativa (Retry Loop): Como o lock otimista pode falhar, toda a lógica de negócio deve rodar dentro de um loop que tentará a operação algumas vezes (ex: 3 tentativas) antes de desistir.
Leitura do Estado Atual: Dentro do loop e de uma nova transação do MongoDB, leia o documento do
Ledger
para obter seubalance
elast_sequence
atuais.Cálculo em Memória: Calcule o novo saldo na sua aplicação.
novo_saldo = saldo_atual + valor_da_transação
.Execução da Escrita Atômica: Execute as duas operações de escrita dentro da mesma transação:
* **`insertOne`** na coleção `transactions` com os dados da nova movimentação.
* **`updateOne`** na coleção `ledgers`. Este update é a chave do lock otimista: ele deve buscar pelo `_id` **e** pela `last_sequence` que você leu no passo 3. A atualização irá então modificar o `balance` e incrementar o `last_sequence`.
- Resultado:
* **Sucesso**: Se o `updateOne` encontrar o documento e a transação for concluída (commit), significa que não houve conflito. Você pode sair do loop e retornar o sucesso.
* **Falha**: Se a transação falhar porque o `last_sequence` não correspondeu, significa que outro processo alterou o Ledger. O loop de tentativa continuará para a próxima iteração, reiniciando o processo a partir do passo 3. É uma boa prática adicionar um pequeno tempo de espera (backoff) entre as tentativas.
Se o loop se esgotar sem sucesso, a operação falhou e um erro deve ser retornado ao cliente.
Como ficam meus dados?
ledgers
{
"_id": ObjectId(),
"balance": {
"amount": 12345,
"currency": "BRL"
},
"last_sequence": 1,
"last_transactions":[
{
"_id": ObjectId(),
"ledger_id": ObjectId(),
"timestamp": UnixTime,
"sequence": 1,
"change": {
"amount": 5000,
"currency": "BRL"
},
"idempotency_key": "timestamp-from-to-amount"
}
]
}
transactions
{
"_id": ObjectId(),
"ledger_id": ObjectId(),
"timestamp": UnixTime,
"sequence": 1,
"change": {
"amount": 5000,
"currency": "BRL"
},
"idempotency_key": "timestamp-from-to-amount"
}
Considerações Finais
Construir um Ledger confiável se baseia em quatro pilares essenciais:
Imutabilidade, garantindo um histórico de transações que é apenas de adição (append-only);
Precisão, usando inteiros para cálculos monetários e evitando erros de ponto flutuante;
Consistência, através de transações atômicas com controle de concorrência como o lock otimista;
Idempotência, para proteger o sistema contra operações duplicadas.
Uma vez que esses fundamentos estão sólidos, o próximo passo na evolução do sistema é pensar em escalabilidade. Para Ledgers com milhões de transações, técnicas como Snapshots (fotos periódicas do saldo) para otimizar a performance de leitura e o arquivamento de transações antigas tornam-se cruciais para manter o sistema performático a longo prazo.
Top comments (0)