Skip to content

Architecture (DDD Light)

  • VO-first — modela tudo como Value Object. Promove a Entity quando precisa de identidade ao longo do tempo. Promove a Aggregate Root quando um conjunto de entities tem invariantes compartilhados.
  • Context nomeado pelo aggregate rootbudget/ orquestra Budget, accounts/ orquestra Account, etc.
  • Exceção: capability-named — quando não há aggregate root próprio, context é nomeado pela capability (planning/, agent/). Só Domain Services stateless lendo aggregates de outros contexts.
  • Ubiquitous Language — code terms = termos que o casal usa. PT/EN mix livre.
  • No anemic models — comportamento mora nos objetos de domínio.
  • Light — sem CQRS mandatory, sem event sourcing default. Promove só quando o cenário cobrar.

Detalhes em AGENTS.md ## DDD Light Conventions.

  • budget/Budget aggregate root, RecurringExpense entity. Custos mensais, expected vs actual, variance.
  • accounts/Account aggregate, Invoice entity, Transaction VO. Cartões e faturas.
  • goals/Goal aggregate, Contribution VO. Metas de poupança do casal.
  • household/Household aggregate, IncomeEntry VO. Renda fixa + variável, members.
  • planning/ — capability-named. FeasibilityCheck, BudgetAlerts, ReminderService, EvalRunner. Domain Services cross-aggregate read-only.
  • agent/ — capability-named. AgentChat (Domain Service stateless), ChatMessage VO, read/write tools, ConversationGuard, ports AgentMemory/WhatsAppGateway.
  • referrals/Referrals root, ReferralLink entity, ShareToken VO. Attribution analítico (sem fila).
  • shared-kernel/Money, BillingDay, Period, Member entity, ExchangeRate VO, port ExchangeRateProvider.
src/contexts/<ctx>/
domain/ # puro: VOs, entities, aggregates, Domain Services
application/ # ports (interfaces), orchestrators
infrastructure/ # adapters (real + fakes)
interface/ # entrada (HTTP, CLI, WhatsApp) quando aparecer
  • Domain nunca importa de application ou infrastructure. Inversão de dependência canônica.
  • Ports em application/ quando a coisa é infra-facing (persistência, LLM, rede).
  • Domain Services em domain/services/ quando a coisa é pura (input aggregate → output, sem I/O).
  • Cross-context infra ports moram em shared-kernel/application/ (ex: ExchangeRateProvider consumido por planning/goals/agent).
  • Read-only via Domain ServiceExpenseReconciler em budget/domain/services/ importa Invoice/Transaction de accounts/ direto. Inversão (port/interface) só quando aparecer segundo consumidor.
  • Agent atravessa context pra ler port, nunca adapter — write tools em agent/domain/tools/ importam BudgetRepository de budget/application/ (interface). NUNCA de budget/infrastructure/ (impl).
  • Stringly-typed cross-boundaryshareToken, waid, resourceId, threadId são strings opacas atravessando contexts. Caller resolve mapping; domínio não conhece a fonte.

InMemoryBudgetRepository, MockWhatsAppGateway, InMemoryAgentMemory — todos moram em <ctx>/infrastructure/ junto com o adapter real. Mock inline na spec seria mais barato hoje, mas o fake vira reuso quando outros cenários precisarem.

  • Aritmética de data em UTC — sempre getUTCFullYear/getUTCMonth/Date.UTC(). Local timezone (BRT) quebra silenciosamente monthsBetween.
  • Domain não converte moeda — caller (UX/tool) entrega ExchangeRate injetado.
  • Métricas derivadas, não armazenadaspace, forecast, total() recalculam a cada chamada.
  • Verbos do cenário revelam a Entity — “registrar” puxa VO; adicionar “atualizar” força promoção a Entity.

Lista completa em AGENTS.md ## Gotchas.