Forem

Cover image for Double Entry: o que é, quando usar e como fazer
Jefferson Rodrigues for Lerian

Posted on

Double Entry: o que é, quando usar e como fazer

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.

Image description

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)
}

Enter fullscreen mode Exit fullscreen mode

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 e Credit 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
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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)