# ADR: Sharding no Aurora — App-Level vs Aurora Limitless vs Citus

Um workload OLTP em alto crescimento esgotou a capacidade de um único writer Aurora PostgreSQL. Este ADR avalia três estratégias de sharding — implementação na camada de aplicação, Aurora Limitless Database e Citus/PostgreSQL gerenciado — pesando complexidade operacional, custo, suporte a queries cross-shard e risco de migração.

- URL: https://fernando.moretes.com/studies/adr-aurora-sharding-limitless-vs-app-level

- Markdown: https://fernando.moretes.com/studies/adr-aurora-sharding-limitless-vs-app-level/study.md?lang=pt

- Type: Decisão (ADR)

- Company: OLTP em alto crescimento (cenário)

- Domain: Dados

- Status: accepted

- Date: 2026-02-18

- Tags: aurora, sharding, postgresql, oltp, aurora-limitless, citus, data-platform, scalability

- Reading time: 9 min

---

Quando um único writer Aurora PostgreSQL deixa de ser suficiente, a decisão de como distribuir a carga não é trivial. Cada abordagem de sharding carrega um contrato diferente com o time de engenharia: controle total com custo cognitivo alto, managed service com restrições de modelo, ou extensão de banco com trade-offs de portabilidade. Este ADR documenta o raciocínio que me levaria a escolher Aurora Limitless como caminho primário — e quando eu mudaria de ideia.

## Contexto do Cenário

- **Sistema:** Plataforma OLTP em alto crescimento (cenário composto, não empresa específica)
- **Engine atual:** Aurora PostgreSQL 15, single writer, múltiplos read replicas
- **Escala do problema:** Writer saturado: CPU > 85% sustentado, WAL lag crescente, latência P99 de writes > 400ms
- **Volume estimado:** ~50k transações/segundo de pico, dataset principal ~4TB, crescimento de 15% ao mês
- **Padrão de acesso:** 80% das writes concentradas em entidade principal com shard key natural (tenant_id / account_id)
- **Restrições:** Time de dados de 6 engenheiros; SLA de 99.99%; janela de manutenção < 30 min/mês
- **Stack relevante:** AWS (us-east-1), Aurora PostgreSQL, Lambda, ECS, Terraform, Datadog
- **Status do ADR:** Accepted — Aurora Limitless como decisão primária

## O Problema Real: Por Que Sharding É a Última Opção, Não a Primeira

Antes de qualquer conversa sobre sharding, é obrigatório esgotar as alternativas verticais e de leitura. Neste cenário, já fizemos isso: o writer está em `db.r7g.16xlarge`, os read replicas absorvem 100% das queries de leitura via PgBouncer com pool de sessão, e os índices foram revisados por um DBA sênior. O problema é estrutural — o volume de writes não cabe em um único nó, e Aurora não oferece write scaling horizontal nativo no modelo clássico.

O diagnóstico importa aqui. O gargalo não é I/O de storage — Aurora desacopla storage de compute e o distributed storage layer escala automaticamente. O gargalo é o **processo de WAL writer e o lock manager** no nó de compute. Cada transação commited precisa passar pelo writer, serializar o WAL e aguardar o ACK do quorum de storage nodes. Com 50k TPS de pico e transações médias de 5-8 statements, estamos falando de centenas de milhares de operações de lock/segundo. Isso é um limite de compute, não de storage.

A segunda dimensão do problema é o crescimento. Com 15% de crescimento mensal, mesmo que um upgrade vertical comprasse 6 meses, estaríamos de volta ao mesmo ponto em menos de um ano — e com uma instância ainda maior, mais cara, e com menos opções de escape. Sharding não é apenas sobre o presente; é sobre criar uma arquitetura que escale com o negócio sem exigir uma nova decisão de plataforma a cada ciclo de crescimento.

A terceira dimensão é o time. Seis engenheiros de dados com SLA de 99.99% significa que cada hora de oncall não planejado é cara. A escolha de sharding não pode criar uma superfície operacional que o time não consiga sustentar. Isso elimina imediatamente qualquer solução que exija que o time implemente seu próprio rebalanceamento, gerenciamento de metadados de shard, ou roteamento de queries cross-shard do zero.

## As Forças em Jogo

Qualquer decisão de sharding envolve tensões que não se resolvem — só se gerenciam. Vou ser explícito sobre cada uma delas:

**Shard key e distribuição de dados.** Temos `tenant_id` como chave natural. Isso é uma vantagem enorme: a maioria das queries é scoped por tenant, o que significa que queries cross-shard são exceção, não regra. Mas tenants não são uniformes — alguns têm 100x mais dados que outros. Qualquer estratégia de sharding precisa lidar com hot shards de tenants grandes, seja via sub-sharding, seja via roteamento especial.

**Queries cross-shard.** Mesmo com 80% das queries scoped por tenant, os 20% restantes existem: relatórios agregados, queries de admin, joins entre tenants em features específicas. A pergunta não é se cross-shard queries acontecerão, mas qual é o custo de executá-las e quem paga esse custo — a aplicação, o banco, ou o engenheiro que precisa reescrever a query.

**Rebalanceamento.** Shards não são estáticos. Tenants crescem, novos tenants chegam, tenants saem. A capacidade de mover dados entre shards sem downtime é crítica. Soluções que exigem que o time implemente isso manualmente são um risco operacional sério para um time de 6 pessoas.

**Compatibilidade com PostgreSQL.** O sistema usa features específicas do PostgreSQL: `JSONB`, `pg_trgm` para full-text search parcial, e algumas stored procedures legadas. Qualquer solução precisa preservar essa compatibilidade ou o custo de migração de aplicação explode.

**Custo.** Aurora Limitless tem um modelo de pricing diferente do Aurora clássico — você paga por Distributed Processing Units (DPUs) além do storage. Para workloads que poderiam ser servidos por 2-3 writers independentes com sharding na aplicação, o custo do Limitless pode ser 30-50% maior (estimativa baseada em benchmarks públicos da AWS, não em medição direta neste cenário). Esse delta precisa ser justificado por redução de complexidade operacional.

**Vendor lock-in.** Aurora Limitless é AWS-specific. Citus é open-source mas tem uma versão gerenciada na Azure (Cosmos DB for PostgreSQL). App-level sharding com PostgreSQL padrão é o mais portável. Para um sistema com SLA de 99.99% e dependência crítica de negócio, a portabilidade tem valor real — mas não é gratuita.

## Opções Avaliadas

### Opção A: Sharding na Camada de Aplicação

**Pros**
- Controle total sobre lógica de roteamento e rebalanceamento
- Máxima portabilidade — qualquer PostgreSQL funciona como shard
- Custo de infra potencialmente menor (Aurora writers independentes)
- Sem dependência de features experimentais ou managed services específicos

**Cons**
- Complexidade de implementação alta: shard registry, connection routing, distributed transactions
- Queries cross-shard precisam ser implementadas e mantidas na aplicação (scatter-gather manual)
- Rebalanceamento de shards é responsabilidade do time — risco operacional sério
- Migrations de schema precisam ser coordenadas em todos os shards simultaneamente
- Referência: Figma e Notion levaram 12-18 meses para estabilizar suas implementações

**Verdict:** Viável para times grandes com expertise em distributed systems. Inadequado para time de 6 engenheiros com SLA crítico.

### Opção B: Aurora Limitless Database

**Pros**
- Sharding transparente gerenciado pela AWS — sem shard registry na aplicação
- Rebalanceamento automático de shards sem downtime
- Interface PostgreSQL padrão — compatibilidade com drivers existentes via Transaction Router
- Escala horizontal de writes sem mudança de arquitetura de aplicação
- Integração nativa com ecossistema AWS (IAM, CloudWatch, Parameter Groups)

**Cons**
- Vendor lock-in: feature exclusiva da AWS, sem path de migração direto para outro provider
- Custo de DPUs adicional — pode ser 30-50% mais caro que sharding manual em escala moderada (estimativa)
- Limitações documentadas: algumas DDLs e features PostgreSQL não suportadas no modelo distribuído
- Feature relativamente nova (GA em 2024) — menor histórico de battle-tested em produção
- Shard key deve ser definida na criação da tabela — mudança posterior é complexa

**Verdict:** Melhor trade-off para este cenário: reduz drasticamente a carga operacional mantendo compatibilidade PostgreSQL e escalabilidade de writes.

### Opção C: Citus / PostgreSQL Gerenciado (Azure ou self-managed)

**Pros**
- Open-source com extensão madura e bem documentada
- Suporte nativo a queries distribuídas, incluindo aggregations e joins cross-shard
- Portabilidade maior que Limitless — pode rodar em qualquer cloud ou on-prem
- Rebalanceamento de shards suportado nativamente pela extensão

**Cons**
- Self-managed Citus: carga operacional alta — upgrades, HA, backups, rebalanceamento são responsabilidade do time
- Managed Citus (Azure Cosmos DB for PostgreSQL): mudança de cloud provider — custo de migração de infra e operacional alto
- Modelo de coordinator/worker introduz single point of failure no coordinator (mitigável com HA, mas adiciona complexidade)
- Compatibilidade com algumas features PostgreSQL avançadas pode variar por versão

**Verdict:** Excelente opção técnica se o sistema não estiver fortemente acoplado à AWS ou se portabilidade for prioridade estratégica. Não é o caso aqui.

## Decisão de Arquitetura

**Status:** accepted

**Contexto**

Workload OLTP com writer Aurora PostgreSQL saturado, time de dados de 6 engenheiros, SLA de 99.99%, shard key natural disponível (tenant_id), e stack fortemente integrada à AWS. As alternativas de escala vertical foram esgotadas. A decisão precisa balancear escalabilidade de writes, carga operacional e custo.

**Decisão**

Adotar **Aurora Limitless Database** como estratégia de sharding primária, com `tenant_id` como shard key para as tabelas de alta escrita. Tabelas de referência (lookup tables, configurações) serão definidas como reference tables no Limitless para evitar joins cross-shard. A migração será feita via dual-write com validação de consistência antes do cutover. Manter Aurora clássico como fallback por 90 dias pós-migração.

**Consequências**
- ✅ Escala de writes horizontal sem mudança de lógica de aplicação — o Transaction Router do Limitless absorve o roteamento
- ✅ Rebalanceamento automático de shards elimina a principal fonte de risco operacional do sharding manual
- ✅ Compatibilidade com drivers PostgreSQL existentes — sem refatoração de DAL (Data Access Layer)
- ⚠️ Vendor lock-in aumenta: saída do Limitless exige migração para Aurora clássico com sharding manual ou Citus
- ⚠️ Custo de DPUs precisa ser monitorado — risco de custo não linear com crescimento de TPS
- ⚠️ Shard key (tenant_id) precisa estar presente em todas as queries de escrita nas tabelas distribuídas — queries sem shard key farão broadcast e serão caras

## Por Que Não Sharding na Aplicação: A Lição de Figma e Notion

Figma e Notion são casos bem documentados de sharding na camada de aplicação. Ambos funcionaram — mas ambos têm times de engenharia de dados de dezenas de pessoas, anos de runway para estabilizar a implementação, e a capacidade de absorver os custos de uma migração complexa. O que esses casos ensinam não é que sharding na aplicação é uma boa ideia para todos — é que é uma boa ideia para quem tem os recursos para fazê-lo corretamente.

Para um time de 6 engenheiros com SLA de 99.99%, o custo real do sharding na aplicação não é o código de roteamento inicial. É o que vem depois: o primeiro hot shard que aparece às 2h da manhã e precisa ser rebalanceado manualmente; a migration de schema que precisa ser executada em 8 shards de forma coordenada com rollback testado; a query de relatório que alguém escreveu sem shard key e que agora está fazendo full scan em todos os shards; o bug no shard registry que faz 0.1% das writes irem para o shard errado e só é descoberto 3 dias depois.

Esses não são problemas hipotéticos. São os problemas que aparecem invariavelmente em implementações de sharding manual, e a pergunta é se o time tem capacidade de resolvê-los sem comprometer o SLA. A resposta honesta, para um time de 6 pessoas sem experiência prévia em sharding distribuído, é não.

Isso não significa que sharding na aplicação seja errado. Significa que tem um custo de maturidade organizacional que precisa ser reconhecido. Se o time crescer para 15-20 engenheiros com experiência específica em distributed systems, a equação muda — e a portabilidade do sharding manual se torna um ativo real. Hoje, não é.

## Arquitetura Alvo: Aurora Limitless com Roteamento por Tenant

Fluxo de write/read em produção após migração para Aurora Limitless. O Transaction Router é o ponto de entrada único — a aplicação não conhece os shard groups. Reference tables são replicadas em todos os shard groups para evitar joins cross-shard.

### 🖥️ Application Layer

- Application (ECS Tasks) (compute)
- PgBouncer Connection Pool (network)

### 🔀 Aurora Limitless — Router Layer

- Transaction Router (Limitless Endpoint) (data)

### 🗄️ Aurora Limitless — Shard Groups

- Shard Group 1 tenant_id: 0-33% (data)
- Shard Group 2 tenant_id: 33-66% (data)
- Shard Group 3 tenant_id: 66-100% (data)
- Reference Tables (replicated all shards) (data)

### 📊 Observability

- CloudWatch DPU / Latency / Errors (external)
- Datadog APM + DB Metrics (external)

### 🔒 Security

- IAM Auth DB Credentials via Secrets Manager (security)

### Fluxos

- app -> pgbouncer: queries PostgreSQL
- pgbouncer -> tr: pool → router
- tr -> sg1: tenant_id hash
- tr -> sg2: tenant_id hash
- tr -> sg3: tenant_id hash
- tr -> reftable: broadcast writes
- sg1 -> cw: métricas
- sg2 -> cw
- sg3 -> cw
- cw -> dd: forwarding
- iam -> tr: auth

## Comparação Técnica Detalhada
| Critério | Critério | App-Level Sharding | Aurora Limitless | Citus Gerenciado |
| --- | --- | --- | --- | --- |
| Carga operacional | Alta | Baixa | Média | — |
| Queries cross-shard | Manual (scatter-gather na app) | Suportado via Transaction Router | Suportado nativamente (Citus planner) | — |
| Rebalanceamento | Manual — responsabilidade do time | Automático pela AWS | Suportado pela extensão Citus | — |
| Compatibilidade PostgreSQL | Total (PostgreSQL padrão) | Alta, com limitações documentadas em DDL | Alta, com limitações em algumas queries complexas | — |
| Portabilidade | Máxima | Mínima (AWS-only) | Alta (open-source) | — |
| Custo relativo (estimativa) | Menor (Aurora writers independentes) | Maior (DPUs adicionais) | Médio (depende de managed vs self-hosted) | — |
| Maturidade da feature | Alta (padrão da indústria) | Média (GA 2024) | Alta (Citus existe desde 2012) | — |
| Tempo até produção (estimativa) | 12-18 meses para estabilizar | 3-6 meses (migração + validação) | 6-9 meses (se mudança de cloud) | — |

## Plano de Migração para Aurora Limitless

1. **Fase 1 — Auditoria (Semanas 1-3)** — Mapear todas as queries que não incluem tenant_id. Identificar stored procedures com DDL incompatível com Limitless. Catalogar reference tables candidatas. Medir distribuição de dados por tenant para identificar hot tenants.

2. **Fase 2 — Ambiente Limitless (Semanas 4-6)** — Provisionar cluster Aurora Limitless em ambiente de staging. Criar tabelas distribuídas com tenant_id como shard key. Criar reference tables para lookup data. Executar testes de carga com shadow traffic.

3. **Fase 3 — Dual-Write (Semanas 7-10)** — Implementar dual-write na DAL: todas as writes vão para Aurora clássico E Aurora Limitless. Reads continuam no Aurora clássico. Executar validação de consistência contínua entre os dois sistemas. Monitorar divergências.

4. **Fase 4 — Cutover Gradual (Semanas 11-14)** — Migrar reads para Aurora Limitless por tenant (começando com tenants menores). Monitorar latência P99 e error rate. Aumentar percentual gradualmente. Cutover completo quando 100% dos tenants validados.

5. **Fase 5 — Estabilização (Semanas 15-24)** — Manter Aurora clássico como fallback por 90 dias. Monitorar custo de DPUs vs projeção. Desativar dual-write. Após 90 dias sem incidentes, deprecar Aurora clássico e liberar recursos.

> **Minha Perspectiva: O Custo Invisível da Complexidade:** Quando eu analiso esse tipo de decisão, a primeira pergunta que faço não é técnica — é organizacional: quem vai acordar às 3h da manhã quando isso quebrar, e o que eles precisarão saber para resolver?

Sharding na aplicação é tecnicamente elegante quando bem feito. Mas 'bem feito' requer um nível de maturidade operacional que a maioria dos times subestima. O código de roteamento é a parte fácil. O difícil é o rebalanceamento de hot shards em produção com SLA ativo, a coordenação de schema migrations em múltiplos shards, e o debugging de inconsistências que só aparecem em queries cross-shard em horário de pico.

Aurora Limitless resolve exatamente esses problemas — ao custo de vendor lock-in e DPUs adicionais. Para um time de 6 engenheiros com SLA de 99.99%, esse é um trade-off que eu aceitaria sem hesitação. O custo de DPUs é previsível e monitorável; o custo de um incidente de sharding mal implementado às 3h da manhã não é.

O que eu faria diferente do que está documentado aqui: eu adicionaria um **circuit breaker explícito no Transaction Router** — se o Limitless retornar erros acima de um threshold, a aplicação cai automaticamente para o Aurora clássico (que permanece em dual-write por 90 dias). Isso não é paranoia; é reconhecer que o Limitless tem GA desde 2024 e que qualquer feature nova de banco de dados terá edge cases que só aparecem em produção.

Sobre Citus: é uma opção genuinamente boa que eu escolheria se o sistema não estivesse fortemente acoplado à AWS ou se houvesse um requisito estratégico de portabilidade. O fato de a versão managed estar na Azure é um problema real para um sistema AWS-first — não é impossível de resolver, mas adiciona uma camada de complexidade operacional que não está justificada aqui.

A lição geral: sharding é uma decisão de equipe, não só de arquitetura. A melhor solução técnica que o time não consegue operar é pior que uma solução boa o suficiente que o time domina.

## AWS Well-Architected: Impacto da Decisão

- **security**: IAM authentication para o endpoint Limitless via Secrets Manager. Encryption at rest e in transit herdados do Aurora. O Transaction Router é o único ponto de entrada — reduz superfície de ataque vs múltiplos writers independentes no modelo app-level sharding.
- **reliability**: Aurora Limitless herda a durabilidade do storage distribuído Aurora (6 cópias em 3 AZs). O rebalanceamento automático de shards reduz o risco de hot shard causar degradação de serviço. O plano de dual-write com fallback para Aurora clássico garante RTO < 5 minutos em caso de regressão.
- **performance**: O Transaction Router distribui writes por shard group baseado em tenant_id hash, eliminando o gargalo do single writer. Reference tables evitam joins cross-shard para dados de lookup. Queries sem shard key fazem broadcast — precisam ser monitoradas e eliminadas.
- **sustainability**: Consolidação de compute em Limitless vs múltiplos writers independentes pode reduzir consumo total de recursos se o Limitless for mais eficiente na alocação de DPUs por TPS. Requer validação com benchmarks reais.

## Veredicto

Aurora Limitless é a decisão correta para este cenário — não porque seja a solução tecnicamente mais elegante, mas porque é a que melhor alinha capacidade do time, restrições de SLA e velocidade de entrega.

Sharding na aplicação é uma solução legítima para times grandes com expertise em distributed systems e tempo para estabilizar a implementação. Para um time de 6 engenheiros com SLA de 99.99%, o custo operacional do sharding manual é proibitivo — não em dinheiro, mas em risco e capacidade cognitiva.

Citus é tecnicamente excelente, mas a versão managed está na Azure, e mover um sistema AWS-first para outro cloud provider para resolver um problema de banco de dados é uma troca que não faz sentido aqui.

Os riscos reais da decisão são dois: o custo de DPUs pode ser não-linear com crescimento de TPS (monitorar desde o primeiro dia), e o Limitless tem GA desde 2024 (manter o fallback para Aurora clássico por pelo menos 90 dias pós-cutover). Nenhum desses riscos invalida a decisão — eles precisam ser gerenciados.

A decisão de sharding é, em última análise, uma decisão sobre onde você quer que a complexidade viva: no código da aplicação, no managed service, ou na extensão do banco. Para este cenário, o managed service é a resposta certa.

## Referências

- [Amazon Aurora Limitless Database — Official Documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-limitless.html)
- [Amazon Aurora — Product Page](https://aws.amazon.com/rds/aurora/)

## Fontes do caso

- [Amazon Aurora — Limitless Database](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-limitless.html)
- [AWS — Amazon Aurora](https://aws.amazon.com/rds/aurora/)
