Teu progresso
0 / 83 módulos0%
Estágio 04 · 04-06
BloqueadoDDD virou buzzword. Times "fazem DDD" e produzem CRUD com vocabulário pomposo (Repository, ValueObject, Entity em todo lado) e pouco da disciplina real, modelo do domínio que reflete realidade, ubiquitous language, bounded contexts, refinamento contínuo via conversa com stakeholders. O resultado: código verbose, abstrações errôneas, e times que confundem padrões táticos com a essência.
Este módulo separa estrátégia (bounded contexts, context mapping, ubiquitous language) de tática (aggregates, value objects, domain events, repositories). Você sai sabendo aplicar DDD com sobriedade e identificar onde NÃO vale.
Livro "Domain-Driven Design: Tackling Complexity in the Heart of Software". Premissa:
DDD não é estilo de código. É disciplina de modelagem.
Vocabulário compartilhado entre devs e domain experts. Termos do código = termos da conversa.
Anti-padrão:
Fonte de bug latente: traduções implícitas. Padrão: glossário no repo, refinado em sprints.
Modelo é coerente dentro de um contexto. Um termo significa coisas diferentes em contextos diferentes.
Em Logística:
Order é entidade central com status pipeline, items, total.Order é um stop com lat/lng e priority.Order é evento monetário.Mesmo "Order" mapeando dados similares mas com perspective distinta. Não force "1 modelo to rule them all".
Bounded context dá autonomia: cada um pode evoluir, scale, choose stack independente.
Diagrama relações entre bounded contexts:
Discutir context map evita arquitetura acidental.
Strategic é o que mais importa. Tactical sem strategic é cargo cult.
Não todo domínio tem mesmo valor:
Em Logística, routing inteligente é core (você compete com isso). Auth é generic (use Auth0 ou implemente sem inovar). Billing é supporting (regras próprias mas sem rocket science).
DDD investe profundo em core; supporting com cuidado; generic compra ou usa pronto.
Aggregate (já vimos): cluster consistente. Aggregate root é única entry. Transações dentro de 1 aggregate. Cross-aggregate via events/saga.
Entity: identity persistente (Order tem id; identidade é o id, não os atributos).
Value Object: identidade pelos atributos. Imutável. (Address com street/city, 2 addresses iguais são "iguais", sem id).
Domain Service: lógica que não cabe naturalmente em entity/VO. Stateless.
Domain Event: algo que aconteceu, com significado de domínio (OrderDelivered).
Repository: abstração pra persistência. "Like a in-memory collection". Hide DB details.
Factory: encapsula criação complex de aggregate.
Specification: encapsula query/filter como objeto.
Em projetos JS/TS modernos, "Repository" pode ser exagero, db.query em camada simples cobre. Não force pattern; use quando complexidade emerge.
Anti-padrão: aggregate gigantesco abrange domínio inteiro. Lock contention, slow loads.
Como decidir o que entra ou sai de um aggregate é onde Senior se diferencia. Critérios em ordem de peso:
Sinais que aggregate ficou grande demais:
loadXWithFullDependencies que joina 5+ tabelas.Order.java / order.ts.Caso real Logística — refactor de Order:
V1 (anti-padrão, common em apps imaturos):
class Order {
id: OrderId; tenantId: TenantId; status: OrderStatus;
customer: Customer; // entidade carregada
items: OrderItem[]; // 1-50 items
payments: Payment[]; // histórico de tentativas
shipments: Shipment[]; // múltiplos splits
trackingPings: TrackingPing[]; // N pings GPS (centenas)
notes: Note[]; // notas colaborativas
addPing(p: TrackingPing) { this.trackingPings.push(p); /* save aggregate inteiro */ }
}
Cada GPS ping (1/30s) re-salva pedido inteiro. Lock contention enorme; latência subindo.
V2 (refactor com critérios acima):
| Aggregate | Entities root | Invariants protegidos |
|---|---|---|
| Order | Order + OrderItems + OrderTotal | sum(items.price) = order.total; status state machine |
| PaymentLedger (separado) | PaymentAttempt list | idempotency keys, total captured ≤ authorized |
| Shipment (separado) | Shipment + ShipmentItems | splits válidos cobrem all OrderItems |
| TrackingHistory (separado, time-series, não DDD aggregate) | TrackingPing append-only stream | nenhum (write-only stream) |
| OrderNotes (CRDT, ver 04-01) | Y.Doc per order | convergência eventual |
Cross-aggregate: events. OrderCreated → notification + analytics. PaymentCaptured → atualiza Order status via handler que carrega Order, atualiza, salva — NÃO joga payment dentro do aggregate Order.
Migração incremental:
id_aggregate_pai como FK.Cada passo deployable, reversível. Nunca faça big-bang refactor de aggregate em produção sem feature flag + canary.
Cruza com 04-03 (events cross-aggregate via outbox), 02-09 §2.13.1 (CDC viabiliza views derivadas sem dual-write), 04-08 §2.20 (extract criteria; mesma lógica pra serviços).
Eventos do domínio comunicam mudanças significativas:
Diferença com integration events:
Map: domain event no aggregate → publisher transforma em integration event quando relevante a outros contexts (com schema versioned).
Você não precisa microservices pra fazer DDD. Modular monolith com módulos = bounded contexts é poderoso:
Java tem Spring Modulith; .NET tem similar; em TS, convenção via package monorepo + lint rules.
Adopt-when-pain-justifies.
Método pra descobrir domínio coletivamente. Walls com sticky notes:
Workshops com stakeholders + devs. Descobre bounded contexts, ubiquitous language, processo real.
Vlingo, Alberto Brandolini popularizaram.
Codebase legacy:
Não pare tudo pra "fazer DDD"; refactor incremental.
ES + DDD são amigos: aggregates emit events; events são domain-language artifacts; replay reconstruct aggregate.
Greg Young, Vaughn Vernon: combinação canônica.
OrderEntityFactoryProvider. Use linguagem do domínio.Bounded contexts candidates:
Cada um com modelo próprio. Order em Routing é diferente de Order em Billing.
Specification pattern encapsula regra de negócio como objeto de primeira classe — combinável, reusável e testável isoladamente. Sem ele, a mesma regra ("este courier pode aceitar este pickup?") aparece duplicada em controller, service, query filter, UI e batch job. Cada cópia drifta no seu próprio ritmo; bug fix em um lugar não propaga. Specification força single source of truth da regra e ainda traduz para SQL — o mesmo predicate roda em-memória (validação de candidate único) e como WHERE clause (query de batch).
interface Specification<T> {
isSatisfiedBy(candidate: T): boolean;
and(other: Specification<T>): Specification<T>;
or(other: Specification<T>): Specification<T>;
not(): Specification<T>;
}
abstract class CompositeSpecification<T> implements Specification<T> {
abstract isSatisfiedBy(candidate: T): boolean;
and(other: Specification<T>) { return new AndSpecification(this, other); }
or(other: Specification<T>) { return new OrSpecification(this, other); }
not() { return new NotSpecification(this); }
}
class AndSpecification<T> extends CompositeSpecification<T> {
constructor(private left: Specification<T>, private right: Specification<T>) { super(); }
isSatisfiedBy(c: T) { return this.left.isSatisfiedBy(c) && this.right.isSatisfiedBy(c); }
}
// OrSpecification, NotSpecification idem.
Specs concretas no Logística:
class CourierIsAvailable extends CompositeSpecification<Courier> {
isSatisfiedBy(c: Courier) { return c.status === 'available' && c.activeOrders < c.maxConcurrent; }
}
class CourierIsInRadius extends CompositeSpecification<Courier> {
constructor(private lat: number, private lng: number, private radiusM: number) { super(); }
isSatisfiedBy(c: Courier) { return haversine(c.lat, c.lng, this.lat, this.lng) <= this.radiusM; }
}
class CourierMeetsVehicleRequirement extends CompositeSpecification<Courier> {
constructor(private required: VehicleType) { super(); }
isSatisfiedBy(c: Courier) { return c.vehicleType === this.required; }
}
const eligible = new CourierIsAvailable()
.and(new CourierIsInRadius(order.lat, order.lng, 5000))
.and(new CourierMeetsVehicleRequirement(order.requiredVehicle));
interface SqlSpecification<T> extends Specification<T> {
toSqlClause(paramOffset: number): { sql: string; params: unknown[] };
}
class CourierIsAvailableSql extends CourierIsAvailable implements SqlSpecification<Courier> {
toSqlClause(offset: number) {
return { sql: `status = $${offset} AND active_orders < max_concurrent`, params: ['available'] };
}
}
Composite traduz and() para AND SQL, indexando params em ordem. Resultado: a mesma eligible.toSqlClause() vira WHERE status = $1 AND active_orders < max_concurrent AND ST_DWithin(location, ST_MakePoint($2, $3)::geography, $4) AND vehicle_type = $5. Sem isso, querer paridade entre validação in-memory e query batch obriga manter duas cópias da regra — drift garantido em 6 meses.
Distinção operacional, não acadêmica:
Order.total >= 0, sum(items.subtotal) === total). Enforced no constructor e no fim de todo método mutador. Violação é bug — lança InvariantViolation (não recuperável). Coberto por unit test do aggregate.Order.confirm() requer status === 'pending'). Violação é fluxo legítimo — retorna Result<_, PreconditionFailed> com razão explícita. API responde 422 com mensagem.Misturar os três é fonte de bug clássica: validação de email no aggregate (deveria ser no boundary), precondition tratada como invariant (crash em vez de 422), invariant silenciada com if defensivo (corrompe estado aos poucos).
Order.create(...) retornando Result<Order, InvariantViolation>. Caller é forçado a tratar falha; impossível instanciar Order inválido.confirm, cancel, addItem) que valida precondition e re-checa invariant.Money, Email, OrderId) validam uma vez na criação; aggregate consome sem re-validar.assertInvariants() privado chamado no fim de todo método mutador. Centraliza checks; não dispersa.class Order {
private constructor(
readonly id: OrderId,
private status: OrderStatus,
private items: ReadonlyArray<OrderItem>,
private total: Money,
) {
this.assertInvariants();
}
static create(items: OrderItem[], currency: Currency): Result<Order, InvariantViolation> {
if (items.length === 0) return Err(new InvariantViolation('Order must have at least 1 item'));
const total = items.reduce((acc, i) => acc.add(i.subtotal), Money.zero(currency));
return Ok(new Order(OrderId.new(), OrderStatus.Pending, items, total));
}
confirm(): Result<void, PreconditionFailed> {
if (this.status !== OrderStatus.Pending) return Err(new PreconditionFailed('Order is not pending'));
this.status = OrderStatus.Confirmed;
this.assertInvariants();
return Ok();
}
private assertInvariants() {
if (this.total.isNegative()) throw new InvariantViolation('Order total cannot be negative');
if (this.items.length === 0) throw new InvariantViolation('Order cannot have zero items');
const sum = this.items.reduce((acc, i) => acc.add(i.subtotal), Money.zero(this.total.currency));
if (!sum.equals(this.total)) throw new InvariantViolation('Order total mismatch with items sum');
}
}
OrderService externo. Aggregate vira DTO; invariant vaza para múltiplos services e drifta.toSqlClause(): para filtrar 50k couriers elegíveis, código resgata todos do banco e filtra em JavaScript. OOM em produção. Specification com SQL força paridade e empurra o trabalho para o índice.if (!email.includes('@')) dentro do aggregate. Decida: aggregate trusts (boundary valida) OU aggregate é o boundary (sem validation layer). Não os dois.if (status === 'pending') ... else if (...) espalhado: substitua por specification com toSqlClause() que reusa em query e em código.Cruza com 04-06 §2.7 (tactical patterns são fundação), 04-06 §2.8 (aggregates carregam invariants), 04-03 §2.4 (event sourcing precisa invariants explícitas para replay determinístico), 02-09 §2.7.1 (specifications viram queries SQL com índices apropriados).
Identificar bounded contexts (BC) errado é a falha mais cara em DDD: borders no lugar errado produzem distributed monolith — services com cardinalidade alta de chamadas síncronas entre si, deploy acoplado, 5x complexidade operacional sem ganho de autonomy. Borders certos entregam deploy independente, autonomy de team e linguistic consistency interna (cada context com ubiquitous language coerente; mesmo termo significa coisa única dentro do context). Eric Evans, canônico: "A bounded context is the conditions under which a particular model is defined and applicable." Fora desse range, o modelo deixa de valer — e tentar esticá-lo é o caminho mais rápido pro Big Ball of Mud.
Alberto Brandolini, ~2013. Workshop colaborativo pra discover business processes via timeline of events em past tense. Três variantes operacionais:
Materiais: parede de 5-10m em papel kraft, post-its laranja (events), roxo (policies/processes), amarelo (actors), azul (commands), rosa (hot spots/problems), verde (read models), Sharpies pretos. Sem laptops abertos.
OrderPlaced, CourierAssigned, OrderShipped, PaymentFailed. Linguagem do business, nunca técnica (RowInserted é ruído).Foco em 1 sub-process (e.g., AssignOrderToCourier). Adiciona:
Output: candidate aggregate borders, command handlers, projections — input direto pra Software Design.
Flow canônico: Command (azul) → Aggregate (yellow background, agrupa events emitidos) → Events (laranja). Policies (roxo) reagem a events e emitem commands, iniciando sagas. Read models (verde) consomem events e expõem projeções pra UI/queries.
BCs identificados:
Pivotal events: OrderPlaced (cross-BC, dispara Routing), CourierAssigned (Routing → Orders), Delivered (cross-BC, fecha Order e dispara Billing).
Context Map:
CourierAssigned; Orders prioriza features no backlog do Routing (latência de match, retry policy).Delivered no broker; Billing consome com schema versionado (Published Language em AsyncAPI).Aggregate borders: Order (per pickup-delivery cycle), Courier (per courier, com status e capacidade), Subscription (per tenant, ciclo de billing).
CreateOrder em vez de OrderCreated) — revela confusão command vs event.Cruza com 04-03 §2.1 (events em DDD são building blocks de Event Storming → implementation), 04-08 (modular monolith com BCs antes de split físico), 04-12 (Conway's Law inevitável; team topology dita BC viability), 02-09 §2.7.1 (schema-per-BC em Postgres antes de database físico separado), 04-02 (Published Language como AsyncAPI entre contexts).
Bounded contexts isolados resolvem complexidade dentro de cada contexto, mas sistema real tem 8-15 BCs que precisam trocar dados. Sem padrões explícitos de integração, BCs viram big ball of mud distribuído: coupling implícito via banco compartilhado, eventos sem schema, clients quebrando a cada release, equipes brigando por ownership. Eric Evans catalogou 9 context relationship patterns no DDD blue book (capítulo 14); o trabalho estratégico é escolher o padrão certo por par de contextos no context map e aplicá-lo com rigor de schema, contract testing e evolution policy.
Stack 2026 deu ferramental concreto pra implementar esses patterns: AsyncAPI 3.0 (GA Q4 2024 — operations e channels separadas, reuso de mensagens, melhor mapeamento pra event-driven BCs), OpenAPI 3.2 estável, JSON Schema 2020-12, Pact 5+ (rust core, performance e cross-language matching), schema registries (Confluent, Apicurio, AWS Glue, Buf Schema Registry pra Protobuf), Buf CLI detectando breaking changes em CI. Não há mais desculpa pra "passa um JSON aí" entre contexts.
| Pattern | Direção | Quando usar | Custo |
|---|---|---|---|
| Partnership | Bidirectional | Dois BCs com sucesso interdependente; teams coordenam roadmap | Alto — exige sync constante |
| Shared Kernel | Bidirectional | Subset pequeno explícito de modelo compartilhado (tenant_id, currency) | Médio — qualquer change vira coordenação |
| Customer-Supplier | Upstream → Downstream | Downstream tem voice no roadmap upstream; SLA explícito | Médio — supplier respeita customer needs |
| Conformist | Upstream → Downstream (passive) | Downstream adota modelo upstream as-is; sem voice | Baixo — mas downstream perde pureza |
| Anti-Corruption Layer (ACL) | Upstream → Downstream (defensive) | Modelo upstream é tóxico/legado; isolar via translator | Médio — código de tradução, mas modelo interno preservado |
| Open Host Service (OHS) | Upstream → many Downstream | BC vira service público; protocolo formal (OpenAPI/AsyncAPI) | Alto — versioning, deprecation, suporte multi-cliente |
| Published Language (PL) | Cross-cutting | Vocabulário formal compartilhado (DTOs, event schemas) | Alto — governance de evolução |
| Separate Ways | Sem relação | Integração não vale custo; cada BC resolve sozinho | Zero — mas exige disciplina pra não acoplar depois |
| Big Ball of Mud | Caos | Anti-pattern reconhecido; isolar com ACL na fronteira | Existencial — todo o resto morre se não isolar |
Quando modelo externo (legado, terceiro, BC com semantics ruim) contamina modelo interno se importado direto. ACL é adapter dedicado: traduz vocabulário, esconde quirks, blinda BC interno. Padrão essencial em Strangler Fig migration (04-08 §2.7) — ACL na fronteira do legado permite reescrever incrementalmente sem corromper modelo novo.
// payments-context/acl/stripe-translator.ts
// ACL: traduz Stripe domain → Payment domain interno.
// Stripe expõe `PaymentIntent` com 47 fields, status enum confuso, currency lowercase string.
// Payment BC interno tem `Payment` aggregate com semantic claro.
import Stripe from 'stripe';
import { Payment, PaymentStatus, Money, Currency } from '../domain/payment';
export class StripePaymentTranslator {
/** External (Stripe) → Internal (Payment aggregate). */
toPayment(intent: Stripe.PaymentIntent): Payment {
return Payment.rehydrate({
id: intent.metadata.payment_id, // ID interno injetado em metadata
tenantId: intent.metadata.tenant_id,
amount: this.toMoney(intent.amount, intent.currency),
status: this.toStatus(intent.status),
externalRef: { provider: 'stripe', id: intent.id },
createdAt: new Date(intent.created * 1000),
});
}
private toMoney(amountMinor: number, currency: string): Money {
// Stripe envia em menor unidade (cents) lowercase; interno usa Money VO + Currency enum.
return Money.fromMinor(amountMinor, Currency.parse(currency.toUpperCase()));
}
private toStatus(stripeStatus: Stripe.PaymentIntent.Status): PaymentStatus {
// 9 statuses Stripe → 4 internos. Translator decide mapeamento.
switch (stripeStatus) {
case 'succeeded': return PaymentStatus.Captured;
case 'processing':
case 'requires_capture': return PaymentStatus.Authorized;
case 'canceled': return PaymentStatus.Canceled;
default: return PaymentStatus.Pending;
}
}
}
ACL fica fora do aggregate (anti-pattern: translator dentro do Payment). Aggregate fala só linguagem interna; adapter na fronteira.
BC publica protocolo formal pra múltiplos consumers. Dois protocolos típicos em 2026: OpenAPI 3.2 pra sync REST/RPC, AsyncAPI 3.0 pra eventos. Schema vira contrato versionado em registry; clients geram SDK; breaking changes detectados em CI via Buf/openapi-diff.
# orders-context/openapi.yaml — Orders BC como OHS
openapi: 3.2.0
info:
title: Orders Service
version: 2.4.0 # SemVer estrito; major bump = breaking
servers:
- url: https://api.logistica.example/orders/v2
paths:
/orders/{orderId}:
get:
summary: Recupera order por id
parameters:
- { name: orderId, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/OrderView' }
components:
schemas:
OrderView: # Published Language: contrato externo, NÃO é o aggregate interno
type: object
required: [id, tenantId, status, items, total]
properties:
id: { type: string, format: uuid }
tenantId: { type: string }
status: { type: string, enum: [created, picking, in_transit, delivered, canceled] }
items: { type: array, items: { $ref: '#/components/schemas/OrderItem' } }
total: { $ref: '#/components/schemas/Money' }
Eventos via AsyncAPI 3.0:
# orders-context/asyncapi.yaml
asyncapi: 3.0.0
info: { title: Orders Events, version: 1.3.0 }
channels:
orderPlaced:
address: orders.placed.v1
messages:
orderPlaced: { $ref: '#/components/messages/OrderPlaced' }
operations:
publishOrderPlaced:
action: send
channel: { $ref: '#/channels/orderPlaced' }
components:
messages:
OrderPlaced:
payload:
$ref: 'https://schemas.logistica.example/orders/order-placed/1.3.0.json'
Schema URL aponta pra registry (Confluent/Apicurio); CI valida compat backward com Buf:
buf breaking --against '.git#branch=main' schemas/
PL é o vocabulário externo formal: schemas em OAS/AsyncAPI/Protobuf que múltiplos BCs compartilham. Não é o modelo interno — é DTO de fronteira. Regra de evolução em 2026:
deprecated: true antes de remover no major);Downstream depende de upstream mas tem canal formal pra influenciar roadmap. Contrato selado por Pact 5 (consumer-driven contract testing): customer publica expectations, supplier valida em CI antes de release.
// orders-context/test/pact/courier-availability.pact.ts
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
const pact = new PactV4({ consumer: 'orders', provider: 'courier-availability' });
pact
.addInteraction()
.given('courier 7f3a is available in zone SP-01')
.uponReceiving('check availability')
.withRequest('GET', '/availability', (b) => b.query({ courierId: '7f3a', zone: 'SP-01' }))
.willRespondWith(200, (b) =>
b.jsonBody({
courierId: MatchersV3.uuid('7f3a...'),
available: MatchersV3.boolean(true),
slotsRemaining: MatchersV3.integer(3),
}),
)
.executeTest(async (mock) => {
/* call client, assert behavior */
});
Pact broker armazena contracts; supplier roda pact:verify em CI com todos os customers' expectations. Breaking change vira PR-blocker.
Downstream adota modelo upstream as-is. Válido quando upstream é estável (Stripe, AWS) e tradução não agrega valor. Risco: modelo upstream vaza pra dentro do BC. Use só pra integration code path, nunca pra core domain.
// notifications-context/conformist/stripe-webhook-handler.ts
// Conformist: aceita payload Stripe direto, sem traduzir. OK porque Notifications BC só
// faz log + relay, não modela Payment internamente.
export async function handleStripeWebhook(event: Stripe.Event) {
await notificationsRepo.log({
source: 'stripe',
type: event.type,
rawPayload: event, // armazena bruto; translation seria desperdício aqui
receivedAt: new Date(),
});
}
Quando Stripe vira fonte de Payment domain real, upgrade pra ACL (caso anterior). Conformist é decisão consciente, não preguiça.
Subset minúsculo de modelo compartilhado entre BCs em Partnership. Mantenha tiny (10-50 linhas) ou converta pra Customer-Supplier. Anti-pattern: "shared types" virou monorepo coupling.
// shared-kernel/index.ts — usado por orders, couriers, payments, notifications
export type TenantId = string & { readonly __brand: 'TenantId' };
export type Currency = 'BRL' | 'USD' | 'EUR';
export class Money {
private constructor(readonly minor: number, readonly currency: Currency) {}
static of(minor: number, currency: Currency): Money {
if (!Number.isInteger(minor) || minor < 0) throw new Error('invalid amount');
return new Money(minor, currency);
}
add(other: Money): Money {
if (other.currency !== this.currency) throw new Error('currency mismatch');
return new Money(this.minor + other.minor, this.currency);
}
}
Mudança aqui exige coordenação entre todos os BCs Partnership. Por isso o tamanho importa.
Web BFF traduz N microservice contracts em shape UI-friendly. Sem translation = puro proxy = anti-pattern.
// web-bff/order-detail-view.ts — ACL entre microservices e UI React
export async function buildOrderDetailView(orderId: string): Promise<OrderDetailView> {
const [order, courier, payment] = await Promise.all([
ordersClient.get(orderId), // OAS: OrderView
couriersClient.byOrder(orderId), // OAS: CourierProfile
paymentsClient.byOrder(orderId), // OAS: PaymentSummary
]);
// Translation: 3 contracts → 1 view model otimizado pra UI
return {
id: order.id,
customerLabel: `${order.customerName} (${order.customerEmail})`,
statusBadge: mapStatusToBadge(order.status),
courierCard: courier ? { name: courier.fullName, phone: courier.phoneE164 } : null,
paymentLine: `${payment.method} • ${formatMoney(payment.total)} • ${payment.status}`,
};
}
Context map típico (8 BCs): Orders, Couriers, Routing, Payments, Notifications, Auth, Tenants, Pricing. Relationships:
Location/Zone types (teams coordenam).TenantId).Schema registry (Apicurio em Postgres backend) hospeda OAS + AsyncAPI; Buf cobre Protobuf interno; CI roda buf breaking + pact:verify + openapi-diff em todo PR.
deprecated: true por N minor antes de remover.Cruza com §2.4 (context map intro), §2.5 (strategic vs tactical), §2.7 (tactical patterns + Repository), §2.10 (modular monolith com BCs), §2.13 (ACL mention), §2.18 (Event Storming workshop), 04-08 §2.7 (Strangler Fig + ACL), 04-08 §2.11 (service mesh — infra pra OHS), 04-05 §2.27 (OpenAPI 3.2 + AsyncAPI como PL formats), 04-02 (events como PL), 04-12 §2.24 (Conway's Law dita BC integration), 04-03 §2.13 (ACL como anti-corruption foundation).
Você precisa, sem consultar:
Refatorar Logística rumo a modular monolith DDD.
src/order-management/, src/routing/, etc.).Order aggregate com root + events.order.markPickedUp(courierId, ts), checa status, courier assigned).OrderPickedUp) emitted.OrderAssigned → Courier Management reage atualizando status courier; Notifications reage emitindo push.Order de Order Management mas tem próprio RoutableStop (mapeia campo necessários, ignora o resto).ARCHITECTURE.md com:
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 diferença essencial entre DDD strategic e tactical?
Q2Por que aggregate gigante é anti-pattern?
Q3Qual a distinção entre domain event e integration event?
Q4Quando o pattern Anti-Corruption Layer (ACL) é o default correto?
Q5Por que Specification pattern com toSqlClause() vence implementação só in-memory?
Destrava
04-06 é prereq dos seguintes módulos: