From ddd
Design and unit-test the domain-model layer of a DDD / Clean Architecture project — Entities, Value Objects, Aggregates, Collections, and their test-data Builders, following the project's base-class contracts and conventions. Use when designing, refactoring, or writing unit tests for an entity, value object, aggregate, collection, or builder.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ddd:entity-designThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill builds and unit-tests the **innermost layer** of a Clean Architecture
references/aggregate.test.tsreferences/aggregate.tsreferences/builder.tsreferences/entity.test.tsreferences/entity.tsreferences/examples/audora/ExternalEvidence.test.ts.txtreferences/examples/audora/ExternalEvidence.ts.txtreferences/examples/audora/FileAggregate.test.ts.txtreferences/examples/audora/Member.ts.txtreferences/examples/audora/MemberBuilder.ts.txtreferences/examples/audora/MemberCollection.ts.txtreferences/examples/audora/ObjectId.test.ts.txtreferences/examples/audora/ObjectId.ts.txtreferences/examples/audora/User.test.ts.txtreferences/examples/audora/User.ts.txtreferences/examples/audora/UserBuilder.ts.txtreferences/value-object.test.tsreferences/value-object.tsThis skill builds and unit-tests the innermost layer of a Clean Architecture
codebase — the domain model: Entities, Value Objects, Aggregates, and
Collections — plus the test-data Builders other layers depend on. It is the
companion to ddd:usecase-design, which sits one layer up (usecases/steps/factories);
this skill owns the objects those usecases manipulate.
The methodology here is generic. Everything that varies per repository — base-class names and import paths, the serialization contract, factory conventions, where builders live, the codegen command, file placement, the run command — lives in a Project Profile. Load that first.
Use for:
Do not use for:
ddd:usecase-design.A domain-model unit test instantiates the real object and asserts on its behavior; it never mocks a collaborator (there are none — the model has no injected ports).
Read the profile that matches the repo before designing or testing anything:
.claude/ddd/entity-design.md in the target repo root. When present,
it is the source of truth for every project-specific slot referenced below.ddd:create-profile to capture those conventions
in a committed .claude/ddd/entity-design.md for next time — but only create
that file when they ask.${CLAUDE_PLUGIN_ROOT}/skills/create-profile/references/examples/audora/entity-design.md.The profile supplies: base-class names + import paths, identity/audit fields, the
serialization contract (snapshot/toJSON), value-object factory conventions,
directory + barrel layout, the codegen command, builder location + import alias,
test placement/naming, and the run command.
Pick the building block from the object's nature, not its data:
| Ask | If yes → | Why |
|---|---|---|
| Does it have a distinct identity that persists across changes to its fields, and a lifecycle (created, modified)? | Entity | Two entities with identical fields are still different things; equality is by uid. |
| Is it defined entirely by its values, interchangeable when its values match, and conceptually immutable? | Value Object | No identity; equality is by value. Wraps and validates a concept (an email, an id, a money amount). |
| Is it a consistency boundary that owns a root entity plus related entities/data that must be loaded and reasoned about together? | Aggregate | The root is the only external entry point; the aggregate guarantees the cluster's invariants. |
| Is it a typed list of one of the above that needs domain query methods? | Collection | A first-class list (e.g. MemberCollection) with named lookups, not a bare T[]. |
When unsure between Entity and Value Object: if you would ever ask "is this the same one as before?" it is an Entity; if you only ask "are these equal?" it is a Value Object.
The profile gives the real names and import paths; the shapes are universal.
Entity — identity + audit fields, protected constructor, serialization:
interface EntityProps { uid: <Id>; createdAt: Date; modifiedAt: Date }
abstract class Entity {
protected readonly _uid: <Id>
protected readonly _createdAt: Date
protected _modifiedAt: Date // mutable: entities change over time
get uid(): <Id> // identity — equality is by this
get createdAt(): Date
get modifiedAt(): Date; set modifiedAt(v: Date)
abstract toJSON(): any // serialization (may be @deprecated — check profile)
protected constructor(props: EntityProps) { /* assigns the three */ }
}
Value Object — no identity, immutable, serializes via a snapshot:
abstract class ValueObject {
abstract get snapshot(): any // the serializable, frozen shape
toJSON(): any { return this.snapshot } // delegates to snapshot
}
Aggregate — a root accessor over a cluster:
abstract class Aggregate<Root> { abstract get root(): Root }
// concrete: class FooAggregate implements Aggregate<Foo> { get root(): Foo … }
Collection — a typed Array subclass:
abstract class Collection<T> extends Array<T> { protected constructor(items: T[]) { super(...items) } }
Mixed-era caveat (record it in the profile): older Value Objects may predate the
ValueObjectbase — they don'textends ValueObject, exposevalue+equals()+ their owntoJSON(), and have nosnapshot. Newer ones extendValueObjectand implementsnapshot. Match whichever the neighbouring file uses; don't "upgrade" a legacy VO unless asked.
Props interface (constructor input) and, when extending the base, a
Snapshot interface (serializable shape — enums become strings).private readonly (use a non-readonly field only for the rare
field with a setter, e.g. a status that legitimately transitions).trim(), toLowerCase(), reject
invalid input by throwing a domain error (§8). The constructor is the invariant gate.get isApplicable()).get snapshot() returning Object.freeze({ … }); inherit toJSON().make(x?) → VO | null), a direct constructor wrapper
(withString(x)), and a restore(snapshot) for rehydration. Add equals(other)
for value comparison.Props/Snapshot types if other layers construct or persist the VO.See ${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/value-object.ts.
interface FooProps extends EntityProps { … } — base identity/audit fields plus
domain fields; mark genuinely optional ones ?.constructor(props: FooProps) { super(props); … } — call super(props) first,
then assign domain fields, coalescing optionals to defaults (props.roles || [],
props.status || Status.Pending, props.x ?? null).get isActive()).toJSON() (unwrap value objects to primitives, e.g. this.uid.value) —
unless the profile marks it deprecated/unused.See ${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/entity.ts.
class FooAggregate implements Aggregate<Foo>. Take a { root, … }
props object; hold the root and related entities/data as private readonly
(a lazily-loaded field like file contents may be mutable). Expose get root() and
getters for the cluster; keep behavior thin — it composes, it doesn't re-implement
entity logic.class FooCollection extends Collection<Foo>, super(items) in the
constructor, then add named domain queries (firstMemberWithRole(roles)) on top of
the inherited Array methods. Return null/undefined per the project's nullable
convention, not a thrown error, for "not found".See ${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/aggregate.ts.
*Error
hierarchy), never bare throw new Error('msg') in business-meaningful paths — so
callers and tests can match the type. The profile names the hierarchy and where it
lives. Tests assert with rejects/toThrow(new SpecificError(...)).If the project has codegen (profile §6 — e.g. plop: <<run command>>), use it: it
creates the concept directory, the class file, the colocated test, the per-concept
index.ts, and appends to the top-level barrel. The generated class throws
Not implemented from the constructor — replace that with the real design (§5/§6).
Otherwise follow the conventions by hand (profile §5): one PascalCase directory per
concept, the class file named after it, a per-concept index.ts re-exporting the
class (and its enums/types), and a line added to the layer's top-level barrel.
The recipe (no mocks — instantiate the real object):
Foo.ts → Foo.test.ts, profile §8) and name the suite
describe(Foo.name, …) — use .name, not a string literal.UPPER_SNAKE_CASE (ids, dates, primitives). Build a shared props
object for entities; pass inline literals for VOs.toEqual; assert equals() both ways.snapshot (frozen shape) and that toJSON() deep-equals
it; for entities assert toJSON() unwraps value objects to primitives
(toStrictEqual({ id: UID.value, … })).it.each([...]) / describe.each([...]) over [input, expected] tuples.TestableX (profile names it) that supplies the abstract member, then assert the
base behavior.new Foo(props) for the entity's own test.Use toStrictEqual for value/shape equality and it.each/describe.each for tables.
See ${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/{value-object,entity,aggregate}.test.ts.
Domain objects are constructed in tests through Builders (a.k.a. fakes): object literals that fill sensible faker-random defaults and accept partial overrides. This is what the user means by "mocks used by other layers" — usecase/handler/repository tests across the monorepo import these instead of hand-assembling entities.
Contract (profile §7 names the file + alias):
interface Builder<Props, Model> { build(overrides?: Partial<Props>): Model }
interface ManyBuilder<Props, Model> extends Builder<Props, Model> {
buildMany(count: number, overrides?: Partial<Props>): Model[]
}
Canonical implementation — defaults object, override-merge, real constructor:
export const FooBuilder: ManyBuilder<FooProps, Foo> = {
build: (model) => {
const storage: FooProps = { /* every field, faker defaults */ }
return new Foo({ ...storage, ...model }) // overrides win
},
buildMany: (n, overrides) => [...Array(n)].map(() => FooBuilder.build(overrides)),
}
Rules:
const NOW = new Date() once so timestamps
don't drift within a build.root: UserBuilder.build()), overridable via the override-merge.@fake-data cross-package, a local ~/tests/fakes in-package).See ${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/builder.ts.
These are the full, worked form of the procedures above (§5–§7, §10) — read the relevant one before designing or testing rather than inlining a skeleton here.
Generic, library-neutral templates (read first):
${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/value-object.ts / ${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/value-object.test.ts${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/entity.ts / ${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/entity.test.ts${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/aggregate.ts / ${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/aggregate.test.ts${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/builder.tsProject profile (owned by ddd:create-profile):
${CLAUDE_PLUGIN_ROOT}/skills/create-profile/references/templates/entity-design.md — blank fill-in template (the portability seam).${CLAUDE_PLUGIN_ROOT}/skills/create-profile/references/examples/audora/entity-design.md — a fully worked profile (@audora/entities).Worked examples for the Audora profile (real files, .txt-suffixed):
${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/examples/audora/ObjectId.ts.txt / .test.ts.txt — legacy scalar VO + table tests.${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/examples/audora/ExternalEvidence.ts.txt / .test.ts.txt — ValueObject with snapshot.${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/examples/audora/User.ts.txt / .test.ts.txt — entity + serialization & describe.each.${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/examples/audora/Member.ts.txt / MemberCollection.ts.txt — aggregate + collection.${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/examples/audora/FileAggregate.test.ts.txt — builder-driven aggregate test.${CLAUDE_PLUGIN_ROOT}/skills/entity-design/references/examples/audora/UserBuilder.ts.txt / MemberBuilder.ts.txt — builder + composed builder.When working in a profiled repo, also open 1–2 real, recent files named in the profile's exemplar list as live patterns to imitate.
Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub alexcristea/over-engineering-plugins --plugin ddd