# ADR: Aurora vs DynamoDB para um Ledger de Dupla Entrada em Core Banking

Este ADR avalia Aurora PostgreSQL e DynamoDB como motor de persistência para um ledger de dupla entrada em um sistema de core banking, pesando consistência forte, padrões de acesso, auditabilidade e custo. A decisão favorece Aurora com particionamento por range de data e uma camada de eventos imutável, reconhecendo as limitações de escala horizontal que essa escolha impõe.

- URL: https://fernando.moretes.com/studies/adr-aurora-vs-dynamodb-ledger

- Markdown: https://fernando.moretes.com/studies/adr-aurora-vs-dynamodb-ledger/study.md?lang=pt

- Type: Decisão (ADR)

- Company: Core banking (cenário)

- Domain: Dados

- Status: accepted

- Date: 2026-01-20

- Tags: aurora, dynamodb, ledger, core-banking, double-entry, consistency, data-platform, adr

- Reading time: 8 min

---

Escolher o banco de dados errado para um ledger de dupla entrada não é um problema de performance — é um problema de corretude. Neste ADR, documento as forças reais que moldaram a decisão entre Aurora PostgreSQL e DynamoDB para o motor de persistência de um sistema de core banking, e explico por que consistência forte e semântica transacional pesaram mais do que escala horizontal neste contexto.

## Ficha do Caso

- **Sistema:** Core banking — ledger de dupla entrada (cenário composto)
- **Domínio:** Dados / Persistência financeira
- **Volume estimado de transações:** ~500 mil lançamentos contábeis/dia (estimativa de referência)
- **Requisito regulatório:** Imutabilidade de registros, trilha de auditoria, reconciliação diária
- **Stack avaliada:** Amazon Aurora PostgreSQL, Amazon DynamoDB, AWS Lambda, Amazon EventBridge
- **Região AWS:** us-east-1 (primária) + us-west-2 (DR)
- **Status da decisão:** Aceita
- **Autores:** Fernando F. Azevedo (Solutions Architect)

## Contexto e Forças em Jogo

Um ledger de dupla entrada é, por definição, um sistema de invariantes. Cada transação financeira produz exatamente dois lançamentos — débito e crédito — cujos valores devem ser iguais e opostos. A soma algébrica de todos os saldos deve ser zero. Essa propriedade não é negociável: qualquer violação, mesmo que temporária, representa um erro contábil com consequências regulatórias e operacionais graves.

O sistema em questão precisa garantir que:

1. **Atomicidade** — débito e crédito são gravados juntos ou nenhum é gravado.
2. **Isolamento** — leituras de saldo durante uma transação em andamento não retornam estados intermediários.
3. **Imutabilidade** — lançamentos gravados não podem ser alterados; correções se fazem por estorno (novo lançamento).
4. **Auditabilidade** — toda operação deve ter trilha rastreável com timestamp, identificador do operador e referência ao evento de negócio que a originou.

Além dessas invariantes de corretude, existem forças operacionais: o volume de lançamentos cresce com a base de clientes, os padrões de leitura são heterogêneos (saldo pontual, extrato paginado, relatórios de reconciliação por período), e o custo precisa ser justificável em um produto financeiro com margens controladas.

Foi nesse cenário que a equipe se viu diante da escolha clássica: um banco relacional com ACID nativo versus um banco NoSQL de escala massiva. A pressão para adotar DynamoDB veio de times de engenharia que operavam outros serviços da plataforma com ele com sucesso. A pressão para manter PostgreSQL veio dos times de produto e compliance, que conheciam bem o modelo relacional e desconfiavam de consistência eventual em qualquer camada do stack financeiro.

## Por Que os Padrões de Acesso Importam Mais do Que o Throughput

O argumento mais comum a favor do DynamoDB em sistemas financeiros é throughput: a capacidade de escalar para milhões de operações por segundo sem degradação. É um argumento válido para sistemas de pagamento de altíssimo volume — processadoras de cartão, por exemplo. Mas para um ledger de core banking de uma instituição de médio porte, esse argumento é, na prática, um red herring.

