Designing Data-Intensive Applications: Capítulo 4 — Codificação e Evolução
Entenda os trade-offs entre JSON, formatos binários e mensageria assíncrona, e aprenda a escolher a melhor forma de codificar dados para seu sistema
Todo sistema muda com o tempo. No capítulo 4 do livro "Designing Data-Intensive Applications", Martin Kleppmann explora como diferentes sistemas lidam com essas mudanças inevitáveis, especialmente na forma como codificamos e transmitimos dados.
A maneira que escolhemos representar dados afeta profundamente como nossos sistemas podem evoluir. Uma escolha errada pode nos travar com uma tecnologia específica por anos.
✨ O que esperar do artigo
Como diferentes formatos de codificação afetam a evolução do seu sistema
Os trade-offs entre formatos textuais vs binários em diferentes cenários
Como garantir compatibilidade em diferentes fluxos de dados
Formatos de Codificação
Sempre que precisamos enviar dados pela rede ou salvá-los em um arquivo, precisamos convertê-los em uma sequência de bytes. Este processo é chamado de codificação (ou serialização).
O formato que escolhemos para essa codificação tem implicações profundas:
Na performance do sistema
Na facilidade de debug e troubleshooting
Na capacidade de evolução do código
Na interoperabilidade com outros sistemas
Por exemplo: dados em memória frequentemente usam ponteiros, que não fazem sentido quando enviados para outro processo. Portanto, precisamos de uma representação que seja:
Auto-contida
Independente de máquina
Fácil de versionar
Vamos explorar os principais formatos usados na indústria, seus trade-offs e casos de uso ideais.
O Problema com Formatos Específicos de Linguagem
Muitas linguagens oferecem formas simples de codificar dados:
Java tem
java.io.Serializable
Python tem
pickle
Ruby tem
Marshal
Embora convenientes, esses formatos têm problemas sérios:
Acoplados a uma linguagem específica
Frequentemente inseguros (podem executar código arbitrário)
Geralmente ineficientes
Péssimos para evolução de schema
Recomendação: evite formatos específicos de linguagem exceto para uso muito temporário, como cache em memória.
Formatos Textuais: JSON, XML, CSV
JSON e XML dominam APIs públicas e integração entre organizações por bons motivos:
Vantagens:
Legíveis por humanos
Amplamente suportados
Auto-descritivos
Debugging mais fácil
Ferramentas universais (curl, jq, navegadores)
Limitações:
Ambiguidade em tipos de dados (números especialmente)
Sem suporte nativo para dados binários
Schemas opcionais
Menos eficientes em espaço
{
"userName": "Martin",
"favoriteNumber": 1337,
"interests": ["daydreaming", "hacking"]
}
Por que continuarão populares:
Padrão de facto para APIs públicas
Excelentes para debugging e desenvolvimento
Ecossistema maduro de ferramentas
A dificuldade de conseguir acordo entre organizações supera preocupações com formato
Formatos Binários com Schema: A Evolução Necessária
Para comunicação interna entre serviços, formatos binários como Protocol Buffers (protobuf), Thrift e Avro oferecem vantagens significativas:
Vantagens principais:
Mais compactos por omitirem nomes de campos
Schema como documentação que não pode ficar desatualizada
Verificação automática de compatibilidade entre versões
Geração de código tipo-seguro para linguagens estáticas
Melhor performance e garantias em tempo de compilação
Exemplo em protobuf:
message Person {
required string user_name = 1;
optional int64 favorite_number = 2;
repeated string interests = 3;
}
Schema Resolution com Avro
Avro se destaca por sua abordagem única para evolução de schema. Em vez de usar números de tag como Protocol Buffers e Thrift, Avro usa um mecanismo chamado schema resolution.
Como funciona:
Cada dado tem um "writer's schema" (usado para escrever) e um "reader's schema" (usado para ler)
Avro reconcilia as diferenças entre eles automaticamente
Campos podem ser reorganizados e renomeados desde que os tipos sejam compatíveis
Geração de Código e Type Safety
Um benefício significativo de formatos binários com schema é a geração automática de código. Em vez de lidar manualmente com bytes, você trabalha com tipos nativos da sua linguagem.
Como Funciona:
Você define o schema (em
.proto
,.avdl
ou similar)O compilador gera classes/structs na sua linguagem
O código gerado lida com codificação/decodificação
Seu código usa tipos fortemente tipados
Por exemplo, este schema protobuf:
message User {
required string name = 1;
optional int64 id = 2;
repeated string roles = 3;
}
Gera em Java:
public class User {
private String name; // required
private Long id; // optional
private List<String> roles; // repeated
public String getName() { ... }
public void setName(String value) { ... }
// outros getters/setters
}
E em Go:
type User struct {
Name string `protobuf:"bytes,1,req,name=name"`
Id *int64 `protobuf:"varint,2,opt,name=id"`
Roles []string `protobuf:"bytes,3,rep,name=roles"`
}
Benefícios para Linguagens Compiladas:
Erros de tipo são pegos em tempo de compilação
IntelliSense e navegação no código IDE
Refatoração mais segura
Performance otimizada para a linguagem
Trade-offs:
Precisa adicionar passo de geração no build
Código gerado precisa ser versionado
Menos flexível que formatos dinâmicos
Pode ser complexo para linguagens dinâmicas
Para linguagens como Python ou JavaScript, que são dinamicamente tipadas, a geração de código é menos crucial. Nesses casos, um parser dinâmico que lê o schema em runtime pode ser mais apropriado.
Casos de Uso Ideais
Protocol Buffers/Thrift: Microserviços internos, RPCs, dados estruturados fixos
Avro: Dados com schema dinâmico, big data, integrações com databases
JSON/XML: APIs públicas, integrações entre organizações, debugging
Padrões Fundamentais de Fluxo de Dados
Hoje, praticamente todos os sistemas de software são distribuídos. Dados precisam fluir constantemente entre diferentes processos, máquinas e data centers.
Este fluxo de dados apresenta desafios únicos:
Processos podem estar em versões diferentes
Dados podem persistir por anos ou décadas
Sistemas precisam evoluir sem interrupção
Falhas de rede são inevitáveis
Um princípio fundamental: "Data Outlives Code"
Diferente do código, que pode ser atualizado de uma vez, dados têm uma natureza duradoura:
Dados escritos hoje podem ser lidos anos depois
O código original pode não existir mais
Múltiplas versões de schemas precisam coexistir
Mudanças precisam manter compatibilidade
Por exemplo, durante deploys graduais:
Alguns nós rodam código novo
Outros ainda rodam código antigo
Todos precisam ler e escrever dados corretamente
Vamos explorar as três principais maneiras como dados fluem em sistemas modernos e como cada uma lida com esses desafios.
1. Através de Bancos de Dados
Um processo escreve dados, outro lê depois
O banco atua como intermediário
Processos são desacoplados no tempo
Desafio: manter compatibilidade entre diferentes versões do schema
2. Através de Serviços (REST/RPC)
Comunicação síncrona via rede
Um processo faz requisição, espera resposta
Acoplamento temporal: ambos precisam estar disponíveis
Desafio: lidar com falhas de rede e latência
Os Desafios Fundamentais de RPCs
RPCs (Remote Procedure Calls) tentam fazer chamadas remotas parecerem locais. Embora conveniente à primeira vista, essa abstração é fundamentalmente problemática:
Por que chamadas remotas são diferentes:
Imprevisibilidade
Chamadas locais são previsíveis: funcionam ou falham
Chamadas remotas podem falhar parcialmente
A rede pode ficar lenta ou indisponível
Você precisa lidar com timeouts e retentativas
Latência
Chamadas locais são rápidas e consistentes
Chamadas remotas são ordens de magnitude mais lentas
A latência varia muito: de milissegundos a segundos
Você precisa projetar sistemas assumindo alta latência
Estado Parcial
Em falhas locais, você sabe exatamente o que aconteceu
Em falhas remotas, você não sabe se a requisição:
Nunca chegou
Chegou mas falhou
Foi processada mas a resposta se perdeu
Consistência
Retentativas podem causar execução duplicada
Você precisa projetar operações para serem idempotentes
O sistema deve lidar com processamento duplicado
3. Através de Mensageria (Comunicação Assíncrona)
Message brokers oferecem um modelo diferente de comunicação:
Vantagens:
Desacoplamento
Produtores não precisam conhecer consumidores
Serviços podem evoluir independentemente
Resiliência
Mensagens persistem mesmo se o consumidor cai
Sistemas podem se recuperar de falhas
O message broker garante a entrega
Escalabilidade
Filas podem absorver picos de carga
Múltiplos consumidores podem processar em paralelo
Os principais desafios nesse modelo são garantir ordenação e consistência das mensagens. Em mais detalhes:
Compatibilidade
Produtores novos, consumidores antigos
Produtores antigos, consumidores novos
Necessidade de preservar campos desconhecidos
Ordenação
Mensagens podem chegar fora de ordem
Diferentes versões podem coexistir no sistema
Necessidade de lidar com processamento não-sequencial
Alguns exemplos de tecnologias que ajudam com esse tipo de comunicação: Kafka, RabbitMQ, AWS Simple Notification Service (SNS)
Cada padrão tem seus trade-offs:
Bancos de Dados
✅ Dados persistentes
✅ Queries complexas possíveis
❌ Schemas podem ser rígidos
❌ Pode se tornar gargalo se múltiplos processos usarem ele
Serviços (REST/RPC)
✅ Resposta imediata
✅ Mais simples do que mensageria
❌ Acoplamento temporal
❌ Menos resiliente a falhas
Mensageria
✅ Alta resiliência
✅ Desacoplamento
❌ Maior complexidade
❌ Mais difícil de debugar
A escolha do padrão de fluxo de dados impacta diretamente:
Como seu sistema evolui
Como você lida com falhas
Que tipos de garantias você pode oferecer
Quão fácil é fazer mudanças
Estratégias para Evolução de Sistemas
Rolling Upgrades
Deploy gradual em poucos nós por vez
Monitore problemas antes de continuar
Mantenha capacidade de rollback
Feature Flags
Código novo entra desativado em produção
Ative gradualmente para grupos de usuários
Permite testar em ambiente real
Migração de Dados
Evite migrações big-bang
Mantenha período de transição
Use dual-writing quando possível. Ou seja, se estiver substituindo uma tabela, por hora escreva tanto na antiga quanto na nova
📚 Leituras Recomendadas
Cases de Empresas
Blogs de Engenharia sobre Formatos Binários
Migrações e Evolução
Aprofundamento Técnico
🌟 Resumo
Use formatos textuais (JSON/XML) para APIs públicas e interoperabilidade. Sempre comece por esse formato - mude para um formato mais sofisticado (como Protocol Buffers) quando houver necessidade
Considere formatos binários para comunicação interna e alta performance
Escolha baseada no seu contexto: público-alvo, escala, frequência de mudanças
Planeje para evolução desde o início
Mantenha compatibilidade tanto retroativa quanto futura para permitir deploys graduais
Se você quiser fazer mais algo pra me ajudar, compartilhe esse artigo com outras pessoas e clique no botão de ❤️ curtir.
Quer fazer parte da comunidade? Entre em nosso servidor no Discord! 💬
O que tu tá achando do livro? Tô terminando o DDD do Evans, e esse vai ser o próximo que eu quero ler. Vou aproveitar esses seus resumos nos estudos aqui rs.
Mais um texto irado! Preciso revisitar protobuf. Última vez que mexi foi em 2019 e o ecossistema ainda não era tão robusto e utilizado amplamente.
Você gosta de algum projeto open-source que utilize gRPC?