Trilha 4 · Escala e evolução · ~30 min · pleno/sênior
Voltei a uma empresa dois anos depois de tê-los ajudado a não migrar para microserviços (a história da aula Quando (e quando NÃO) migrar para microserviços). Eles seguiram o conselho: monolito modular, fronteiras via Public API, estado isolado. E cresceram — muito. Agora tinham um problema bom: o monolito modular estava impecável por dentro, mas era um único deploy. Uma mudança de uma linha no catálogo exigia rebuildar e redeployar a aplicação inteira, incluindo o processador de vídeo e o painel admin. O build levava 25 minutos. E o processador de vídeo, que precisava de máquinas potentes e escalava sozinho, estava amarrado ao mesmo artefato do catálogo, que rodava em máquinas modestas.
Eles estavam presos numa falsa dicotomia que domina a indústria: "ou você é um monolito (um deploy, simples, mas acoplado no deploy) ou você é microserviços (N deploys, flexível, mas complexo)". A verdadeira resposta estava numa distinção que quase ninguém faz explícita: limite lógico e limite de deploy são coisas diferentes. Você pode ter fronteiras lógicas nítidas (módulos) e escolher, separadamente, como agrupá-las em artefatos de deploy — e mudar essa escolha sem reescrever nada. A ferramenta que torna isso prático é o monorepo com Nx. Esta aula final fecha o curso mostrando como colher deploy seletivo e escala independente sem pagar o custo dos microserviços — porque tudo o que construímos até aqui já preparou o terreno.
A distinção que muda tudo: lógico ≠ deploy
Durante 15 aulas, falamos de fronteiras lógicas: billing, content, identity como módulos com contratos, estado isolado e baixo acoplamento. Nunca dissemos como esses módulos viram processos rodando. Essa é uma decisão separada, e confundir as duas é a raiz da falsa dicotomia monolito-vs-microserviço.
- Limite lógico (o que construímos): onde termina a responsabilidade de um módulo, o que ele expõe, quem é dono de qual estado. É sobre organização do código e do modelo.
- Limite de deploy: quais módulos são empacotados juntos num artefato executável e sobem juntos. É sobre operação e runtime.
O insight liberador: um limite lógico não obriga a um limite de deploy. Você pode ter três módulos lógicos (identity, billing, content) e deployá-los como:
- 1 artefato (monolito modular): os três juntos num processo. Simples de operar.
- 3 artefatos (microserviços): cada um seu processo. Máxima autonomia, máximo custo.
- 2 artefatos (híbrido):
identity + billingjuntos,content(com o pesadovideo-processor) separado. O meio-termo que a empresa da abertura precisava.
E — o ponto crucial — você escolhe, e muda essa escolha, sem tocar no código dos módulos, porque as fronteiras lógicas já são limpas (Public API, estado isolado). O limite de deploy vira um arquivo de configuração, não uma reescrita. Microserviços tratam lógico e deploy como a mesma decisão (cada módulo lógico = um deploy, obrigatoriamente); o monorepo modular os desacopla, deixando você mover o limite de deploy conforme a evidência (aula Por que não existe arquitetura perfeita), independentemente das fronteiras lógicas.
A tese da aula: decida as fronteiras lógicas cedo e com cuidado (foi o curso todo). Decida as fronteiras de deploy tarde, por evidência, e mude-as quando a evidência mudar. Uma é arquitetura; a outra é topologia — e topologia deve ser barata de alterar.
"Voltar" de microserviços não é retrocesso
A indústria viveu um ciclo: euforia de microserviços (2015-2020) → ressaca (empresas percebendo o custo operacional) → correção ("monolito modular" / "modular monolith" / o movimento que a Amazon Prime Video tornou famoso ao reconsolidar um sistema e cortar custo drasticamente). Isso confunde quem vê de fora: "então microserviços eram errados?".
Não. O que estava errado era tratar deploy distribuído como default em vez de como ferramenta para um problema específico (autonomia de time em escala — aula Quando (e quando NÃO) migrar para microserviços). "Voltar" para um monolito modular não é abrir mão de fronteiras — as fronteiras lógicas continuam lá, nítidas. É apenas reagrupar os limites de deploy de N artefatos para 1 (ou poucos), colhendo simplicidade operacional sem perder a modularidade. A empresa da abertura fez o inverso da ida: partiu de 1 artefato e foi para 2, movendo o limite de deploy na direção da fragmentação, mas só onde a evidência (build lento, escala díspar do vídeo) justificou.
Ambos os movimentos — consolidar e fragmentar — são a mesma capacidade: mover o limite de deploy livremente porque o limite lógico é sólido. É isso que o monorepo modular entrega e que nem o "monolito bagunçado" (não dá para separar) nem os "microserviços dogmáticos" (não dá para juntar sem reescrever contratos de rede) oferecem. Você fica com a liberdade dos dois sem a prisão de nenhum.
Estruturando o FakeFlix com Nx: apps/ e libs/
Nx é uma ferramenta de monorepo que dá exatamente essa separação. A convenção central:
libs/— onde vivem os módulos lógicos (a arquitetura). Cada módulo do FakeFlix vira uma lib: código, contratos, testes. Libs não deployam sozinhas; são consumidas por apps.apps/— os artefatos de deploy (a topologia). Cada app é um executável que compõe libs. Um app é fino: ele só monta as libs que precisa e sobe.
fakeflix/
├── apps/
│ ├── monolith/ # deploy: TODAS as libs juntas (dev, produção pequena)
│ ├── content-streaming/ # deploy: identity + content (só o que serve streaming)
│ ├── video-processor/ # deploy: só content/video-processor (workers pesados)
│ └── admin-panel/ # deploy: identity + content/admin
│
└── libs/ # os MÓDULOS lógicos — a arquitetura do curso inteiro
├── identity/
├── billing/
├── content/
│ ├── catalog/
│ ├── admin/
│ └── video-processor/
└── shared/ # mecanismo reutilizável (persistência, logging — aulas *Persistência e State Isolation* e *Preparação para produção: testes com factories, logging e JWT entre módulos*)
Repare que este é o mesmo desenho lógico do FakeFlix da aula DDD estratégico + Arquitetura Hexagonal no FakeFlix — os módulos não mudaram. O que Nx adiciona é a camada apps/ que compõe esses módulos em topologias diferentes. Um app é trivial:
// apps/content-streaming/src/main.ts — compõe SÓ as libs necessárias
@Module({
imports: [
IdentityModule, // de libs/identity
ContentModule, // de libs/content
// billing NÃO entra: este app só serve streaming; entitlement vem por projeção (aulas *Persistência e State Isolation* e *Event-Driven na prática: Transactional Outbox, idempotência e sagas*)
],
})
class ContentStreamingApp {}
// apps/video-processor/src/main.ts — só os workers pesados
@Module({
imports: [VideoProcessorModule], // de libs/content/video-processor — escala sozinho (aula *Subdomínios e comunicação assíncrona (BullMQ): mensagens vs. eventos*)
})
class VideoProcessorApp {}
O mesmo ContentModule (uma lib) participa de vários apps sem mudar — isso é Composability (aula Os 10 princípios da arquitetura modular, Princípio 4) tornado concreto. E porque as libs respeitam Public API e estado isolado, compor um subconjunto delas num app é seguro: nenhuma lib depende do interno de outra. As fitness functions (aula Governança automatizada: fitness functions com ESLint + Husky) do Nx inclusive impõem o grafo de dependências permitido entre libs:
// nx impõe quais libs podem depender de quais (fitness function de dependência — aula *Governança automatizada: fitness functions com ESLint + Husky*)
{
"depConstraints": [
{ "sourceTag": "scope:content", "onlyDependOnLibsWithTags": ["scope:content", "scope:shared", "type:public-api"] },
{ "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:feature", "type:public-api"] }
]
}
Isto é a aula Governança automatizada: fitness functions com ESLint + Husky subindo de nível: em vez de regular imports entre pastas, o Nx regula dependências entre libs no grafo do monorepo — a governança arquitetural do curso, aplicada à topologia.
Build afetado e deploy seletivo: a velocidade sem a fragmentação
Resolvida a estrutura, o problema da abertura (build de 25 min, deploy tudo-ou-nada) some com dois recursos do Nx:
Build/test afetado (nx affected): o Nx conhece o grafo de dependências entre libs e apps. Quando você muda uma linha no catalog, ele calcula exatamente o que foi afetado por essa mudança e rebuilda/testa só isso — não o monorepo inteiro.
# ✅ builda/testa APENAS o que a mudança afetou, não tudo
nx affected:build # se mudei catalog, builda só as libs/apps que dependem de catalog
nx affected:test # roda só os testes impactados
Mudou o catalog? O Nx rebuilda libs/content/catalog e os apps que a usam (content-streaming, admin-panel, monolith) — mas não toca em libs/billing nem no video-processor (que não dependem de catalog). O build de 25 minutos vira 2, porque 90% do trabalho era recompilar o que não mudou. Em CI, isso é transformador: PRs validam em minutos, não em meia hora.
Deploy seletivo: como cada app é um artefato independente, você deploya só o que mudou e escala cada um por sua necessidade.
# ✅ deploya só o app afetado pela mudança
nx run content-streaming:deploy # subiu uma feature de streaming? só este sobe
# ✅ escala cada artefato conforme SUA demanda (Individual Scale — aula *Os 10 princípios da arquitetura modular*, P5)
nx run video-processor:deploy # e roda 12 réplicas dele, sem tocar no catálogo
O video-processor roda em máquinas potentes com muitas réplicas; o content-streaming em máquinas modestas; o admin-panel numa réplica. Cada um escala e deploya independentemente — o benefício que a empresa da abertura queria dos microserviços — mas compartilhando o mesmo repositório, os mesmos contratos tipados (sem versionar API de rede entre eles quando estão no mesmo app), o mesmo pipeline. Você pegou a autonomia de deploy sem a explosão de complexidade operacional.
O que você colheu e o que economizou: colheu deploy seletivo, escala individual e build rápido (os benefícios reais que empurram times para microserviços). Economizou: versionamento de contratos de rede entre todos os pares, N repositórios e pipelines, a tentação de fragmentar além da conta. O monorepo modular é o "híbrido" da aula Quando (e quando NÃO) migrar para microserviços tornado operacional.
Quantos artefatos? A linha que se move
A pergunta final do curso: quantos apps você cria? A resposta é a mesma de sempre (aula Por que não existe arquitetura perfeita): o mínimo que resolve sua dor atual, movendo a linha com evidência.
- Começo: 1 app (
monolith) compondo todas as libs. Simples de operar, e as libs já estão modulares por dentro. Este é o default saudável. - Primeira evidência (build lento / escala díspar): extraia o app que dói. A empresa da abertura extraiu
video-processor(escala e build) — foi de 1 para 2 apps, sem tocar nas libs. - Evidência organizacional (times autônomos — aula Quando (e quando NÃO) migrar para microserviços): extraia o app do time que precisa deployar sozinho. Cada extração é adicionar um
app/que compõe libs existentes. - Correção (a ressaca): se você fragmentou demais e a operação doeu, reconsolide — junte apps de volta, também sem tocar nas libs.
O ponto que amarra o curso inteiro: porque as fronteiras lógicas foram feitas certo (aulas DDD estratégico + Arquitetura Hexagonal no FakeFlix, Comunicação entre módulos: o padrão Public API Provider e Persistência e State Isolation), a topologia de deploy é uma decisão barata e reversível. Você move a linha entre "1 app" e "N apps" ao longo da vida do sistema, nos dois sentidos, respondendo à evidência — sem jamais reescrever um módulo. Isso é arquitetura evolutiva no sentido pleno: não é "escolher a arquitetura certa uma vez", é manter todas as opções abertas e mover-se entre elas com baixo custo. Foi para isso que o curso inteiro trabalhou.
Antes → Depois
| Antes | Depois |
|---|---|
| Limite lógico = limite de deploy (falsa dicotomia) | Lógico e deploy desacoplados |
| Build de 25 min do monolito inteiro | nx affected: builda só o que mudou |
| Deploy tudo-ou-nada | Deploy seletivo por app |
| Vídeo (pesado) preso ao catálogo (leve) no mesmo artefato | Cada app escala por sua demanda |
| "Monolito OU microserviços" | Mover a linha de deploy livremente, nos dois sentidos |
| Fronteiras num repo, contratos de rede versionados | Fronteiras lógicas + composição de libs, sem rede entre libs do mesmo app |
Tradeoffs e armadilhas
- Monorepo tem custo de tooling. Nx, o grafo, o cache distribuído, a configuração de tags/constraints — é uma curva de aprendizado e uma infraestrutura de build a manter. Para um projeto pequeno de um app, é overkill (aula Por que não existe arquitetura perfeita): o
monolithsimples sem Nx basta até a dor de build/escala aparecer. - Monorepo ≠ ausência de disciplina. Ter tudo no mesmo repo facilita furar fronteiras (o interno de outra lib está ali, a um import de distância). Sem as fitness functions de dependência do Nx (
depConstraints), o monorepo vira o monolito espaguete da aula Comunicação entre módulos: o padrão Public API Provider com passos extras. A governança (aula Governança automatizada: fitness functions com ESLint + Husky) é ainda mais necessária aqui, não menos. - Apps que compartilham processo não têm isolamento de falha. Duas libs no mesmo app compartilham memória e destino: se uma vaza memória, derruba o app inteiro. Microserviços dão isolamento de falha real (um serviço cai, os outros seguem). Se isolamento de falha é um requisito duro (não só escala/deploy), aí a separação física de processo — e o custo dela — se justifica. O monorepo modular dá deploy e escala independentes, mas libs no mesmo app ainda compartilham o destino do processo.
nx affecteddepende de um grafo correto. Dependências implícitas (via banco compartilhado — o anti-State-Isolation da aula Persistência e State Isolation, ou via variável global) enganam o cálculo de "afetado", e o Nx pode não rebuildar algo que deveria. O afetado só é confiável se as dependências forem explícitas — mais um motivo para as fronteiras limpas do curso.- A tentação de criar apps demais. Ter
apps/barato pode levar a fragmentar por reflexo ("um app por módulo, fica organizado"). Isso recria a complexidade operacional dos microserviços sem a evidência que a justifica. A linha de deploy se move por dor medida, não por estética.
Cola mental
- Limite lógico (fronteira de módulo, o curso todo) ≠ limite de deploy (artefato que sobe). Microserviços os fundem; monorepo modular os desacopla.
- Decida o lógico cedo e com cuidado; decida o deploy tarde e por evidência, e mude-o nos dois sentidos sem reescrever módulos.
- Nx:
libs/= módulos lógicos (arquitetura),apps/= artefatos de deploy (topologia) que compõem libs. MesmoContentModuleem vários apps = Composability (aula Os 10 princípios da arquitetura modular). nx affectedbuilda/testa só o impactado (build de 25min → 2min). Deploy seletivo por app + escala individual (aula Subdomínios e comunicação assíncrona (BullMQ): mensagens vs. eventos (princípio 5)) sem fragmentar em N repos/pipelines.depConstraints= fitness function (aula Governança automatizada: fitness functions com ESLint + Husky) no grafo do monorepo — governança ainda mais necessária num monorepo.- "Voltar" de microserviços = reagrupar limites de deploy, não abrir mão de fronteiras. Consolidar e fragmentar são a mesma capacidade: mover a linha porque o lógico é sólido.
- Custo: tooling, disciplina (fronteiras furam fácil no mesmo repo), sem isolamento de falha entre libs do mesmo app. Justifica-se por evidência (build/escala/organização), não por moda.
Referências para Aprofundamento
Monorepo e limites
- Nx — documentação — A ferramenta da aula. As páginas sobre project graph e affected são as que sustentam o build seletivo.
- Nx — Enforce Module Boundaries — Tags por biblioteca e regras de quem pode depender de quem: é a fitness function de fronteira integrada ao monorepo.
- Why Google Stores Billions of Lines of Code in a Single Repository — O artigo da ACM sobre o monorepo do Google, com os ganhos e os custos reais em escala.
- Turborepo — A alternativa mais leve ao Nx, útil para comparar o modelo de cache e de tarefas antes de escolher.
A decisão de quantos artefatos
- MonolithFirst — Martin Fowler e MicroservicePremium — Martin Fowler — A base do argumento de que a linha entre lógico e físico se move nos dois sentidos.
- Modular Monolith: A Primer — Kamil Grzybek — O desenho completo de um sistema com fronteira lógica forte e deploy único.
Exercícios
Exercício 1 — Separe os conceitos
Explique, com o FakeFlix, a diferença entre um limite lógico e um limite de deploy. Dê um exemplo de três módulos lógicos deployados como 1, como 2 e como 3 artefatos, e diga o que muda no código dos módulos em cada caso.
Ver resposta
✓ Resposta: Limite lógico é onde termina a responsabilidade de um módulo e o que ele expõe (ex.: billing é dono do estado de assinatura e expõe BillingSubscriptionStatusApi); é organização de código/modelo. Limite de deploy é quais módulos são empacotados e sobem juntos num artefato; é runtime/operação. Três módulos (identity, billing, content) como: 1 artefato = um app monolith importando os três (um processo); 2 artefatos = identity+billing num app e content noutro (dois processos); 3 artefatos = um app por módulo (três processos, ≈ microserviços). O que muda no código dos módulos em cada caso: nada. Os módulos (libs) permanecem idênticos — mesmos contratos, mesmo estado isolado; o que muda é só a camada apps/ que os compõe e a config de deploy. É essa invariância do código dos módulos que torna a topologia barata e reversível.
Exercício 2 — Estruture em Nx
Você tem os módulos identity, billing, content (com subdomínios catalog, admin, video-processor) e shared. Proponha uma estrutura apps/ + libs/ para: (a) desenvolvimento local (tudo junto); (b) um deploy que serve só o streaming ao usuário final; (c) um deploy dos workers de processamento de vídeo. Diga quais libs cada app compõe.
Ver resposta
✓ Resposta: Estrutura:
libs/
identity/ billing/
content/{catalog, admin, video-processor}/
shared/
apps/
monolith/ (a) → compõe: identity, billing, content(catalog+admin+video-processor), shared
content-streaming/ (b) → compõe: identity, content/catalog, shared (billing entra por projeção/entitlement, não como lib pesada)
video-processor/ (c) → compõe: content/video-processor, shared
(a) monolith importa todas as libs — um processo, ideal para dev/produção pequena. (b) content-streaming compõe só identity (auth) + content/catalog (o que o usuário navega/assiste) + shared; não inclui admin (não é do usuário final) nem os workers; o status de assinatura vem de uma projeção de entitlement (aulas Persistência e State Isolation e Event-Driven na prática: Transactional Outbox, idempotência e sagas) para não arrastar billing inteiro. (c) video-processor compõe só content/video-processor + shared — os workers pesados, que escalam sozinhos (aula Subdomínios e comunicação assíncrona (BullMQ): mensagens vs. eventos).
Exercício 3 — nx affected
Um dev muda uma linha em libs/content/catalog. Sabendo que content-streaming e admin-panel dependem de content, mas video-processor e billing não, o que nx affected:build rebuilda e o que ele pula? Por que isso derruba o build de 25 minutos?
Ver resposta
✓ Resposta: nx affected:build rebuilda libs/content/catalog (mudou) e tudo que depende dela: as libs/apps content-streaming e admin-panel (dependem de content), e o monolith (contém tudo). Ele pula libs/billing, o app video-processor (depende só de content/video-processor, não de catalog) e qualquer coisa que não esteja no caminho de dependência de catalog. Isso derruba o build de 25 min porque, no build tradicional, todo o código é recompilado a cada mudança, mesmo o que não foi afetado (billing, os workers, etc.); o Nx usa o grafo de dependências para recompilar só o subconjunto realmente impactado, e como a maioria das mudanças toca uma fração pequena do grafo, o tempo cai proporcionalmente. (Ressalva: o cálculo só é confiável se as dependências forem explícitas — dependências implícitas via banco compartilhado enganariam o "afetado", mais um motivo para o State Isolation da aula Persistência e State Isolation.)
Exercício 4 — "Voltar" é retrocesso?
Uma empresa migrou de 30 microserviços para 3 apps num monorepo Nx e cortou custo de infra pela metade. Um crítico diz "eles abandonaram a arquitetura de microserviços, é um retrocesso". Responda usando a distinção lógico/deploy: o que exatamente eles mudaram, o que não mudaram, e por que não é retrocesso.
Ver resposta
✓ Resposta: Eles mudaram o limite de deploy: de 30 artefatos que sobem/escalam separadamente para 3, eliminando 27 pipelines, a rede entre a maioria dos pares de serviços, o versionamento de contratos de rede entre eles e a sobrecarga operacional — daí o corte de custo. Eles não mudaram o limite lógico: os 30 módulos lógicos (fronteiras, contratos, estado isolado) continuam existindo como libs no monorepo, cada um com sua responsabilidade; ninguém fundiu billing com content. Por que não é retrocesso: microserviços nunca foram a arquitetura — eram uma topologia de deploy. A arquitetura (modularidade, fronteiras) permaneceu intacta; o que foi corrigido foi a topologia excessivamente fragmentada, que impunha custo operacional sem benefício proporcional (o erro da aula Quando (e quando NÃO) migrar para microserviços). Reagrupar limites de deploy mantendo limites lógicos é exatamente a capacidade que uma arquitetura evolutiva deve ter — é engenharia madura respondendo a evidência de custo, não abandono de arquitetura. O crítico confunde topologia com arquitetura.
Exercício 5 — Quantos apps?
Sua startup tem 5 devs, um app monolito Nx, e o build começou a levar 15 min porque o video-processor (pesado) recompila junto com tudo, além de precisar de mais máquina que o resto. Você deveria migrar para microserviços? Qual é a menor mudança que resolve, e que evidência futura te faria criar um terceiro app?
Ver resposta
✓ Resposta: Não migre para microserviços — você não tem o problema que eles resolvem (5 devs, um time; sem dor organizacional de deploy — aula Quando (e quando NÃO) migrar para microserviços), e pagaria a complexidade toda por nada. Menor mudança que resolve: extrair um segundo app, video-processor, do monolito Nx, compondo só a lib content/video-processor + shared. Isso resolve as duas dores de uma vez: (1) build — com nx affected, mudanças no resto do sistema não recompilam mais os workers e vice-versa, cortando o tempo; (2) escala — o video-processor vira um artefato que roda em máquinas potentes com suas próprias réplicas, sem arrastar o catálogo. Você foi de 1 para 2 apps sem tocar nas libs — topologia, não arquitetura. Evidência futura para um terceiro app: por exemplo, o painel admin passar a ter um perfil de deploy/segurança próprio (releases em cadência diferente, acesso restrito), ou o time crescer e surgir um grupo dedicado a um domínio que precise deployar sozinho (evidência organizacional — aula Quando (e quando NÃO) migrar para microserviços), ou outro subdomínio começar a dominar o build/escala. Em todos os casos, a regra é a mesma do curso: mova a linha de deploy por dor medida, adicionando um app/ que compõe libs existentes — nunca por estética.