O problema real não é throughput de escrita. É a **diversidade dos padrões de leitura**.

Um ledger precisa responder a pelo menos quatro classes de consulta fundamentalmente diferentes:

- **Saldo atual de uma conta** — leitura pontual, latência crítica, frequência alta.
- **Extrato paginado** — range scan por `account_id + timestamp`, ordenado, com filtros opcionais por tipo de lançamento.
- **Reconciliação diária** — agregação de todos os lançamentos de um período, agrupados por conta e tipo, potencialmente cruzando milhões de linhas.
- **Auditoria de uma transação específica** — lookup por `transaction_id`, retornando os dois lados do lançamento e o evento de negócio associado.

No DynamoDB, cada um desses padrões exige um design de chave diferente. O modelo de acesso único por partition key + sort key significa que você precisa de múltiplos índices secundários globais (GSIs) ou de duplicação de dados para cobrir todos os padrões. GSIs no DynamoDB têm consistência eventual por padrão — o que é aceitável para muitos casos, mas não para leitura de saldo em um sistema que precisa garantir que o saldo lido reflete exatamente o estado após a última transação confirmada.

É possível contornar isso com leituras fortemente consistentes no índice primário e modelagem cuidadosa. Mas o custo de design e operação dessa solução começa a superar o custo de simplesmente usar um banco relacional com índices bem projetados. A complexidade não desaparece — ela migra do banco de dados para o código da aplicação e para as convenções de modelagem que precisam ser mantidas por toda a equipe.

Aurora PostgreSQL, por outro lado, trata todos esses padrões como queries SQL padrão. O otimizador de queries lida com a complexidade de execução. A equipe mantém um modelo mental único — tabelas, índices, transações — em vez de múltiplos padrões de acesso codificados em chaves compostas.

## Matriz de Decisão: Aurora PostgreSQL vs DynamoDB

### Aurora PostgreSQL

**Pros**
- ACID nativo com isolamento serializável configurável — sem workarounds para atomicidade de dupla entrada
- SQL padrão cobre todos os padrões de leitura do ledger sem duplicação de dados
- Particionamento por range de data (declarativo no PostgreSQL 10+) mantém performance de reconciliação
- Aurora Global Database oferece DR multi-região com RPO < 1s
- Familiaridade da equipe de produto e compliance — menor risco operacional

**Cons**
- Escala vertical tem limite prático — instâncias grandes são caras e têm teto de IOPS
- Conexões são stateful — connection pooling (RDS Proxy) é necessário em ambientes serverless
- Sharding horizontal requer mudança de arquitetura significativa se o volume ultrapassar a capacidade de uma instância
- Custo de instância é fixo independente do uso — ineficiente para cargas muito variáveis

**Verdict:** Escolha preferencial para este caso — corretude e operabilidade superam as limitações de escala no volume projetado

### DynamoDB

**Pros**
- Escala horizontal ilimitada sem gerenciamento de shards — throughput de escrita massivo
- Latência de leitura/escrita consistentemente baixa em P99 (single-digit ms)
- Modelo de custo pay-per-request elimina custo de capacidade ociosa
- DynamoDB Streams + Kinesis para captura de eventos em tempo real sem polling
- Sem gerenciamento de conexões — nativo para arquiteturas serverless

**Cons**
- Transações (TransactWriteItems) limitadas a 100 itens e 4MB — suficiente para dupla entrada simples, mas restritivo para lançamentos compostos
- GSIs têm consistência eventual — leituras de saldo via GSI não são safe sem design explícito
- Queries analíticas e de reconciliação requerem exportação para S3 + Athena ou DynamoDB Streams — latência adicional
- Modelagem de dados complexa — múltiplos padrões de acesso exigem GSIs ou single-table design com overloading de chaves
- Custo de GSIs é proporcional ao volume de dados replicados — pode superar Aurora em volumes médios com múltiplos índices

**Verdict:** Descartado como motor principal do ledger — adequado como store de eventos downstream ou cache de saldo

## Comparação Técnica Detalhada
| Critério | Critério | Aurora PostgreSQL | DynamoDB |
| --- | --- | --- | --- |
| Atomicidade de dupla entrada | BEGIN/COMMIT nativo — sem código adicional | TransactWriteItems — funciona, mas tem limites de tamanho e custo 2x por WCU | — |
| Isolamento de leitura de saldo | READ COMMITTED (padrão) ou REPEATABLE READ — configurável por transação | Strongly consistent reads no índice primário apenas — GSIs são eventually consistent | — |
| Imutabilidade de registros | Enforced via trigger ou política de aplicação — não nativo, mas auditável via WAL | Enforced via IAM policy (deny UpdateItem/DeleteItem) — mais simples de implementar | — |
| Reconciliação por período | Query SQL com partição por data — executa in-database sem ETL | Requer export para S3 + Athena ou processamento via Streams — latência e custo adicionais | — |
| Escala de escrita | Vertical (até ~200k IOPS em Aurora I/O-Optimized) — suficiente para ~500k lançamentos/dia | Horizontal ilimitado — necessário apenas em volumes de processadora de cartão (>10M tx/dia) | — |
| Custo estimado (referência) | ~$400-800/mês (db.r6g.xlarge Multi-AZ + storage) — custo fixo previsível | ~$200-600/mês (on-demand WCU/RCU + GSIs) — variável, pode superar Aurora com múltiplos GSIs | — |
| Curva de aprendizado operacional | Baixa para equipes com background relacional — SQL, índices, explain plan | Alta — modelagem single-table, GSI design, hot partition avoidance, capacity planning | — |

## A Questão da Imutabilidade e Auditabilidade

Nenhum dos dois bancos oferece imutabilidade de registros como primitiva nativa no sentido que reguladores financeiros exigem. Ambos permitem deleção e atualização por padrão. A diferença está em onde e como você implementa a restrição.

No DynamoDB, a abordagem mais limpa é via política IAM: você cria um role de aplicação que tem permissão de `PutItem` mas não de `UpdateItem` ou `DeleteItem`. Isso é elegante e difícil de contornar acidentalmente. O problema é que não resolve a questão de auditoria de *quem* escreveu *o quê* e *quando* com semântica de banco de dados — você depende de CloudTrail para isso, e CloudTrail tem latência e não captura o conteúdo do item.

No Aurora PostgreSQL, a abordagem é diferente: você implementa um trigger `BEFORE UPDATE OR DELETE` que rejeita qualquer operação nas tabelas de lançamento (exceto por um role de sistema específico para estornos auditados). O WAL (Write-Ahead Log) do PostgreSQL, combinado com Aurora's continuous backup, cria uma trilha de auditoria de baixo nível que captura cada mudança com precisão de microsegundo. Para auditoria de alto nível, você adiciona uma tabela de `audit_log` preenchida por trigger, ou usa uma extensão como `pgaudit`.

A combinação de trigger de imutabilidade + pgaudit + Aurora Backtrack (que permite retroceder o banco para qualquer ponto nos últimos 72 horas) cria uma camada de auditabilidade que é difícil de replicar no DynamoDB sem infraestrutura adicional.

Há um padrão que considero superior para ambos os casos: **event sourcing com projeção**. O ledger primário é uma tabela de eventos imutáveis (`ledger_events`) onde cada linha representa um lançamento contábil e nunca é modificada. Saldos são projeções computadas sobre esses eventos — seja em tempo real via query, seja materializados em uma tabela de saldo atualizada por trigger. Esse padrão funciona bem no Aurora e seria a única forma de usar DynamoDB de maneira segura para este caso. Mas se você vai implementar event sourcing de qualquer forma, o Aurora oferece mais ferramentas para manter a consistência das projeções dentro da mesma transação.

## Decisão

**Status:** accepted

**Contexto**

Sistema de core banking com ~500k lançamentos contábeis/dia, requisito de consistência forte, múltiplos padrões de leitura (saldo, extrato, reconciliação, auditoria), equipe com background relacional, e necessidade de auditabilidade regulatória. Volume não justifica escala horizontal de DynamoDB no horizonte de 3 anos.

**Decisão**

Adotar **Aurora PostgreSQL** como motor de persistência do ledger de dupla entrada, com as seguintes decisões de design complementares: (1) modelagem de event sourcing — tabela `ledger_events` imutável como fonte da verdade, tabela `account_balances` como projeção materializada atualizada por trigger; (2) particionamento declarativo por range de data em `ledger_events` para isolar reconciliação histórica de operações correntes; (3) RDS Proxy para gerenciamento de conexões em ambiente com Lambda; (4) Aurora Global Database para DR multi-região com RPO < 1s; (5) pgaudit habilitado para trilha de auditoria de nível de statement. DynamoDB é reservado para o store de eventos de domínio downstream (EventBridge Pipes → DynamoDB) onde consistência eventual é aceitável.

**Consequências**
- ✅ Atomicidade de dupla entrada garantida por transações SQL nativas — zero código de compensação na aplicação
- ✅ Todos os padrões de leitura cobertos por SQL sem duplicação de dados ou GSIs adicionais
- ✅ Trilha de auditoria completa via pgaudit + WAL + Aurora Backtrack
- ⚠️ Escala horizontal requer sharding explícito se volume ultrapassar capacidade de instância única — ponto de revisão em 18 meses
- ⚠️ RDS Proxy é obrigatório para workloads serverless — adiciona ~1-2ms de latência e custo adicional
- ⚠️ Trigger de projeção de saldo cria acoplamento entre escrita e leitura dentro da mesma transação — monitorar lock contention em contas de alto volume

## Arquitetura Resultante: Ledger de Dupla Entrada com Aurora

Fluxo de uma transação financeira desde a API até a persistência no ledger, projeção de saldo, e propagação de evento downstream. O DynamoDB aparece apenas como store de eventos de domínio — fora do caminho crítico de consistência.

### 🌐 Ingress

- Client (mobile/web) (user)
- API Gateway REST (edge)

### ⚙️ Application Layer

- Lambda Transaction Handler (compute)
- RDS Proxy Connection Pool (network)

### 🗄️ Aurora Cluster (Primary — us-east-1)

- Aurora Writer PostgreSQL 15 (data)
- ledger_events (immutable, partitioned) (data)
- account_balances (materialized projection) (data)
- Aurora Reader (reconciliation / reports) (data)

### 🔒 Audit & Compliance

- pgaudit Statement Log (security)
- CloudWatch Logs Audit Trail (security)

### 📡 Event Propagation

- Lambda Stream Processor (compute)
- EventBridge Domain Events (messaging)
- DynamoDB Domain Event Store (data)

### 🌍 DR Region (us-west-2)

- Aurora Global DB Secondary Cluster (data)

### Fluxos

- client -> apigw: HTTPS
- apigw -> lambda_tx: invoke
- lambda_tx -> rds_proxy: SQL (TLS)
- rds_proxy -> aurora_writer: pool
- aurora_writer -> ledger_events: INSERT (BEGIN/COMMIT)
- ledger_events -> account_balances: trigger (projeção)
- aurora_writer -> pgaudit: statement log
- pgaudit -> cloudwatch_audit: export
- aurora_writer -> aurora_reader: replication
- aurora_writer -> aurora_global: global replication (RPO<1s)
- ledger_events -> lambda_stream: Aurora → Lambda (CDC)
- lambda_stream -> eventbridge: publish
- eventbridge -> dynamodb_events: EventBridge Pipes

## Avaliação pelo AWS Well-Architected Framework

- **security**: RDS Proxy com IAM authentication elimina credenciais de banco em variáveis de ambiente. pgaudit + CloudWatch Logs cria trilha de auditoria imutável. Row-level security no PostgreSQL isola dados por tenant. Aurora Encryption at rest (AES-256) e in-transit (TLS 1.2+) por padrão.
- **reliability**: Aurora Multi-AZ com failover automático em ~30s. Aurora Global Database para DR multi-região com RPO < 1s e RTO < 1 minuto (estimativa). Aurora Backtrack para recuperação de erros lógicos sem restore completo. Particionamento por data isola falhas de performance em partições antigas.
- **sustainability**: Aurora armazena dados em um storage distribuído compartilhado — sem replicação de dados entre réplicas de leitura, reduzindo footprint de storage. Particionamento permite arquivamento de partições antigas para S3 Glacier, reduzindo dados ativos no cluster.

> **Minha Perspectiva Senior:** A decisão entre Aurora e DynamoDB para um ledger não é uma decisão de banco de dados — é uma decisão sobre onde você quer que a complexidade viva. DynamoDB não elimina a complexidade de um ledger de dupla entrada; ele a move do banco de dados para o design de chaves, para os GSIs, para o código de transação da aplicação, e para os runbooks operacionais da sua equipe. Se sua equipe tem fluência nesse modelo, essa troca pode valer a pena. Para a maioria dos times de core banking que conheço, não vale.

O que eu faria diferente do que vejo com frequência: não usaria Aurora como um banco relacional tradicional com tabelas mutáveis de saldo. Implementaria event sourcing desde o início — `ledger_events` como append-only, saldos como projeções. Isso resolve o problema de imutabilidade de forma mais limpa do que triggers, e abre a porta para recalcular saldos a partir de eventos se uma projeção ficar inconsistente. O custo de implementar isso corretamente desde o início é menor do que migrar de um modelo mutável depois.

Sobre DynamoDB: ele tem lugar nessa arquitetura, mas não no caminho crítico de consistência. Como store de eventos de domínio downstream — onde você publica `TransactionCompleted`, `BalanceUpdated` — ele é excelente. Latência baixa, escala horizontal, sem gerenciamento de conexões. O erro é tentar fazê-lo ser o ledger primário porque o resto da plataforma já usa DynamoDB. Consistência eventual em saldo bancário não é um trade-off aceitável, independente de quão elegante seja o design de chaves.

Um ponto que frequentemente é ignorado: o custo de DynamoDB com múltip

## Veredicto

Aurora PostgreSQL é a escolha correta para este ledger de dupla entrada. Não porque DynamoDB seja inadequado como banco de dados — é excelente no que faz — mas porque as invariantes de um ledger financeiro (atomicidade, isolamento, imutabilidade, auditabilidade com semântica transacional) mapeiam diretamente para as primitivas do modelo relacional. A complexidade que DynamoDB exigiria para cobrir todos os padrões de acesso deste domínio não é justificada pelo throughput que ele oferece no volume projetado.

A decisão tem um ponto de revisão claro: se o volume de lançamentos ultrapassar a capacidade de uma instância Aurora em 18-24 meses, a arquitetura precisa evoluir para sharding horizontal — seja com Citus (PostgreSQL distribuído), seja com uma separação de domínio que isola contas de alto volume. Esse é o custo real da escolha: você troca escala horizontal automática por corretude transacional nativa, e precisa ter um plano para quando o volume eventualmente justificar a complexidade adicional.

O padrão de event sourcing com projeção materializada, combinado com Aurora's continuous backup e pgaudit, entrega uma base de auditabilidade que satisfaz requisitos regulatórios sem inf

## Referências

- [Amazon Aurora — Product Page](https://aws.amazon.com/rds/aurora/)
- [Amazon DynamoDB — Product Page](https://aws.amazon.com/dynamodb/)
- [Amazon Aurora PostgreSQL — Documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.AuroraPostgreSQL.html)
- [DynamoDB Transactions — Developer Guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transactions.html)
- [Aurora Global Database — Documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database.html)
- [RDS Proxy — Documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html)
- [Aurora I/O-Optimized — AWS Blog](https://aws.amazon.com/blogs/aws/new-amazon-aurora-i-o-optimized-cluster-configuration-with-up-to-40-cost-savings-for-i-o-intensive-applications/)
- [DynamoDB Best Practices for Financial Workloads — AWS Blog](https://aws.amazon.com/blogs/database/amazon-dynamodb-gaming-use-cases-and-design-patterns/)

## Fontes do caso

- [AWS — Amazon Aurora](https://aws.amazon.com/rds/aurora/)
- [AWS — Amazon DynamoDB](https://aws.amazon.com/dynamodb/)
