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 + billing juntos, content (com o pesado video-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 monolith simples 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 affected depende 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. Mesmo ContentModule em vários apps = Composability (aula Os 10 princípios da arquitetura modular).
  • nx affected builda/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

A decisão de quantos artefatos

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.