Teu progresso
0 / 83 módulos0%
Estágio 04 · 04-07
Bloqueado"Clean Architecture" virou desculpa pra adicionar 4 camadas de mapeamento entre HTTP e DB pra fazer um CRUD. Times leem Uncle Bob, copiam diagrama de cebola, e entregam codebase com 30% mais código sem maior maintainability. Outros vão pro extremo oposto: tudo no controller, lógica grudada em ORM models, refactor é remendo. Nenhum dos dois é resposta default.
Este módulo é arquitetura de aplicação com sobriedade: hexagonal/ports-adapters, clean architecture, onion, vertical slices, MVC tradicional, transaction script. Quando cada um vence, quando custa mais que entrega. Você sai sabendo decidir baseado em contexto, não em fé.
Do mais simples ao mais elaborado:
Não existe "melhor". Existe ajuste a domínio + time + maturity.
Alistair Cockburn, ~2005. Inverte dependência:
Domain não importa lib HTTP, lib DB. Tudo flui através de ports. Adapters são plug-and-play.
Permite testar domain sem subir HTTP/DB. Trocar DB sem mudar domain (hipótese atraente; raramente acontece em prática).
Camadas concêntricas:
Dependency Rule: deps apontam pra dentro. Inner layer nunca importa outer.
Boilerplate típico: Request DTO → Controller → Use Case → Entity → Repository → DB. Mapping entre cada camada.
Em projetos pequenos: overkill. Em projetos grandes com lógica core complexa: estrutura útil.
Jeffrey Palermo. Quase sinônimo com Clean. Layers:
Mesmo princípio: deps pra dentro.
Diferenças com Clean são detalhes; tratamento equivalente em prática.
Jimmy Bogard (Mediator pattern, MediatR em .NET).
Em vez de horizontal layers, organize por feature:
features/
CreateOrder/
CreateOrderCommand.ts
CreateOrderHandler.ts
CreateOrderValidator.ts
CreateOrderEndpoint.ts
AssignCourier/
...
Cada slice cross-cuts layers; mudança de feature toca 1 pasta.
Pros:
Cons:
CQRS encaixa naturalmente: command slices vs query slices.
Express/Rails MVC clássico. Funciona até controllers ficarem 500 linhas com lógica + validation + DB call + email send + saga.
Refactor incremental:
Não força hexagonal full; só aplique camada onde dor justifica.
order.markPaid() valida invariants e muda state).Rich exige cuidado: entity não deve depender de DB; lógica fica em domain pure.
RoutingService.findBestCourier(order, fleet).AssignCourierUseCase.execute(orderId) busca order, chama domain service, persiste, emite event.Use case = fluxo de aplicação; domain service = lógica de domínio.
Estrutura típica:
src/
domain/
Order.ts # entity com behavior
OrderRepository.ts # PORT (interface)
AssignCourierService.ts # domain service
application/
AssignCourierUseCase.ts # use case
infrastructure/
DrizzleOrderRepository.ts # ADAPTER
HttpController.ts # ADAPTER
KafkaPublisher.ts # ADAPTER
composition.ts # wire-up
DI manual ou via lib (tsyringe, awilix). Em greenfield, injeção manual fácil de seguir.
src/
features/
create-order/
schema.ts
handler.ts
route.ts
assign-courier/
...
shared/
db.ts
auth.ts
Sem layers globais. Cada feature traz seu pacote.
Fastify/Hono encaixam bem; cada slice registra plugin.
Adicione layer quando dor justifica. Don't pre-architect.
DDD strategic é ortogonal a hexagonal: você pode aplicar bounded contexts sem hexagonal interno.
DDD tactical (aggregate, repository) encaixa naturalmente em hexagonal: aggregate é domain entity; repository é port.
Em projeto sério: bounded contexts (DDD strategic) + dentro de cada, hexagonal (DDD tactical) ou simpler dependendo de complexity.
Pattern: cada frontend (web, mobile, partner) tem backend agregador próprio. Cada BFF orquestra microservices internos.
Pros: cliente recebe data shaped pra suas necessidades; evolução independente. Cons: mais services pra operar.
Combinação prática:
Em 2026, modular monolith é "default sane" pra projetos médios. Microservices só quando demanda justifica.
Front também tem patterns:
Em Next/React modernos, mix entre componentização + features-sliced é comum.
Logística:
Padrão arquitetura por contexto.
"Microservices vs monolith" é debate falso em 2026. Resposta correta = "depende do estágio". Logística começa modular monolith (1 deploy, fast iteration), evolui pra serviços extraídos quando dor real aparece (org scale, scale técnico independente), serverless para edges (cron, image processing). Decisão por estágio: v1 (PMF), v2 (scale), v3 (multi-team), v4 (regional). Esta seção entrega decision tree concreto + signals que indicam evoluir.
Stage v1 — modular monolith (PMF, 0-50k MAU, equipe 2-8):
logistica-monorepo/
├── apps/
│ ├── web/ Next.js
│ ├── mobile/ Expo
│ └── api/ Fastify monolith
├── packages/
│ ├── core/ Domain logic (shared)
│ ├── db/ Drizzle schema + queries
│ ├── auth/ Auth flows
│ ├── notifications/ Email/SMS/push
│ └── billing/ Stripe integration
└── infra/
├── docker-compose.yml (dev)
└── railway.json (deploy single VM)
auth.users, orders.orders, billing.invoices).Stage v2 — modular monolith + extracted edges (50k-500k MAU, equipe 8-25):
Stage v3 — service-oriented (500k-5M MAU, equipe 25-100):
Stage v4 — multi-region + edge (5M+ MAU, equipe 100+):
Decision tree — quando extrair serviço do monolito:
Para cada module candidato:
1. Tem traffic profile diferente? (high-volume notifications vs low-volume admin)
→ SIM: candidato.
2. Tem scale axis diferente? (CPU-bound image processing vs I/O-bound API)
→ SIM: candidato.
3. Squad dedicada quer deploy independente?
→ SIM: candidato.
4. Tem dependency externa risky? (3rd-party API down derruba monolith)
→ SIM: isolar pra fault-tolerance.
5. Tem compliance/audit boundary? (PII processing isolado)
→ SIM: extrair pra reduce blast radius.
Se 0-1 sim: NÃO extrair. Custo > benefício.
Se 2+ sim: candidato; evaluate cost (build, deploy, observability, ops).
Signals que indicam estágio errado:
Monolítico estagnado em v1 quando deveria ser v2:
Microservices prematuro em v2 quando deveria ser v1+extracted:
Microservices estagnado em v3 quando deveria ser v4:
Logística — caminho real recomendado:
v1 (Year 1, MVP): Monorepo Next.js + Fastify monolith + Postgres + Railway. 5 devs.
v2 (Year 2, growth): Extract notifications + image processing. Add Redis. Read replica. 12 devs.
v3 (Year 3-4, scale): Identity + Orders + Dispatch + Billing + Analytics services. K8s. Kafka. 35 devs.
v4 (Year 5+, global): Multi-region (US, EU, BR). Edge auth. Serverless ML inference. 80+ devs.
Custo de cada transição (estimativa real):
Anti-patterns observados:
Cruza com 04-07 §2.15 (modular monolith concretizado), 04-08 §2.20 (services-monolith-serverless decisão geral), 04-06 §2.3 (bounded contexts são pré-requisito pra service extraction), 04-12 §2.14 (Conway's Law alinha org com arquitetura), 04-09 §2.x (scale axes informam decisão).
Três arquiteturas, três filosofias, mesmo princípio raiz: Dependency Inversion (high-level modules não dependem de low-level; ambos dependem de abstractions).
Origens:
Hexagonal — anatomia:
// Domain (pure, no I/O)
class Order {
constructor(public id: string, public status: OrderStatus) {}
markDelivered() {
if (this.status !== 'in_transit') throw new InvalidTransition();
this.status = 'delivered';
}
}
// Primary port (driving)
interface OrderUseCase {
markDelivered(orderId: string, courierId: string): Promise<void>;
}
// Secondary ports (driven)
interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
interface NotificationPort {
notifyDelivered(order: Order): Promise<void>;
}
// Application layer (orchestrates domain via ports)
class OrderApplicationService implements OrderUseCase {
constructor(private repo: OrderRepository, private notifier: NotificationPort) {}
async markDelivered(orderId: string, courierId: string) {
const order = await this.repo.findById(orderId);
if (!order) throw new OrderNotFound(orderId);
order.markDelivered();
await this.repo.save(order);
await this.notifier.notifyDelivered(order);
}
}
// Primary adapter (REST controller)
app.post('/orders/:id/delivered', async (req, res) => {
await orderUseCase.markDelivered(req.params.id, req.user.courierId);
res.json({ ok: true });
});
// Secondary adapter (Postgres repo)
class PostgresOrderRepository implements OrderRepository {
async findById(id: string): Promise<Order | null> {
const row = await db.select().from(orders).where(eq(orders.id, id)).get();
return row ? new Order(row.id, row.status) : null;
}
async save(order: Order): Promise<void> {
await db.update(orders).set({ status: order.status }).where(eq(orders.id, order.id));
}
}
Onion — anatomia (4 anéis, dependência só pra dentro):
import de infrastructure. Test sem nada externo rodando.Clean Architecture — anatomia (4 camadas explícitas):
Comparison matrix:
| Aspect | Hexagonal | Onion | Clean |
|---|---|---|---|
| Year | 2005 | 2008 | 2012 |
| Layers | Domain + Application + Adapters | Domain + Domain Svc + App Svc + Infra | Entities + Use Cases + Adapters + Frameworks |
| Symmetry | Yes (driving/driven equal) | No (inward only) | No (inward only) |
| Use case explicit | Implicit | Implicit | Explicit layer |
| Best for | Apps com many integrations | Domain-heavy apps | Mix domain + use case modeling |
| Verbosity | Low-Medium | Medium | High (more layers) |
| Test ease | Excellent (mock adapters) | Excellent | Excellent |
When each wins:
Common pitfalls (across all three):
GET /products é absurdo. Pragmatic: simple CRUD pode pular layers.import { db } from 'infra' em Domain → cycle dependency, defeats inversion.OrderUseCase.create() que apenas chama repo.save() sem business logic = wasted abstraction.CreateOrderPort, FindOrderPort, UpdateOrderPort separados = explosão. Combine em OrderRepository.Pragmatic application Logística:
src/
orders/
domain/ # Order entity, OrderStatus VO, invariants
application/ # OrderApplicationService, use cases
infrastructure/ # PostgresOrderRepo, KafkaPublisher
interface/ # REST controllers, GraphQL resolvers
Testing implications (layer-based test pyramid):
Anti-patterns observados:
findByEmailAndStatusAndCreatedAt) viola SRP; use Specification pattern (cobre 04-06 §2.17).CreateOrderPort, UpdateOrderPort) explosão; combine em Repository.main.ts; use NestJS/tsyringe/awilix).Cruza com 04-06 (DDD, building blocks, Specification pattern), 04-08 (services/monolith, modular structure), 04-03 (event-driven, ports as event consumers), 02-04 (React, frontend hexagonal pra testability), 03-01 (testing, layer-based test strategy).
Modular monolith virou default 2024-2026 para projetos médios (5-15 devs, 8-12 módulos). Microservices premature custa coordenação distribuída sem ganho organizacional. Framework support across ecosystems matured: Spring Modulith 1.3+ (Q4 2024, Spring Boot 3.4 compat), .NET Aspire 9.0+ (Nov 2024 GA), Encore.ts 1.x (Q4 2024, TS-native), Helidon 4.1+ (Oracle, virtual threads). Monorepo TS via Turborepo 2.x / Nx 20+ / Rush. Boundary enforcement automated via ESLint plugin-boundaries, dep-cruiser 16+, ts-arch. §2.15 introduziu modular monolith conceitual; §2.20 é o 2026 framework deep.
Spring Modulith 1.3+: extension oficial do Spring Boot. Module = package + sub-packages internos. Public API exposto via top-level package; internals em internal/ package-private (Java compiler enforce).
// orders/package-info.java
@ApplicationModule(
displayName = "Orders",
allowedDependencies = { "shared", "payments::events" } // só shared + events de payments
)
package com.logistica.orders;
import org.springframework.modulith.ApplicationModule;
// orders/internal/OrderRepository.java — package-private, invisível fora do módulo
package com.logistica.orders.internal;
class OrderRepository { /* ... */ }
// orders/OrderService.java — public API
package com.logistica.orders;
@Service
public class OrderService {
// expõe DTOs, esconde entities
}
// Boundary test (build-time)
class ModularityTest {
@Test
void verifiesModularStructure() {
ApplicationModules.of(LogisticaApplication.class).verify();
}
@Test
void documentsModules() {
new Documenter(ApplicationModules.of(LogisticaApplication.class))
.writeDocumentation(); // gera C4 PlantUML por módulo
}
}
Observability built-in (Micrometer per-module metrics, traces). Events via ApplicationEventPublisher — in-process, type-safe, @ApplicationModuleListener async + transactional.
.NET Aspire 9.0+ (Nov 2024 GA): distributed application orchestration. AppHost.cs declara serviços; orquestra SQL/Redis/RabbitMQ + microservices em dev. OTel out-of-the-box. Deploy targets Azure Container Apps, AWS ECS, K8s.
// AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("pg")
.WithDataVolume()
.AddDatabase("logistica");
var redis = builder.AddRedis("redis");
var rabbit = builder.AddRabbitMQ("rabbit");
var orders = builder.AddProject<Projects.Orders_Api>("orders")
.WithReference(postgres)
.WithReference(rabbit);
builder.AddProject<Projects.Couriers_Api>("couriers")
.WithReference(postgres)
.WithReference(orders); // service discovery automático
builder.Build().Run();
// Dashboard em localhost:18888 — traces + metrics + logs por serviço
Production secrets externalize via builder.AddParameter("ConnectionString", secret: true) + Azure Key Vault / AWS Secrets Manager — AppHost é dev orchestration, não production runtime.
Encore.ts 1.x: TypeScript-native services framework. Service = pasta com encore.service.ts. RPC type-safe cross-service em mesmo monolith. Auto-OpenAPI. Cron + secrets + DB declarative. Deploy single binary OU split em microservices sem rewrite.
// orders/encore.service.ts
import { Service } from "encore.dev/service";
export default new Service("orders");
// orders/api.ts
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { secret } from "encore.dev/config";
const db = new SQLDatabase("orders", { migrations: "./migrations" });
const stripeKey = secret("StripeSecretKey");
export const create = api(
{ method: "POST", path: "/orders", expose: true },
async (req: { items: Item[] }): Promise<{ id: string }> => {
const id = await db.queryRow<{ id: string }>`
INSERT INTO orders ... RETURNING id
`;
return { id: id.id };
}
);
// couriers/api.ts — cross-service call type-safe
import { create as createOrder } from "~encore/clients/orders";
const order = await createOrder({ items }); // compile-time typed, runtime RPC
// orders/cron.ts
import { CronJob } from "encore.dev/cron";
export const cleanup = new CronJob("cleanup-stale", {
title: "Cleanup stale orders",
every: "1h",
endpoint: cleanupHandler,
});
Java Helidon 4.1+ (Oracle): MicroProfile-based. Helidon Nima — virtual threads (Loom) ready, sync programming model com escalabilidade async. Alternativa Quarkus / Micronaut quando Spring é overkill.
Monorepo TS modular: Turborepo 2.x (Vercel — task pipelines + remote cache; turbo.json declara dependências entre packages); Nx 20+ (generators + module boundaries via tags + module federation improvements); Rush (Microsoft, monorepo at scale com phantom-dep prevention).
// turbo.json
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"test": { "dependsOn": ["build"] },
"lint": {}
}
}
// nx.json — boundaries via tags
{
"targetDefaults": {
"build": { "dependsOn": ["^build"] }
},
"implicitDependencies": {}
}
// .eslintrc — Nx enforce tags
{
"@nx/enforce-module-boundaries": ["error", {
"depConstraints": [
{ "sourceTag": "scope:orders", "onlyDependOnLibsWithTags": ["scope:orders", "scope:shared"] },
{ "sourceTag": "type:feature", "onlyDependOnLibsWithTags": ["type:ui", "type:data-access", "type:util"] }
]
}]
}
Boundary enforcement automated (CI gate, não advisory):
// .eslintrc — eslint-plugin-boundaries
module.exports = {
plugins: ["boundaries"],
settings: {
"boundaries/elements": [
{ type: "feature", pattern: "src/features/*" },
{ type: "shared", pattern: "src/shared/*" },
{ type: "api", pattern: "src/features/*/api/*" },
],
},
rules: {
"boundaries/element-types": ["error", {
default: "disallow",
rules: [
{ from: "feature", allow: ["shared", "api"] },
{ from: "shared", allow: ["shared"] },
],
}],
},
};
// .dependency-cruiser.cjs
module.exports = {
forbidden: [
{ name: "no-domain-to-infra",
from: { path: "^src/domain" },
to: { path: "^src/infrastructure" } },
{ name: "no-cross-feature",
from: { path: "^src/features/([^/]+)" },
to: { path: "^src/features/(?!$1)([^/]+)" } }, // regex back-ref
],
};
// arch.test.ts — ts-arch
import { filesOfProject } from "tsarch";
test("domain doesn't import infrastructure", async () => {
const violations = await filesOfProject()
.inFolder("src/domain")
.shouldNot()
.dependOnFiles()
.inFolder("src/infrastructure")
.check();
expect(violations).toEqual([]);
});
In-process events: publish to internal event bus (Spring ApplicationEventPublisher, MediatR .NET, EventEmitter Node). Consumers same process — sub-ms latency vs Kafka. Mesma boundary enforcement: módulo só publica/consome eventos declarados em events.ts API. Para cross-process, outbox + Kafka (cruza com 04-03 §2.19).
Decision matrix 2026:
| Stack | Default 2026 |
|---|---|
| Java/Kotlin team | Spring Modulith 1.3+ (zero ceremony, observability built-in) |
| C#/.NET team | .NET Aspire 9.0 (orchestration dev + production targets) |
| Greenfield TS, future split possível | Encore.ts (single binary now, microservices later) |
| Monorepo TS já existente | Turborepo + ESLint plugin-boundaries + dep-cruiser |
| Polyglot ou heterogêneo | Raw modular monolith + boundary tests |
Stack Logística aplicada: Encore.ts modular monolith — services orders / couriers / payments / notifications / auth. RPC cross-service type-safe. In-process events (OrderCreated publicado em orders, consumido por notifications + couriers). Outbox pattern + Kafka para cross-process com analytics service (separado por scaling profile diferente). Turborepo para shared packages (@logistica/types, @logistica/utils). ESLint plugin-boundaries em CI gate (PR fail se feature importa internal de outra feature). Threshold split: ao atingir 12 módulos / 15 devs, módulos analytics + ml-pricing saem para serviços dedicados (scaling + team boundary Conway).
10 anti-patterns:
@ApplicationModule annotations — zero benefit sobre plain Spring; verifica nada.lint rodando local advisory; PR merge ignora; instale gate em pipeline.Money, UserId).Cruza com 04-07 §2.10 (vertical slices applied), §2.15 (modular monolith concretizado intro), §2.18 (architecture decision per stage), §2.19 (hex vs clean vs onion), 04-06 §2.19 (DDD ACL/OHS/PL — boundary integration patterns), 04-06 §2.10 (modular monolith with BCs DDD), 04-08 §2.2 (modular monolith intro), §2.7 (Strangler Fig from monolith), 04-03 §2.19 (Outbox + Saga production), 03-04 (CI/CD — boundary tests as PR gate), 04-12 §2.24 (Conway's Law — team topology drives module boundaries).
Você precisa, sem consultar:
Refatorar Order Management (do 04-06) com hexagonal puro; manter outros bounded contexts mais simples. Mostrar mix consciente.
pg, sem fastify).OrderRepository, EventPublisher, Clock, IdGenerator.DrizzleOrderRepository, KafkaEventPublisher, SystemClock, UuidGenerator.CreateOrderUseCase, AssignCourierUseCase, MarkOrderDeliveredUseCase.register-courier/update-location/set-availability/ARCHITECTURE.md (continuação do 04-06):
app/, pages/, widgets/, features/, entities/, shared/.Logger, MetricsCollector injetados.Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Qual é a Dependency Rule fundamental em Clean Architecture?
Q2Quando vertical slices vencem horizontal layers?
Q3Qual sintoma indica que um modular monolith está pronto para ser dividido em serviços?
Q4Por que adicionar 7 camadas em um endpoint GET /products é considerado anti-pattern?
Q5Em modular monolith production, qual o papel das ferramentas de boundary enforcement (eslint-plugin-boundaries, dep-cruiser, ts-arch)?
Destrava
04-07 é prereq dos seguintes módulos: