No primeiro artigo da série System Design: da teoria à prática, vimos que todo sistema é um conjunto de trade-offs. Falamos sobre latência, escalabilidade e adaptabilidade como princípios universais. Mas, na prática, onde esses trade-offs mais cedo ou mais tarde se manifestam?
A resposta é quase sempre a mesma: nos bancos de dados e sistemas de armazenamento.
É aqui que os gargalos aparecem primeiro, que os custos explodem e que a complexidade se multiplica. Não importa se você usa MySQL, Postgres, MongoDB ou DynamoDB: cedo ou tarde, você vai se deparar com problemas de índices, replicação, particionamento, consistência e cache.
Neste segundo artigo da série, mergulhamos em cinco princípios fundamentais para entender como projetar sistemas de dados mais resilientes e escaláveis, sempre equilibrando teoria, prática e exemplos do mundo real.
6. Índices são alavancas poderosas (e custosas)
Índices são a ferramenta clássica para acelerar consultas. Eles criam uma estrutura auxiliar de dados (B-Tree, Hash, GiST, etc.) que o banco mantém para permitir buscas rápidas.
- Benefício: consultas que poderiam varrer milhões de linhas passam a localizar registros em milissegundos.
- Custo: cada escrita precisa também atualizar os índices, aumentando latência de inserção/atualização e consumo de disco.
📚 Teoria:
- Database System Concepts (Silberschatz, Korth) detalha a estrutura de índices.
- Designing Data-Intensive Applications (Kleppmann) mostra trade-offs em workloads mistos.
Exemplo real:
Um e-commerce que indexa customer_id
em uma tabela de pedidos melhora buscas de histórico. Mas indexar status
em uma tabela com milhões de linhas, onde 95% são “completed”, gera mais custo que benefício — seletividade baixa.
Boa prática: medir index hit ratio e eliminar índices pouco usados. Ferramentas como pg_stat_user_indexes
(Postgres) ou sys.dm_db_index_usage_stats
(SQL Server) ajudam a identificar “índices mortos”.
7. Replicação ajuda leituras, particionamento ajuda escritas
À medida que os dados crescem, chega o momento de escalar horizontalmente. Mas replicação e particionamento não resolvem o mesmo problema.
Replicação
- O que é: cópias idênticas dos dados em múltiplos nós.
- Benefícios: melhora throughput de leitura, aumenta resiliência (failover).
- Custos: escritas continuam centralizadas no primário, e réplicas assíncronas podem servir dados desatualizados.
Exemplo: o Instagram usa Postgres com réplicas de leitura para dashboards e analytics. Assim, relatórios pesados não travam o nó primário.
Particionamento (sharding)
- O que é: dividir dados em múltiplos nós (cada shard armazena uma parte).
- Benefícios: distribui escritas e armazenamento, escala quase “infinitamente”.
- Custos: consultas que atravessam shards são caras, rebalanço é difícil, escolha errada de shard key gera hot partitions.
Exemplo: o Twitter particiona dados por ID de usuário. Isso resolve escala de escrita, mas dificulta queries globais como “tweets com hashtag X”.
Quando usar?
- Workload dominado por leitura: replicação resolve.
- Workload dominado por escrita: particionamento é inevitável.
- Grandes players: combinam ambos (shards com réplicas internas).
📚 Teoria:
- CAP theorem (Brewer, 2000): replicar é escolher entre consistência e disponibilidade.
- PACELC theorem (Abadi, 2010): mesmo sem falhas, há trade-off entre latência e consistência.
- Google Spanner: particionamento em tablets + replicação sincronizada com relógios atômicos.
Insight: replicação ≠ escala de escrita, particionamento ≠ simplicidade. É preciso diagnosticar onde dói mais antes de escolher.
8. Dual writes são uma armadilha
O padrão clássico: gravar em dois sistemas de forma independente.
INSERT INTO orders ...
PUBLISH order_created_event ...
O problema: se um commit falha, você gera drift entre sistemas.
📚 Teoria e soluções:
- Outbox Pattern (Chris Richardson): salvar evento junto com a transação local e publicar depois via CDC.
- Transactional outbox com Kafka + Debezium virou padrão de mercado.
- Papers sobre atomic broadcast e distributed consensus mostram que atomicidade sem coordenação é impossível.
Exemplo real: uma fintech brasileira sofreu inconsistências ao salvar transações em Postgres e Kafka separadamente. Resolveram adotando outbox + Debezium para replicar eventos com consistência.
Insight: dual writes simples nunca são seguros. Use outbox, sagas ou coordenação transacional.
9. Event stores vs filas: rastreabilidade vs simplicidade
Filas (SQS, RabbitMQ) entregam mensagens e descartam. Event stores (Kafka, EventStoreDB) tratam eventos como imutáveis, preservados para replay.
- Fila: simples, barata, ótima para jobs assíncronos.
- Event store: mais complexa, mas permite reconstruir o estado e auditar histórico.
📚 Teoria:
- Event Sourcing (Martin Fowler) — o estado como resultado de eventos.
- The Log: What Every Software Engineer Should Know About Real-Time Data (Jay Kreps).
Exemplos reais:
- Netflix usa Kafka como log de eventos de streaming.
- Nubank usa Event Sourcing em contas correntes para rastreabilidade total.
Insight: filas resolvem processos pontuais; event stores resolvem história.
10. Cache invalidation: o problema eterno
Invalidar cache é difícil porque é balancear freshness vs performance.
Estratégias comuns
- Write-through: grava banco e cache juntos.
- Write-around: grava só no banco; cache expira.
- Write-back: grava no cache e persiste depois.
📚 Referências:
- Phil Karlton: “There are only two hard things in Computer Science: cache invalidation and naming things.”
- High Performance Browser Networking (Ilya Grigorik).
- Blog da Cloudflare sobre TTL dinâmico.
Exemplo real: Stack Overflow já enfrentou inconsistências porque caches regionais serviam dados obsoletos após updates críticos.
Boa prática: medir stale hit ratio — quantas leituras servem dado velho — e ajustar TTL conforme SLA de frescor.
Conclusão — onde os gargalos realmente moram
Armazenamento é o coração da arquitetura. É aqui que os trade-offs deixam de ser teóricos e se tornam incidentes em produção: índices que pesam mais do que ajudam, replicações que entregam dados desatualizados, shards que criam hot partitions, dual writes que quebram consistência e caches que servem dados velhos.
Resumo deste capítulo:
- Índices: aceleram consultas, mas penalizam escritas.
- Replicação: melhora leitura, mas não resolve escrita.
- Particionamento: escala escrita, mas complica queries.
- Dual writes: inconsistência garantida sem coordenação.
- Event stores: rastreabilidade ao custo de simplicidade.
- Cache invalidation: um dos problemas mais difíceis da computação.
Este foi o Artigo 2 da série System Design: da teoria à prática.
No próximo, vamos explorar Confiabilidade e Consistência — idempotência, eventual consistency, conflitos em sistemas ativos-ativos e o preço da durabilidade.