Quem está começando a trabalhar no setor financeiro pode se deparar com esse termo "double-entry", que é um conceito fundamental para o gerenciamento de transações e registros financeiros. Neste post, quero explicar em detalhes como esse conceito funciona, quando ele deve ser utilizado e sua importância para manter a integridade das transações financeiras.
A melhor maneira de explicar o que é um double-entry é com um exemplo. Então, considere que você faça uma transferência Pix de R$ 1.000 para pagar uma conta; esse processo de transferência, na verdade, realiza duas operações:
- A primeira operação é a de débito de R$ 1.000 na sua conta;
- A segunda operação é a de crédito de R$ 1.000 na conta destino.
É imprescindível que, para a transação seja efetuada com sucesso, ambas as operações sejam registradas corretamente. Se uma das operações falhar, todo o processo precisa ser revertido para manter a integridade da conta. Esse conceito é conhecido como double entry (entrada dupla) e é fundamental para garantir a integridade das transações financeiras.
Importância no Setor Financeiro
O sistema de double entry é crucial no setor financeiro por diversos motivos:
- Precisão contábil: Garante que todas as transações financeiras sejam registradas com exatidão, reduzindo erros e discrepâncias nas contas.
- Auditoria facilitada: Permite rastrear facilmente o histórico de transações e identificar possíveis irregularidades ou fraudes.
- Conformidade regulatória: Atende aos requisitos legais e regulatórios do setor financeiro, que exigem registros precisos e transparentes.
- Reconciliação bancária: Simplifica o processo de reconciliação, permitindo a comparação eficiente entre registros internos e extratos bancários.
Aplicações além dos sistemas financeiros
O sistema de double entry, embora tradicionalmente associado a sistemas financeiros, pode ser aplicado em outros contextos também, como em gestão de inventário e logística em que é preciso registrar a entrada e saída de produtos.
Em todos esses casos, o princípio fundamental do double entry — onde cada transação afeta dois registros diferentes — ajuda a manter a integridade e rastreabilidade dos dados.
Como desenvolver
Vamos criar um exemplo simples de implementação do sistema double entry em Go. Neste exemplo, vamos simular uma transferência bancária:
package main
import (
"fmt"
"time"
)
type Account struct {
ID string
Balance float64
}
type Transaction struct {
ID string
FromID string
ToID string
Amount float64
Timestamp time.Time
}
func (a *Account) Debit(amount float64) error {
if a.Balance < amount {
return fmt.Errorf("saldo insuficiente")
}
a.Balance -= amount
return nil
}
func (a *Account) Credit(amount float64) {
a.Balance += amount
}
func Transfer(from, to *Account, amount float64) error {
// Primeira entrada: débito da conta origem
if err := from.Debit(amount); err != nil {
return err
}
// Segunda entrada: crédito na conta destino
to.Credit(amount)
return nil
}
func main() {
// Criando contas de exemplo
accountA := &Account{
ID: "conta_a",
Balance: 1000.0,
}
accountB := &Account{
ID: "conta_b",
Balance: 500.0,
}
fmt.Printf("Antes da transferência:\nConta A: R$%.2f\nConta B: R$%.2f\n\n",
accountA.Balance, accountB.Balance)
// Realizando uma transferência
err := Transfer(accountA, accountB, 300.0)
if err != nil {
fmt.Printf("Erro na transferência: %v\n", err)
return
}
fmt.Printf("Após a transferência:\nConta A: R$%.2f\nConta B: R$%.2f\n",
accountA.Balance, accountB.Balance)
}
Neste exemplo, implementamos:
- Uma estrutura
Account
para representar contas com ID e saldo - Uma estrutura
Transaction
para registrar os detalhes das transações - Métodos
Debit
eCredit
para realizar as operações nas contas - Uma função
Transfer
que implementa o conceito de double entry, garantindo que ambas as operações sejam realizadas
Quando executado, este código demonstra uma transferência de R$ 300,00 da conta A para a conta B, mostrando os saldos antes e depois da operação. Se houver qualquer erro durante o processo (como saldo insuficiente), a transação não é completada.
As ditas operações
No exemplo que vimos anteriormente, criamos duas operações fundamentais que compõem uma transação:
- Uma operação de débito (Debit) que remove o valor da conta de origem
- Uma operação de crédito (Credit) que adiciona o valor na conta de destino
Essas operações são as unidades básicas de uma transação financeira e devem sempre ocorrer em pares para manter o princípio do double-entry. Cada operação é atômica, ou seja, ou ela acontece por completo ou não acontece, não existindo estados intermediários.
No código, implementamos essas operações como métodos separados da estrutura Account, mas elas são sempre chamadas juntas através da função Transfer para garantir que o princípio do double-entry seja respeitado.
Encapsulamento e Segurança
Para garantir que o saldo só possa ser alterado através da função de transação, podemos utilizar encapsulamento e tornar o campo Balance privado na estrutura Account. Veja como podemos modificar o código anterior:
type Account struct {
ID string
balance float64 // Note o 'b' minúsculo tornando o campo privado
}
// Método getter para acessar o saldo
func (a *Account) GetBalance() float64 {
return a.balance
}
// Métodos de débito e crédito agora trabalham com o campo privado
func (a *Account) debit(amount float64) error {
if a.balance < amount {
return fmt.Errorf("saldo insuficiente")
}
a.balance -= amount
return nil
}
func (a *Account) credit(amount float64) {
a.balance += amount
}
Com essa implementação, o campo balance só pode ser modificado através dos métodos do próprio pacote, garantindo que todas as alterações de saldo passem pelo sistema de double entry.
Claro, há outras considerações importantes ao implementar um sistema de double entry, como garantir a atomicidade das transações usando um banco de dados transacional e implementar logs detalhados de todas as operações para fins de auditoria. Essas práticas ajudam a manter a integridade e rastreabilidade do sistema.
O código em produção
Como exemplo prático de uma ferramenta que implementa o sistema de double entry, temos o Midaz, um ledger open source mantido pela Lerian que utiliza essa técnica para garantir a integridade das transações financeiras.
Ao contrário de basicamente todos os sistemas financeiros brasileiros que são fechados, o Midaz nos permite examinar e discutir abertamente o uso de double entry e estudar como essa prática funciona em um ambiente de produção.
Como é criada uma operação
Vamos examinar como o Midaz cria uma operação individual dentro de uma transação. Dentro da criação de uma transação, é chamado o método CreateOperation
que recebe os detalhes da transação e as contas envolvidas. Segue o código da criação de uma operação, comentado:
func (uc *UseCase) CreateOperation(ctx context.Context,
accounts []*account.Account,
transactionID string,
dsl *goldModel.Transaction,
validate goldModel.Responses,
result chan []*operation.Operation,
err chan error) {
// Declara uma lista para armazenar as operações criadas
var operations []*operation.Operation
// Cria uma lista `fromTo` contendo as contas de origem e destino envolvidas na transação
var fromTo []goldModel.FromTo
fromTo = append(fromTo, dsl.Send.Source.From...) // Adiciona contas de origem
fromTo = append(fromTo, dsl.Send.Distribute.To...) // Adiciona contas de destino
// Percorre todas as contas envolvidas na transação
for _, acc := range accounts {
// Verifica se a conta está na lista `fromTo`
for i := range fromTo {
// Verifica se a conta atual está envolvida na transação, seja pelo ID ou pelo alias.
if fromTo[i].Account == acc.Id || fromTo[i].Account == acc.Alias {
// Define o saldo atual da conta
balance := operation.Balance{
Available: &acc.Balance.Available,
OnHold: &acc.Balance.OnHold,
Scale: &acc.Balance.Scale,
}
// Valida a operação e calcula os valores da transação
amt, bat, er := goldModel.ValidateFromToOperation(fromTo[i], validate, acc)
if er != nil {
logger.Errorf("Error creating operation: %v", er)
}
// Converte os valores da transação para float64 e a escala de casas decimais
v := float64(amt.Value)
s := float64(amt.Scale)
amount := operation.Amount{
Amount: &v,
Scale: &s,
}
// Define o saldo da conta após a operação
ba := float64(bat.Available)
boh := float64(bat.OnHold)
bs := float64(bat.Scale)
balanceAfter := operation.Balance{
Available: &ba,
OnHold: &boh,
Scale: &bs,
}
// Determina se a operação será um débito ou crédito
var typeOperation string
if fromTo[i].IsFrom {
typeOperation = constant.DEBIT
} else {
typeOperation = constant.CREDIT
}
// Cria uma nova operação com os dados processados
save := &operation.Operation{
ID: pkg.GenerateUUIDv7().String(),
TransactionID: transactionID,
Type: typeOperation,
AssetCode: dsl.Send.Asset,
Amount: amount,
Balance: balance,
BalanceAfter: balanceAfter,
AccountID: acc.Id,
AccountAlias: acc.Alias,
...
}
// Salva a operação no banco de dados
op, er := uc.OperationRepo.Create(ctx, save)
if er != nil {
logger.Errorf("Error creating operation: %v", er)
}
// Adiciona a operação criada à lista de operações
operations = append(operations, op)
break // Sai do loop para evitar múltiplas inclusões da mesma conta
}
}
}
// Envia a lista de operações criadas pelo canal `result`
result <- operations
}
Para facilitar a leitura, removi loggers e tracers e alguns outros detalhes, mas você pode analisar na íntegra o código no GitHub da criação de uma transação e da operação. Coloque nos comentários caso queira um artigo destrinchando e explicando como é feito o código de uma transação financeira por completo.
Top comments (0)