NWave nw-ddd-tactical

Tactical DDD — aggregate design rules, entities, value objects, domain events, repositories, domain services, and anti-pattern detection

install
source · Clone the upstream repo
git clone https://github.com/nWave-ai/nWave
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/nWave-ai/nWave "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/nw/skills/nw-ddd-tactical" ~/.claude/skills/nwave-ai-nwave-nw-ddd-tactical-7eb1a4 && rm -rf "$T"
manifest: plugins/nw/skills/nw-ddd-tactical/SKILL.md
source content

Tactical DDD

Implementation patterns within a bounded context. Tactical DDD answers: "How do we structure the domain model?"

Aggregates

An aggregate is a consistency boundary -- a cluster of domain objects that must be transactionally consistent.

Vernon's Four Design Rules

  1. Model true invariants in consistency boundaries: Only include elements that MUST be consistent within the same transaction. If two entities don't share an invariant, they belong in separate aggregates.

  2. Design small aggregates: ~70% of aggregates contain only a root entity with value-typed properties. Large aggregates create concurrency contention, scalability failures, and memory pressure.

  3. Reference other aggregates by identity: Use

    ProductId
    not
    Product
    . Direct object references create accidental cross-aggregate transactions and prevent independent scaling.

  4. Use eventual consistency outside the boundary: Domain events communicate across aggregates. One transaction = one aggregate. If you need to update two aggregates "atomically," reconsider your boundaries.

Aggregate Design Checklist

  • Contains only elements sharing true invariants
  • Root entity controls all access (no direct child manipulation)
  • External references by ID only (no object graphs)
  • Small enough for single-transaction performance
  • Command produces events (in event-sourced systems) or mutates state (in traditional)
  • Business rules validated in command handling, not in persistence layer

Common Aggregate Smells

SmellSignalFix
God Aggregate>5 entities, frequent concurrency conflictsSplit by invariant analysis
Anemic AggregateAll logic in services, aggregate = data bagMove business rules into aggregate methods
Cross-Aggregate TransactionTwo aggregates modified in one DB transactionUse domain events + eventual consistency
Deep NestingAggregate root -> entity -> entity -> valueFlatten; promote nested entities to own aggregates
Missing AggregateBusiness rules scattered in application servicesIdentify invariant cluster, create aggregate

Entities

Objects with identity that persists across state changes. An entity is the "same" entity even when all its attributes change (a person who changes name, address, and job is still the same person).

Design rules: Identity is assigned once and never changes | Equality by identity, not attributes | Track lifecycle (created, modified, archived) | Place business rules that depend on identity here

Value Objects

Objects defined by their attributes, not identity. Two value objects with the same attributes are interchangeable.

Design rules:

  • Immutable: Never modify; create new instances
  • Self-validating: Constructor rejects invalid state. If a
    Money
    object exists, it has a valid amount and currency
  • Side-effect-free behavior: Methods return new values, don't mutate
  • Structural equality: Compare by attributes, not reference

Examples: Money(amount, currency) | Email(address) | DateRange(start, end) | Address(street, city, zip, country) | Temperature(value, unit)

Context sensitivity: Whether something is Entity or Value Object depends on bounded context. Address is a VO in e-commerce (shipping destination) but an Entity in utility billing (service location with lifecycle).

Domain Events

Represent something that happened in the domain. Always past tense.

Naming Convention

Correct (past tense)Wrong (imperative)
OrderPlacedPlaceOrder (that's a command)
PaymentReceivedReceivePayment
UserRegisteredRegisterUser
InventoryReservedReserveInventory

Event Design Guidelines

  • Self-contained: Include all data needed to understand what happened
  • Domain language: Business terms, not technical jargon
  • One fact per event:
    OrderConfirmed
    not
    OrderStatusChanged
  • No behavior: Events carry data only
  • Granularity matters: Too coarse (OrderUpdated) loses meaning; too fine (OrderFieldChanged) is a generic audit log

Event Categories

Domain events (in-process): Within a bounded context, dispatched via mediator. Collected during command handling, dispatched before/after commit.

Integration events (distributed): Cross bounded-context via message brokers. Always asynchronous. Published only after successful persistence. May use different schema than domain events (translated at boundary).

Repositories

One repository per aggregate (not per entity). Interface defined in domain layer, implementation in infrastructure.

Collection-oriented: add/remove/find -- repository tracks changes (Unit of Work pattern). Natural for ORMs.

Persistence-oriented: explicit save/load -- caller manages lifecycle. Natural for document DBs and event-sourced systems.

Rules: Never expose persistence details (SQL, JSON) to domain | Return domain objects, not DTOs | Query methods use domain language (findActiveOrders, not findByStatusEquals)

Domain Services

Stateless operations that span multiple aggregates or don't naturally belong to any single entity.

When to use: Operation involves multiple aggregates | Logic doesn't belong to any entity | Named using ubiquitous language

Danger: Overuse creates anemic domain models. First try to place the behavior on an entity or aggregate. Domain service is the last resort for truly cross-aggregate logic.

Domain ServiceApplication Service
Domain logic (rules, calculations)Orchestration (transaction, security, events)
Domain types onlyDomain + infrastructure ports
Domain layerApplication layer
TransferFunds, CalculateDiscountPlaceOrderHandler, RegisterUserUseCase

Anti-Pattern Detection

When analyzing existing code, look for these patterns:

Anti-PatternCode SignalRecommendation
Anemic Domain ModelEntities = data classes, services contain all
if
logic
Move business rules to entities/aggregates
Primitive Obsession
String email
,
double amount
,
int quantity
Create value objects: Email, Money, Quantity
Database-Driven DesignEntities mirror DB tables 1:1, foreign keys as navigationModel domain first, map to persistence second
Missing BoundariesSingle model used by all features, vocabulary conflictsIdentify contexts via language divergence
Logic in Wrong LayerBusiness rules in controllers/handlersPush down to domain objects
Service BloatService class with 20+ methodsSplit by use case, push logic to aggregates
Event as RPCEvent handler returns result that caller depends onEvents are fire-and-forget; use command for request-response