O Gigante Mais Elegante Da Cidade - O Gigante Mais Elegante da Cidade PDF Julia Donaldson
O Gigante Mais Elegante da Cidade PDF Julia Donaldson

Arquitetura distribuída e o problema do monólito

O gigante mais elegante da cidade não é mágica. É basicamente um padrão de deploy para quando seu código cresce demais e você percebe que manter tudo num único processo é uma má ideia. Começa tudo bem. Você escreve uma aplicação, ela roda num servidor, o banco de dados acompanha. Dois anos depois, esse mesmo servidor engole 40 cores de CPU, o deploy leva vinte minutos, e qualquer mudança no módulo de pagamentos derruba o módulo de recomendações. Aí você pensa em microsserviços. A maioria das pessoas vai ler um artigo e tentar partitionar o banco, colocar RabbitMQ em todo lugar, e criar quinze containers que ninguém consegue debugar. Isso não resolve nada. Apenas distribui o caos. A abordagem real é mais lenta no início e muito mais simples no final. Primeiro, você mapeia as dependências de fato, não as que você acha que existem. Roda um script de tracing nos processos ativos por 48 horas. Anota quais módulos se chamam entre si com frequência. Os que têm acoplamento baixo podem sair sozinhos. Os que têm acoplamento alto ficam juntos. O resultado costuma ser quatro ou cinco domínios, não quinze. Cada domínio vira um serviço independente, mas eles compartilham o mesmo banco ainda, pelo menos até você ter confiança de que a separação funciona. Sim, isso viola a doutrina pura de microsserviço. Funciona melhor na prática.

o gigante mais elegante da cidade na prática

Depois de definir os domínios, você escolhe a ferramenta de orquestração. Docker Compose funciona até certa escala. Quando você passa de três serviços rodando em dois nós, precisa de algo que gerencie health checks, retry logic e secret management sem você escrever bash scripts manuais. Kubernetes é overkill para a maioria das startups. Eu usei Nomad no começo, migrei para ECS na AWS, e agora uso Kubernetes só porque o meu time cresceu e já tem SRE dedicado. Se você está sozinho ou com menos de dez pessoas de engenharia, fique com ECS ou até mesmo com máquinas virtuais antigas e supervisord. A complexidade que você economiza vale mais do que a flexibilidade que ganha. O deploy em si segue um pipeline simples: CI pega o código, roda testes unitários, faz build da imagem, envia para o registry, atualiza o manifest no cluster, e o controlador faz rolling update. Nada revolucionário. O que a maioria erra é a parte dos testes de integração. Testes unitários isolados mentem pra você. Eles passam porque o mock não reflete o comportamento real do serviço chamado. Você precisa de um staging que rode todos os serviços juntos, com dados reais, antes de qualquer deploy ir pra produção. Eu implementei isso com Docker Compose em rede isolada e bancos de dados efêmeros criados via migration scripts. O custo de setup inicial era de cerca de duas semanas. O ganho foi evitar três incidentes graves no primeiro mês.

Um problema específico que eu enfrentei foi com a propagação de timeouts entre serviços síncronos. Quando o serviço A chama o serviço B e B demora, o timeout padrão de 5 segundos do HTTP client gera filas acumuladas no A. Se o serviço A tiver cinco threads ativas e cada uma esperar 5 segundos, você trava tudo rapidamente. A solução que funcionou foi implementar circuit breakers com fallbacks granulares. Não o tipo genérico que desliga tudo, mas um que identifica qual operação específica falhou e retorna um valor default ou cache. Usei a biblioteca Resilience4j no lado Java e um padrão semelhante em Go com context timeouts e retry exponencial com jitter. O resultado foi uma queda de 70% nos erros em cascata durante picos de tráfego. O maior pitfall que vejo gente cometendo é a obsessão por consistência forte entre serviços. Eles colocam transações distribuídas em tudo, tentam implementar Two-Phase Commit, e acabam com latência inaceitável. A verdade prática é que a maioria dos sistemas tolera eventualidade. Use sagas em vez de transações. Se o serviço de pagamento falha depois de confirmar o estoque, você compensa com uma operação de cancelamento, não com um rollback global. Isso exige modelagem cuidadosa dos fluxos, mas escala muito melhor. Eu perdi duas semanas refactorizando um sistema de pagamentos que usava lock pessimista entre três serviços. Troquei por sagas com events e o throughput triplicou.

