Scalability is one of the most misunderstood goals in software architecture. Teams often treat it as an afterthought — something to bolt on when the system is already under strain. In practice, the decisions made in the first few sprints determine whether your system can grow gracefully or requires a painful rewrite at scale.Escalabilidade é um dos objetivos mais mal compreendidos em arquitetura de software. Equipes frequentemente a tratam como uma reflexão tardia — algo a ser adicionado quando o sistema já está sob pressão. Na prática, as decisões tomadas nos primeiros sprints determinam se o sistema pode crescer de forma fluida ou se vai exigir uma reescrita dolorosa em escala.
This article walks through the core patterns and principles I've applied in production systems to design architectures that scale horizontally, remain maintainable under team growth, and tolerate the inevitable failure modes of distributed computing.Este artigo percorre os padrões e princípios fundamentais que apliquei em sistemas em produção para projetar arquiteturas que escalam horizontalmente, permanecem fáceis de manter com o crescimento da equipe, e toleram os inevitáveis modos de falha da computação distribuída.
The Foundation: Understanding Your Scalability AxisA Base: Entendendo Seu Eixo de Escalabilidade
Before choosing an architecture pattern, define what needs to scale. The three dimensions are:Antes de escolher um padrão de arquitetura, defina o que precisa escalar. As três dimensões são:
- X-axis scaling: Running multiple identical instances behind a load balancer. Simple, effective for stateless services.Escalonamento no eixo X: Executar múltiplas instâncias idênticas atrás de um balanceador de carga. Simples, eficaz para serviços sem estado.
- Y-axis scaling: Splitting by function — decomposing a monolith into services organized around business capabilities.Escalonamento no eixo Y: Divisão por função — decompor um monólito em serviços organizados em torno de capacidades de negócio.
- Z-axis scaling: Partitioning by data — sharding by customer ID, geography, or tenant. Requires careful key design.Escalonamento no eixo Z: Particionamento por dados — sharding por ID do cliente, geografia ou tenant. Requer um design cuidadoso de chaves.
Principle: Most teams jump to Y-axis (microservices) prematurely. Start with X-axis. Decompose only when you can draw a clean boundary around a business capability with independent data ownership.Princípio: A maioria das equipes pula para o eixo Y (microsserviços) prematuramente. Comece com o eixo X. Decomponha apenas quando puder traçar uma fronteira clara em torno de uma capacidade de negócio com propriedade independente de dados.
Domain-Driven Design as the CompassDomain-Driven Design como Bússola
The hardest part of microservices is not the technology — it's identifying the right service boundaries. Domain-Driven Design (DDD) provides the tools to do this systematically.A parte mais difícil dos microsserviços não é a tecnologia — é identificar as fronteiras certas dos serviços. O Domain-Driven Design (DDD) fornece as ferramentas para fazer isso de forma sistemática.
Bounded ContextsContextos Delimitados
A Bounded Context defines the scope within which a particular domain model applies. The same term ("Order") can mean very different things in Billing, Logistics, and Customer Support. Each context owns its model, its data, and its schema.Um Contexto Delimitado define o escopo no qual um modelo de domínio específico se aplica. O mesmo termo ("Pedido") pode significar coisas muito diferentes em Faturamento, Logística e Suporte ao Cliente. Cada contexto possui seu modelo, seus dados e seu schema.
lifecycle, status
invoices, payments
shipment, tracking
Aggregates and InvariantsAgregados e Invariantes
An Aggregate is the unit of transactional consistency. Operations that must happen atomically live within one Aggregate. Communication between Aggregates is always eventual — via domain events, never synchronous calls within a transaction.Um Agregado é a unidade de consistência transacional. Operações que devem acontecer atomicamente residem dentro de um Agregado. A comunicação entre Agregados é sempre eventual — via eventos de domínio, nunca chamadas síncronas dentro de uma transação.
Rule of thumb: If two entities must always be consistent at the same moment, they belong in the same Aggregate. If they can tolerate a few seconds of lag, they belong in separate Aggregates — potentially in separate services.Regra geral: Se duas entidades precisam sempre ser consistentes ao mesmo tempo, elas pertencem ao mesmo Agregado. Se podem tolerar alguns segundos de defasagem, pertencem a Agregados separados — potencialmente em serviços separados.
CQRS: Separating Reads from WritesCQRS: Separando Leituras de Escritas
Command Query Responsibility Segregation (CQRS) is one of the highest-leverage patterns in distributed systems. The core idea is simple: the model used to update state does not need to be the same model used to read it.O CQRS (Command Query Responsibility Segregation) é um dos padrões de maior alavancagem em sistemas distribuídos. A ideia central é simples: o modelo usado para atualizar o estado não precisa ser o mesmo usado para lê-lo.
- Commands mutate state, enforce invariants, and are processed by the Aggregate.Commands mutam o estado, impõem invariantes e são processados pelo Agregado.
- Queries are read-only, can be served from a denormalized projection, and can be cached aggressively.Queries são somente leitura, podem ser servidas a partir de uma projeção desnormalizada e podem ser cacheadas agressivamente.
# Command side — enforces business rules
class PlaceOrderCommand:
customer_id: str
items: List[OrderItem]
payment_method: str
# Query side — optimized read model (no joins needed)
class OrderSummaryView:
order_id: str
customer_name: str
total_amount: Decimal
status: str
last_updated: datetimeThe write side can use a normalized relational schema (optimized for consistency), while the read side uses a document store or a pre-computed projection table (optimized for query patterns). This allows each side to scale independently.O lado de escrita pode usar um schema relacional normalizado (otimizado para consistência), enquanto o lado de leitura usa um document store ou uma tabela de projeção pré-computada (otimizada para padrões de consulta). Isso permite que cada lado escale independentemente.
Event-Driven CommunicationComunicação Orientada a Eventos
In a distributed system, synchronous REST calls between services create temporal coupling — if Service B is down, Service A fails too. Event-driven architecture breaks this dependency.Em um sistema distribuído, chamadas REST síncronas entre serviços criam acoplamento temporal — se o Serviço B estiver fora do ar, o Serviço A também falha. A arquitetura orientada a eventos quebra essa dependência.
Domain Events vs. Integration EventsEventos de Domínio vs. Eventos de Integração
- Domain Events record something that happened within a Bounded Context. They are facts:
OrderPlaced,PaymentProcessed.Eventos de Domínio registram algo que aconteceu dentro de um Contexto Delimitado. São fatos:OrderPlaced,PaymentProcessed. - Integration Events are published to a message broker (Kafka, RabbitMQ) so other services can react asynchronously. They are derived from Domain Events but are designed for external consumption.Eventos de Integração são publicados em um message broker (Kafka, RabbitMQ) para que outros serviços possam reagir de forma assíncrona. São derivados de Eventos de Domínio, mas projetados para consumo externo.
# Integration event published to Kafka
{
"eventType": "order.placed",
"eventVersion": "1.0",
"orderId": "ord_789xyz",
"customerId": "cust_123",
"totalAmount": 149.99,
"occurredAt": "2025-03-15T10:22:00Z"
}Idempotency is Non-NegotiableIdempotência é Inegociável
Message brokers guarantee at-least-once delivery. Your consumers will receive the same event twice. Design handlers to be idempotent: processing the same event twice must produce the same result as processing it once. The simplest mechanism is an idempotency key stored in a processed-events table.Message brokers garantem entrega pelo menos uma vez. Seus consumidores receberão o mesmo evento duas vezes. Projete handlers para serem idempotentes: processar o mesmo evento duas vezes deve produzir o mesmo resultado que processar uma vez. O mecanismo mais simples é uma chave de idempotência armazenada em uma tabela de eventos processados.
The Strangler Fig: Migrating from Monolith to ServicesO Strangler Fig: Migrando do Monólito para Serviços
Greenfield microservices are rare. Most engineers face the problem of extracting services from an existing monolith. The Strangler Fig pattern allows incremental migration without a big-bang rewrite:Microsserviços em projetos do zero são raros. A maioria dos engenheiros enfrenta o problema de extrair serviços de um monólito existente. O padrão Strangler Fig permite migração incremental sem uma reescrita completa de uma vez:
- Put a facade (API Gateway or reverse proxy) in front of the monolith.Coloque uma fachada (API Gateway ou proxy reverso) na frente do monólito.
- Identify a bounded context to extract — ideally one with high change frequency and clear data ownership.Identifique um contexto delimitado para extrair — idealmente com alta frequência de mudanças e propriedade de dados clara.
- Build the new service, implement the same API contract.Construa o novo serviço, implemente o mesmo contrato de API.
- Route traffic to the new service via the facade. Keep the monolith code until metrics confirm stability.Roteie o tráfego para o novo serviço via fachada. Mantenha o código do monólito até que as métricas confirmem estabilidade.
- Delete the monolith code for that context. Repeat.Exclua o código do monólito para aquele contexto. Repita.
Lesson learned: Never try to extract a service and change its behavior at the same time. Extract first, validate parity, then evolve. Mixing both makes debugging production incidents nearly impossible.Lição aprendida: Nunca tente extrair um serviço e mudar seu comportamento ao mesmo tempo. Extraia primeiro, valide a paridade, depois evolua. Misturar ambos torna a depuração de incidentes em produção quase impossível.
Resilience PatternsPadrões de Resiliência
Scalability without resilience is fragile. Three patterns every distributed system must implement:Escalabilidade sem resiliência é frágil. Três padrões que todo sistema distribuído deve implementar:
Circuit BreakerCircuit Breaker
Wraps calls to external services. When the failure rate exceeds a threshold, the circuit "opens" and subsequent calls fail immediately (fast fail) instead of waiting for a timeout. This prevents cascading failures across the system.Envolve chamadas a serviços externos. Quando a taxa de falhas ultrapassa um limiar, o circuito "abre" e chamadas subsequentes falham imediatamente (fast fail) em vez de aguardar um timeout. Isso previne falhas em cascata pelo sistema.
BulkheadBulkhead
Isolates critical resources using separate thread pools or connection pools per downstream dependency. A slow database call consuming all connections won't starve the thread pool handling user-facing requests.Isola recursos críticos usando thread pools ou connection pools separados por dependência downstream. Uma chamada lenta ao banco de dados consumindo todas as conexões não vai privar o thread pool que lida com requisições voltadas ao usuário.
Retry with Exponential BackoffRetry com Backoff Exponencial
Transient failures are normal in distributed systems. Retrying immediately can worsen an already overloaded downstream. Exponential backoff (with jitter) gives the downstream service time to recover without creating a thundering herd.Falhas transitórias são normais em sistemas distribuídos. Tentar novamente imediatamente pode piorar um downstream já sobrecarregado. O backoff exponencial (com jitter) dá tempo ao serviço downstream para se recuperar sem criar um efeito de manada.
# Retry with jitter (pseudocode)
def call_with_retry(fn, max_attempts=3):
for attempt in range(max_attempts):
try:
return fn()
except TransientError:
if attempt == max_attempts - 1:
raise
wait = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait)Database Scaling StrategiesEstratégias de Escalabilidade do Banco de Dados
Application servers are stateless and scale trivially with X-axis replication. Databases are stateful — they are almost always the first bottleneck when traffic grows. Three progressive strategies:Servidores de aplicação são sem estado e escalam trivialmente com replicação no eixo X. Bancos de dados são stateful — são quase sempre o primeiro gargalo quando o tráfego cresce. Três estratégias progressivas:
1. Read Replicas1. Réplicas de Leitura
Replicate the primary to one or more read replicas. Route all reads to replicas, all writes to the primary. Effective when your workload is read-heavy (typical for user-facing APIs: 80–90% reads). Requires that your application can tolerate replication lag — usually milliseconds, occasionally seconds during high write load.Replique o primário para uma ou mais réplicas de leitura. Roteie todas as leituras para as réplicas, todas as escritas para o primário. Eficaz quando sua carga de trabalho é predominantemente de leitura (típico para APIs voltadas ao usuário: 80–90% leituras). Requer que a aplicação tolere o lag de replicação — geralmente milissegundos, ocasionalmente segundos durante alta carga de escrita.
2. Vertical Partitioning (Decomposition)2. Particionamento Vertical (Decomposição)
Split a single large database into smaller ones, each owned by a different service (aligns with DDD Bounded Contexts). The Orders service owns its schema; the Billing service owns its schema. This is Y-axis scaling applied to data. The trade-off: cross-service queries require API calls, not JOINs.Divida um banco grande em bancos menores, cada um pertencendo a um serviço diferente (alinhado com Contextos Delimitados do DDD). O serviço de Pedidos possui seu schema; o serviço de Faturamento possui o seu. Este é o escalonamento no eixo Y aplicado aos dados. O trade-off: consultas entre serviços exigem chamadas de API, não JOINs.
3. Horizontal Sharding3. Sharding Horizontal
Distribute data across multiple database nodes by a shard key. All rows for a given customer ID always go to the same shard. Enables linear write scaling — but adds significant complexity: cross-shard queries are expensive, migrations are painful, and choosing the wrong shard key creates hotspots.Distribua os dados entre múltiplos nós de banco por uma chave de shard. Todas as linhas de um determinado ID de cliente sempre vão para o mesmo shard. Permite escalonamento linear de escrita — mas adiciona complexidade significativa: consultas entre shards são caras, migrations são dolorosas e escolher a chave de shard errada cria hotspots.
Sequence: Before sharding, try read replicas + caching (Redis/Memcached) + query optimization. Sharding should be the last resort — it solves a write-throughput problem that most systems never reach. The teams that shard prematurely spend engineering cycles fighting the complexity instead of shipping features.Sequência: Antes do sharding, tente réplicas de leitura + cache (Redis/Memcached) + otimização de queries. Sharding deve ser o último recurso — resolve um problema de throughput de escrita que a maioria dos sistemas nunca atinge. Times que fazem sharding prematuramente gastam ciclos de engenharia lutando contra a complexidade em vez de entregar features.
Caching: The First Scalability LeverCache: A Primeira Alavanca de Escalabilidade
Before reaching for read replicas or sharding, apply caching. A cache hit costs microseconds; a database query costs milliseconds. At scale, that difference is what separates a system that handles peak load from one that collapses under it.Antes de recorrer a réplicas de leitura ou sharding, aplique cache. Um cache hit custa microssegundos; uma query no banco custa milissegundos. Em escala, essa diferença é o que separa um sistema que aguenta o pico de carga de um que colapsa sob ele.
Cache-Aside (Lazy Loading)Cache-Aside (Lazy Loading)
The application checks the cache first. On a miss, it fetches from the database, writes the result to the cache, and returns it. Only data that is actually requested gets cached — cold data never consumes cache memory.A aplicação verifica o cache primeiro. Em um miss, busca no banco de dados, escreve o resultado no cache e retorna. Apenas os dados que são realmente solicitados são armazenados — dados frios nunca consomem memória de cache.
# Cache-aside pattern with Redis
def get_order(order_id: str) -> Order:
cache_key = f"order:{order_id}"
cached = redis.get(cache_key)
if cached:
return Order.parse(cached) # cache hit — microseconds
order = db.orders.find_by_id(order_id) # cache miss — query DB
redis.setex(cache_key, ttl=300, value=order.to_json()) # cache 5 min
return orderWrite-ThroughWrite-Through
On every write, update both the cache and the database synchronously. Reads are always fast — no cache misses after the first write. Best for data that is read far more often than it is written (product catalog, user profiles, configuration). The trade-off: every write pays double latency.A cada escrita, atualize o cache e o banco de dados de forma síncrona. Leituras são sempre rápidas — sem cache misses após a primeira escrita. Melhor para dados lidos muito mais frequentemente do que escritos (catálogo de produtos, perfis de usuário, configuração). O trade-off: cada escrita paga latência dupla.
Cache Invalidation: The Hard PartInvalidação de Cache: A Parte Difícil
Cache invalidation is one of the two famously hard problems in computer science. Three practical strategies:Invalidação de cache é um dos dois problemas famosamente difíceis em ciência da computação. Três estratégias práticas:
- TTL: Expire the entry after N seconds. Simple, but risks serving stale data up to the TTL window. Choose TTL based on how much staleness the business can tolerate.TTL: Expire a entrada após N segundos. Simples, mas arrisca servir dados desatualizados até a janela do TTL. Escolha o TTL com base em quanto desatualização o negócio pode tolerar.
- Event-driven invalidation: Publish a cache-invalidation event when data changes. Consumers delete the relevant entry immediately. More consistent, but adds event infrastructure.Invalidação orientada a eventos: Publique um evento de invalidação de cache quando os dados mudam. Os consumidores deletam a entrada relevante imediatamente. Mais consistente, mas adiciona infraestrutura de eventos.
- Write-invalidate: On write, delete the cache entry (don't update it). The next read repopulates from the database. Simpler than write-through and avoids stale reads, at the cost of one cache miss per write cycle.Write-invalidate: Na escrita, delete a entrada do cache (não atualize). A próxima leitura reabastece do banco. Mais simples que write-through e evita leituras obsoletas, ao custo de um cache miss por ciclo de escrita.
Anti-pattern: Cache stampede. When a popular entry expires, thousands of requests hit the database simultaneously to repopulate it. Solution: add a short random jitter to TTLs so entries don't expire in sync, or recompute the entry on a background thread slightly before expiry while still serving the old value.Anti-padrão: Cache stampede. Quando uma entrada popular expira, milhares de requisições acessam o banco simultaneamente para reabastecê-la. Solução: adicione um jitter aleatório curto aos TTLs para que entradas não expirem em sincronia, ou recalcule a entrada numa thread de background ligeiramente antes da expiração enquanto ainda serve o valor antigo.
Observability: The Invisible PillarObservabilidade: O Pilar Invisível
A system you cannot observe is a system you cannot operate. The three pillars of observability are:Um sistema que você não pode observar é um sistema que você não pode operar. Os três pilares da observabilidade são:
- Metrics: Quantitative signals (latency percentiles, error rates, throughput). Use RED method — Rate, Errors, Duration — per service.Métricas: Sinais quantitativos (percentis de latência, taxas de erro, throughput). Use o método RED — Rate, Errors, Duration — por serviço.
- Logs: Structured JSON logs with correlation IDs. Every request across service boundaries must carry the same trace ID.Logs: Logs JSON estruturados com IDs de correlação. Toda requisição entre fronteiras de serviço deve carregar o mesmo trace ID.
- Traces: Distributed tracing (OpenTelemetry + Jaeger/Zipkin) to visualize the full request path across services and pinpoint latency sources.Traces: Rastreamento distribuído (OpenTelemetry + Jaeger/Zipkin) para visualizar o caminho completo da requisição entre serviços e identificar fontes de latência.
Anti-pattern: Treating logs as the only observability tool. Logs alone cannot help you understand which downstream call caused a p99 latency spike in a system with 15 services.Anti-padrão: Tratar logs como a única ferramenta de observabilidade. Logs sozinhos não conseguem ajudá-lo a entender qual chamada downstream causou um pico de latência p99 em um sistema com 15 serviços.
Key TakeawaysConclusões Principais
- Match your architecture to your team and traffic patterns — premature microservices are a liability.Alinhe sua arquitetura ao seu time e aos padrões de tráfego — microsserviços prematuros são um passivo.
- Use DDD Bounded Contexts to find service boundaries; let data ownership guide decomposition.Use Contextos Delimitados do DDD para encontrar fronteiras de serviço; deixe a propriedade dos dados guiar a decomposição.
- CQRS lets read and write sides evolve and scale independently — apply it where query patterns diverge from write patterns.CQRS permite que os lados de leitura e escrita evoluam e escalem independentemente — aplique-o onde os padrões de consulta divergem dos padrões de escrita.
- Event-driven communication removes temporal coupling but requires idempotent consumers.A comunicação orientada a eventos remove o acoplamento temporal, mas requer consumidores idempotentes.
- Apply caching (cache-aside with Redis) before read replicas or sharding — a cache hit is orders of magnitude cheaper than any database query.Aplique cache (cache-aside com Redis) antes de réplicas de leitura ou sharding — um cache hit é ordens de magnitude mais barato do que qualquer query no banco.
- Scale databases progressively: read replicas before sharding. Sharding adds complexity that kills velocity — defer it until write throughput actually demands it.Escale bancos progressivamente: réplicas de leitura antes do sharding. Sharding adiciona complexidade que mata a velocidade — adie até que o throughput de escrita realmente exija.
- Instrument from day one: metrics, structured logs, and distributed traces are not optional in production.Instrumente desde o primeiro dia: métricas, logs estruturados e traces distribuídos não são opcionais em produção.