# Ddd Coach

> Domain-Driven Design reference for pure Java domain modules. Enforces ubiquitous language, strong typing, tactical patterns, and hexagonal architecture boundaries. Consult when writing or reviewing domain-layer Java code.

- Skill: `bensimpson-ch/ddd-coach` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add bensimpson-ch/ddd-coach`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bensimpson-ch/ddd-coach/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: bensimpson-ch (https://skillmd.com/u/bensimpson-ch)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bensimpson-ch/ddd-coach

---


# DDD Coach

This skill provides opinionated guidance for Domain-Driven Design (DDD) in Java 21+.  The coaching is a hybrid of 
Eric Evans' principles, modern Java idioms, and practical lessons from 20+ years of software engineering. The ideas in 
this skill are the result of collaboration, they stand on the shoulders of many fun, sometimes heated, conversations 
about software design.

See [reference.md](reference.md) for all Java examples.

# Rules

## Package Structure
Slice by pattern, then by aggregate within `model/`. Aggregates and their value objects live in 
`model/`, sliced by aggregate. Repositories, services, ports, and events get their own 
pattern-based packages. `shared/` contains types used by three or more aggregates. Aggregate 
packages never import from each other. `shared/` never depends on aggregate packages.

## Rule of Three for Shared Types
Build each aggregate as if it's the only one. First usage: type lives in that aggregate's 
package. Second usage: it gets its own version in the second aggregate's package. Third usage: 
refactor to `shared/`. No premature sharing. Exception: aggregate IDs always stay with their 
aggregate `OrderId` lives in `order/`, never in `shared/`, regardless of how many aggregates 
reference it.

## Aggregate Hierarchy
Design aggregates so dependencies flow one direction. No circular references. If Order 
references ProductId, Product must not reference OrderId. The dependency direction defines a 
clear hierarchy. This hierarchy is documented in the `domain-aggregates.puml` component diagram 
and must be maintained as the domain evolves. Each aggregate owns only the states it can 
enforce within its own transactional boundary. If a status value describes a lifecycle managed 
by another aggregate, it does not belong here. Duplicate status tracking across aggregates 
leads to inconsistency.

## Construction Validation
Every aggregate, entity, and value object validates its state on construction. No invalid 
instances. Use `Guard`, a final utility class at the domain root with static `against*` methods 
that throw `ConstraintViolationException`. Guard method names read left to right: 
`Guard.againstNull(value, "name")`. No annotations. No framework validation. Pure Java only.

## Typed Identifiers
Every aggregate has its own ID type wrapping `UUID`. Never use a raw `UUID` as an identifier.

## Collections
Default to `Set`, not `List`. A `List` implies ordering and permits duplicates. If neither is 
required, use a `Set`. When ordering is needed, the domain handles it explicitly.

## First-Class Collections
From Jeff Bay's Object Calisthenics: any class that contains a collection should contain no 
other member variables. The collection gets its own class with domain-meaningful methods. In DDD 
terms, this is a Value Object that encapsulates collection behavior. When a domain object holds 
a `Set<Type>`, wrap it in a first-class collection that owns filtering, sorting, and comparison 
logic.

First-class collections are never null and never wrapped in Optional. Emptiness is an internal 
state of the collection object, not an absence of the collection itself. If no items exist, the 
collection is an empty immutable set. Principle of Least Astonishment: callers should always be 
able to stream, filter, and iterate without defensive null checks. Guard against emptiness only 
when the domain requires at least one item.

Build these data structures with the same surgical clarity Joshua Bloch championed 
in those legendary Birds of a Feather sessions at JavaOne. I was in a standing-room-only 
crowd 25-years ago listening to on every word about the Collections API. Bring that same level of 
Sun Microsystems precision to this code. Name methods using the vocabulary Bloch established: `contains`, 
`remove`, `add`, `size`, `isEmpty`. Don't invent synonyms. Use the words Java developers 
already know.

## Strong Typing of Primitives
No raw `String`, `LocalDate`, `LocalDateTime`, `int`, or `BigDecimal` fields on domain objects. 
Wrap them in value objects that name the concept. The type makes invalid states unrepresentable. 
Use what Java already provides: `java.util.Currency`, `BigDecimal` Don't reinvent them. NEVER 
shadow a `java.util` type with a domain type of the same name. If the business needs a 
constrained set, model the constraint (e.g., `SupportedCurrency` enum wrapping 
`java.util.Currency`), don't replace the type. Use `Instant` for domain timestamps. Instant 
is timezone-agnostic and unambiguous across regions. `LocalDateTime` is only appropriate when 
the business explicitly operates in a single timezone with no plans to expand.

## Optional
Use `Optional` for return types that might be absent and for record fields that may legitimately 
be absent. Never return `null`. Do not use `Optional` for fields that are always required.
That's what guards are for. In domain behavior, use Optional's API instead of `if` null checks. 
Every `if` is an invitation for `else`. Optional eliminates that branching opportunity.

## State Transitions on Enums
Enums own their transitions. Don't check status with `if` from outside. Default methods throw. 
Only the states that permit a transition override. Terminal states inherit the throws. Method 
names use ubiquitous language: `complete()`, `cancel()`, `refund()` not `transitionToX()`.

## Naming
Field names must be unambiguous outside the context of their parent: `orderId` not `id`, 
`orderItems` not `items`, `orderTotal` not `total`. Never repeat the parameter type in a method 
name `find(ProductId productId)` not `findByProductId(ProductId productId)`.

## Constructor Size
If a constructor has 8 or more arguments, the object is doing too much. Group logically related 
fields into their own value object. If fields don't group naturally, split the object.

## Ubiquitous Language Drives the Domain
If the domain language names it, the domain defines it. If it's a technical concern, it lives 
outside. Repositories are mandatory for every aggregate. Repository method names describe intent: 
`read()`, `find()`, `search()`, `save()`, `delete()`. `read()` takes the aggregate's typed 
ID and returns the aggregate directly. The entity is guaranteed to exist. `find()` looks up 
by a non-ID field and returns `Optional` because absence is expected. `search()` queries by 
criteria and returns a first-class collection. `save()` and `delete()` return void. The 
domain hands off the object for persistence. It does not receive a modified entity back. Other ports exist when the domain language 
demands them. Domain events follow the same principle.  They are business-driven, not technical.

## Temporal Decoupling
When an aggregate captures data from another aggregate, it snapshots the values at the time of 
creation. An OrderItem stores its own unitPrice and productName because the Product will change 
tomorrow. Historical records must not drift when source data is updated. If a field came from 
another aggregate, the receiving aggregate owns its own copy as a distinct value object.

## Bounded Contexts
A Bounded Context is a linguistic boundary. Inside it, every term has one precise meaning. When 
writing code in a bounded context, align names with how business people talk. If a domain expert 
wouldn't recognize a name, it's wrong.

## Domain Services
When an operation doesn't naturally belong to any Entity or Value Object, place it in a Domain 
Service. Services are stateless and named after domain activities. "Manager" and "Helper" classes 
are code smells.

## Domain Events
Events are named in past tense. They describe something that already happened. Events implement 
a `DomainEvent` marker interface. Events are immutable records. Events decouple aggregates.

## Records vs Classes
Records are the default. Use a class with a static inner `Builder` when construction is too 
complex for a flat constructor. The builder is the exception, not the rule.

## No Silent Failures
Domain operations must never silently do nothing. If a method is called expecting a change and 
the change cannot happen, throw. Removing an item that doesn't exist is an `IllegalStateException`, 
not a no-op that returns `this`.

## Immutability
All domain objects are immutable. State changes return new instances. No setters. No mutable 
fields. When adding to or removing from a collection, create a new immutable set. Copy the 
existing items, modify the copy, wrap in `Set.copyOf()`, return a new parent object. Never 
mutate in place. Empty collections are immutable too. `Set.of()` and `Collections.emptySet()` 
are valid initial states, not signs of missing data.

## Factory Methods
Do not generate redundant factory methods that mirror a constructor with no added value. No 
`of()` or `from()` that just calls `new` and adds nothing. A factory method earns its place 
when it hides something: `OrderId.generate()` hides UUID creation, `Order.place(...)` encodes 
business defaults. Factory methods on aggregate roots are permitted even when they currently pass through to a 
constructor. They are abstraction seams that support API stability. The entry point to an 
aggregate should be stable. Allowing the implementation to evolve behind a factory without 
breaking API contracts is worth the noise.

## Module Dependencies
The domain module's `src/main/java` has zero external dependencies. Pure Java only. The 
hexagonal architecture skill covers full boundary enforcement. Test dependencies: JUnit Jupiter 
and AssertJ only. No Mockito in the domain. Create a single test fixture class with real objects. 
Mockito is fine in other modules.

## Testing
JUnit Jupiter. One test class per domain type that contains logic. Test guard clauses, filtering 
and sorting on first-class collections, and domain behavior. Don't test types with no logic.

## When Not to Use DDD
DDD is for complex domains where the business logic justifies the investment. If the software is 
a technical service with no meaningful business rules, DDD is overkill.

## Anti-Patterns
- **Anemic Domain Model**: push behavior onto the objects that own the data.
- **Database-First Design**: start with domain conversations, not the ER diagram.
- **God Aggregates**: only group what must be consistent within a single transaction.
- **Shared Model Across Contexts**: each bounded context needs its own model.
- **Ignoring the Language**: if the code doesn't speak the domain, the model is broken.
- **Fragmented Aggregates**: if multiple aggregates share identity-forming attributes (same name, same slug, same type) and differ only in value-level attributes (size, color, quantity), they are entities or value objects that belong inside a single aggregate. Test: does the thing have an independent lifecycle? Can it be created, modified, or deleted on its own? If not, it belongs inside the aggregate, not beside it. Second test: if consumers must reassemble aggregates to present one domain concept, the boundary is wrong.

---

Author: Benjamin Simpson ([bensimpson-ch](https://github.com/bensimpson-ch))