👉 Clique no botão abaixo para saber mais sobre o assunto!

Há também a questão da observabilidade. Sem logging estruturado, métricas padronizadas e tracing distribuído, você vira explorador cego. Eu configurei Prometheus pra métricas, Loki pra logs, e Jaeger pro tracing. A configuração inicial foi confusa, mas seguindo o template padrão da Cloud Native Computing Foundation, consegui levantar tudo em menos de três dias. O pulo do gato foi criar dashboards unificados no Grafana que mostravam latência, erro rate e saturação lado a lado. Quando o serviço de notificação começou a gerar latência alta, eu consegui identificar em dois minutos que o gargalo era o broker de mensagens, não o código do serviço em si. Sem essa visibilidade, levaria horas de guesswork. A escalabilidade horizontal vem naturalmente quando você para de depender de estado compartilhado. Cada serviço deve ser stateless na medida do possível. Sessões de usuário vão pra Redis ou para cookies assinados. uploads de arquivo vão pra S3 ou equivalente. Dados transacionais ficam no banco próprio do serviço. Quando tudo isso está separado, você escala cada peça independentemente. O serviço de notificação pode ter dez réplicas e o de recomendação duas. Isso é vantajoso economicamente e operacionalmente.

O Giant Elémegante da Cidade não é uma solução para todos os problemas. Se o seu sistema tem menos de dez mil requisições por dia, microsserviços são overhead desnecessário. Monolito modular com boundaries bem definidos funciona perfeitamente. A regra prática que eu sigo é: só partitione quando o custo de manutenção do monolito superar claramente o custo operacional dos serviços. Isso geralmente acontece quando o time de desenvolvimento cresce acima de oito pessoas, ou quando a frequência de deploy precisa ser diária, não semanal. Antes disso, a simplicidade vence. Outra limitação séria é a complexidade de desenvolvimento local. Rodar todos os serviços localmente exige muita máquina e configuração. Eu resolvi isso com um script de bootstrap que sobe apenas os serviços relevantes pra cada tarefa, usando perfis de configuração específicos. Desenvolvedores não precisam rodar tudo. Só o que precisam testar. Isso reduziu o tempo médio de setup de ambiente local de uma hora pra quinze minutos.

A escolha de tecnologia entre os serviços também importa menos do que as pessoas pensam. Homogeneidade facilita operações, mas heterogeneidade permite escolher a ferramenta certa pro trabalho certo. Meu time preferiu ficar com Java e Go por simplicidade operacional. Dois linguagens, poucos frameworks diferentes, padrões de logging idênticos. O trade-off foi aceito sem resistência significativa. O monitoramento de custos operacionais merece atenção separada. Serviços mal dimensionados consomem recursos ociosos. Configure HPA com base em métricas reais, não em gut feeling. O CPU usage sozinho é insuficiente. Inclua request rate e latência p99. Eu vi times configurarem auto-scaling baseado apenas em CPU e o sistema escalar pra cima quando não precisava, eescalar pra baixo tarde demais quando precisava. Ajuste fino leva tempo, mas compensa.

No fim das contas, o que separa uma implementação bem-sucedida de um desastre é a disciplina de começar pequeno, validar cada boundary antes de partitionar, e não adiar a implementação de observabilidade pra depois. Muitas equipes adiam observabilidade achando que vão implementar depois. A realidade é que implementam nunca. Comece com logs estruturados e métricas básicas desde o dia um. O esforço adicional é mínimo, e o retorno em debugging e confiança é enorme. Se você está considerando dar esse passo, comece com um sistema de exemplo, partitione apenas dois serviços, e valide o pipeline completo antes de aplicar ao resto. A curva de aprendizado é íngreme nos primeiros trinta dias, mas depois disso o ritmo acelera significativamente. A produtividade do time tende a melhorar em cerca de quatro a seis semanas após a primeira partition bem-sucedida, desde que a equipe tenha maturidade operacional mínima.