domain-model-design
Design the entities, relationships and rules of a system so the model itself forbids invalid states - name things in the domain's own words, state each entity's invariant, decide what owns what, separate entities from value objects, draw aggregate boundaries around what must change together, and make forbidden states unrepresentable or flag them as runtime-guarded.
- Use it when
- Before writing a schema, migrations, ORM models or core types, and when the user says "design the schema", "model this domain", "what entities do we need", "entity or value object", "where does the aggregate boundary go" or "make illegal states unrepresentable". Not for choosing a database engine or deployment architecture, not for naming terms and writing a glossary (see domain-modeling), and not for thin CRUD with only field-level validation.
- What kind it is
- A written method: it lays out how the work is done and what to check along the way.
- Size
- 1,419 words. The whole text loads when the skill fires; only the 125-word description is always loaded.
- Starts with
- Ask for the job it describes.
How it is organised
What’s inside
The skill’s top-level sections, in order. The bar is each section’s share of the 1,419 words, so you can see where the weight sits before reading.
- 1First, is a rich model warranted?98 words
- 2The steps790 words
- 3Report101 words
- 4Worked example: library lending138 words
- 5Red flags86 words
- 6Changing an existing model111 words
The skill itself
Read it before you install it
domain-model-design/SKILL.md · 1,419 wordsDownloadSkill on GitHub ↗--- name: domain-model-design description: Design the entities, relationships and rules of a system so the model itself forbids invalid states - name things in the domain's own words, state each entity's invariant, decide what owns what, separate entities from value objects, draw aggregate boundaries around what must change together, and make forbidden states unrepresentable or flag them as runtime-guarded. Use before writing a schema, migrations, ORM models or core types, and when the user says "design the schema", "model this domain", "what entities do we need", "entity or value object", "where does the aggregate boundary go" or "make illegal states unrepresentable". Not for choosing a database engine or deployment architecture, not for naming terms and writing a glossary (see domain-modeling), and not for thin CRUD with only field-level validation. license: MIT --- # Domain model design A model that permits a state the business forbids is a bug you will keep patching in application code. The model's job is to make illegal states impossible, not merely unlikely. Name the rules before you name the columns or classes. This skill produces a model: entities, relationships, invariants, ownership and forbidden states. It does not produce migrations and it does not touch an existing database. ## First, is a rich model warranted? Apply this skill's full vocabulary only if at least one of these is true: - Business rules span several fields or objects and must hold together (an order's total equals the sum of its lines). - Some objects must change together to keep a rule true (an aggregate candidate). - The same word means different things to different teams or features. - A multi-step process moves things through states. If none is true, the domain is thin. Say so, keep plain records with field-level validation, and stop. Tactical patterns are overhead where there are no invariants to protect. ## The steps Work through these in order. Do not present a model until each step has an answer. ### 1. Name things in the domain's own words List the core entities using the words a domain expert would use. If you are inventing names nobody in the business would recognize, you are modeling your first guess at the solution, not the domain. Go back to the domain's vocabulary. (If the vocabulary itself is disputed, settle it first; see domain-modeling.) ### 2. State each entity's invariant For every entity, write at least one rule that must always be true, as a sentence you could turn into a test: "an order's total equals the sum of its line items", "a copy on loan has exactly one borrower". An entity with no invariant is a bag of fields. Question whether it is really an entity or just attributes of another one. ### 3. Entity or value object | Question | Entity | Value object | |---|---|---| | Has identity that persists as its data changes? | Yes | No | | Compared by | its identity | all of its values | | Mutable? | Yes, through rules it enforces | No; replace it, do not edit it | | Validates itself? | On every state change | On construction | Money, an address, a date range and a discount rate are usually values. A customer, a loan and an order are usually entities. When several value variants share one idea (different kinds of discount), give them the same interface, so callers never branch on the variant's type and a new variant needs no caller changes. ### 4. Ownership and cardinality For every relationship, say which side **owns the lifecycle** (creates and deletes the other) and which only references it, and state the cardinality (1:1, 1:N, N:M). A foreign key says "references", not "owns". Unowned or doubly-owned relationships are where orphan rows and surprise cascade deletes come from. ### 5. Aggregate boundaries An aggregate is a group of objects that must be consistent together, entered through one root. Let boundaries emerge from use cases rather than designing them in advance: 1. Start from an operation a user needs to perform. 2. Find the rule that must hold for it to succeed. 3. Find the smallest set of objects needed to enforce that rule. 4. Choose the root: the one entry point for every change. 5. Reference other aggregates by identifier only, never by direct object reference. If two objects must change together to keep a rule true, they belong in one aggregate. If they can change independently, keep them separate and link them by id. | Sign | Aggregate too large | Aggregate too small | |---|---|---| | Load and contention | Loading is slow, locks collide | Rules break because the data is split | | Transactions | Long ones | Need coordination across parts | | Change | Unrelated changes conflict | Related changes need choreography | ### 6. Copy or reference When one aggregate uses data owned by another, decide whether it needs the **current** value or the value **at the time of the action**. Reference by id for current data (the order's customer). Copy as a value object for historical truth (the shipping address and unit price at the time of purchase), and never update that copy when the source changes. Use copies for anything that would corrupt a past record if it changed retroactively: prices, addresses, terms. ### 7. Forbidden states List the states the domain forbids: "a copy lent to two members at once", "a hold on a copy that is available". For each, check the model: does it still allow that state? If yes, change the model so the state cannot be expressed: a required field, a unique constraint, a variant type, a moved relationship. If it truly cannot be prevented structurally, mark it **runtime-guarded**, name where the guard lives, and say why. Choose this deliberately for each state. "We can validate that in the app layer" is not a default. ### 8. Lifecycle and events For anything with states, write the allowed transitions (state, event, next state). When other parts of the system must react to a change, name the event in the past tense (`OrderPlaced`), make it immutable, and include everything a listener needs and the time it happened. ### 9. Context boundaries The same real-world thing can legitimately be different models in different places: a customer in sales has preferences, a recipient in shipping has an address, a payer in billing has a payment method. Do not force one model to serve all of them. Translate at the edge where one context calls another, so a third-party or neighboring model does not leak into yours. ## Report Present the model as a short list, not a table dump: ``` Model: <system> Entities: <entity: its invariants, one line each> Value objects: <name: what it holds and validates> Relationships: <A to B, cardinality, which side owns the lifecycle> Aggregates: <root: what it keeps consistent; what it references by id> Forbidden states: <state: how the model prevents it, or "runtime-guarded: where, why"> Open questions: <anything unresolved that changes the shape> ``` Say which rules rest only on runtime guards. If core invariants are unclear or ownership is contested, say the model is not ready to build on and resolve that first. ## Worked example: library lending | Step | Result | |---|---| | Entities | Member, Title, Copy, Loan, Hold (a librarian would use these words) | | Invariants | A copy has at most one active loan. A hold exists only for a title with no available copy. | | Ownership | A Title owns its Copies. A Loan references one Member and one Copy and is owned by neither; it is created by the lending operation. | | Forbidden: copy lent twice | Prevent structurally: a uniqueness rule over active loans per copy. | | Forbidden: hold on an available copy | Cannot be fully expressed in the structure. Runtime-guarded in the operation that places a hold; reason stated. | | Copy or reference | The loan stores the due date at checkout (a copy); it references the member by id. | ## Red flags | Thought | Why it is wrong | |---|---| | "I'll just add the obvious fields" | Fields without rules encode nothing; invalid states leak into every query. State the rule first. | | "Ownership is obvious from the foreign key" | A foreign key shows reference, not who deletes whom. | | "It's a simple domain" | Simple domains still forbid states. Name three and check them. | | "The app layer will catch it" | Sometimes true. Decide it per state, on purpose. | ## Changing an existing model When the model already exists and holds data, tightening a rule (a new unique constraint, a required field, a removed state) can fail or lose data. Check whether existing rows already violate the new rule before proposing the change, and propose the change as a plan for the user. Run migrations or alter stored data only when the user asks. If you are splitting one large model into contexts, go in small steps: put context-specific interfaces in front of the old model first, extract the simplest context first, and keep each step independently testable and releasable. If a step needs a big-bang cutover, the boundary is probably in the wrong place.
The second audit
What we found in the sources
It joins two same-named skills that cover one job, designing the structure of a domain or data model, from different depths: xd4o/praxis (invariants first, ownership, make illegal states unrepresentable) and elct9620/ai-coding-skills (entity, value object, aggregate, bounded context, domain event). The glossary-and-ADR skills in the same bundle are a different job and are in the domain-modeling skill. Every source below was read at the commit the auditor recorded. Nothing was taken from a source without a permissive license.
What each source contributed
- xd4o/praxis ↗ · Primary basis · MIT
The order of work (name entities in the domain's own words, state an invariant for every entity, assign lifecycle ownership and cardinality to every relationship, make forbidden states unrepresentable or flag them as runtime-guarded); the point that a foreign key shows reference, not ownership; the red-flags list ('I'll just add the obvious fields', 'it's a simple domain', 'the app layer will catch it'); the report fields; and the library-lending rules used in the worked example (a copy lent to one member at a time, a hold on an available copy).
- elct9620/ai-coding-skills ↗ · Added ideas · Apache-2.0
The applicability check for whether a rich model is warranted; the entity versus value-object comparison; aggregate design from a use case to the smallest set that enforces one rule, with a root, and other aggregates referenced by id only; the too-large and too-small sizing signs; reference versus snapshot for data owned by another aggregate; variants of a value object sharing one interface; past-tense immutable domain events; the same real-world thing being different models in different contexts; and the incremental steps for splitting a monolithic model (condensed).
- xu91102/claude-everything-Workflow ↗ · Added ideas · MIT
Writing lifecycle as state, event, next state, and keeping unresolved consequential decisions as an explicit list in the output.
What changed
- The description keeps the situations xd4o/praxis lists (design the schema, model this domain, what entities do we need) and adds entity-or-value-object, aggregate boundary and 'make illegal states unrepresentable', plus a not-for clause for database-engine choice, glossary work and thin CRUD. The upstream descriptions are 'MANDATORY modeling protocol. You MUST invoke...' (xd4o) and 'Make sure to use this skill whenever the user works on business rules... even for simple CRUD features' (elct9620).
- Removed the mandatory, all-caps and custom HARD-GATE framing. The one gate that mattered is kept in plain words: do not present a model until each step has an answer.
- Joined the two sources into one flow and ordered it so aggregate boundaries come after ownership and before the forbidden-states check.
- Made it language-neutral. The Ruby examples are gone; one worked example (library lending) is a table written for this skill.
- Replaced xd4o's HIGH/MEDIUM/LOW confidence line with 'say which rules rest only on runtime guards, and say plainly if the model is not ready'. The source gives no basis for how the confidence rating is assigned.
- Replaced the box-drawing report tree with a plain list.
- Added a 'Changing an existing model' paragraph: check whether existing rows violate a new rule, propose the change as a plan, and run migrations or alter stored data only when asked. This is an addition by the rewrite; neither source says it. The monolith-splitting steps in that paragraph are elct9620's, condensed.
- States up front that the output is a model, not migrations.
Problems found in the sources
- xd4o/praxis: The description and the Handoff section both route to a skill named architecture-reasoning, which is not in the bundle. It is written in mandatory language (EXTREMELY_IMPORTANT, MANDATORY, 'Do not skip steps. Do not combine steps. Do not summarize') and wraps its checklist in a custom <HARD-GATE> tag, which is not markup most hosts render. The report ends with a HIGH, MEDIUM or LOW confidence rating with a short definition of each but no stated basis; whether that is useful is unclear. Its core order of work (invariants before columns, ownership, forbidden states) is sound and compact.
- elct9620/ai-coding-skills: Examples are Ruby only. The 'Related Skills' section sends the reader to skills named schema, design-patterns and architecture, none of which are in the bundle. The description says to use it 'even for simple CRUD features that involve domain invariants', while the body's applicability rubric says thin, CRUD-like domains should keep plain objects; the two pull in different directions (unclear whether deliberate). In its Money example the error message says 'amount must be positive' but the check is 'amount < 0', which allows zero. At about 16.7 KB it repeats the building-block points in a comparison table and again in four pass/fail tables. It also prescribes a directory layout, which is a project convention, not a modeling rule.
- xu91102/claude-everything-Workflow: Mixes glossary work and structural modeling, and depends on docs/agent-workflow/project-context.md, a router and Spec Gate, none of which are in the bundle. Only its lifecycle and open-decisions output format was used here; the rest is in the domain-modeling skill.
What was dropped
- All Ruby code (value-object, entity and event classes, polymorphic discount example).
- elct9620's four pass/fail completion tables, the 'Related Skills' and 'Relationship to architecture' sections that point to skills named schema, design-patterns and architecture, the directory-naming guidance (single domain folder versus per-context folders) and its anti-pattern table, the context-mapping pattern table (shared kernel, customer-supplier, published language; only the idea of translating at the edge was kept), and its ASCII diagrams.
- xd4o's Handoff section naming architecture-reasoning, and the EXTREMELY_IMPORTANT and HARD-GATE wrapper text.
Read but not used (11)
- mattpocock/skills: skills/engineering/domain-modeling/SKILL.md: glossary-and-ADR skill that the other copies derive from. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- rengwu/wayfinder-maps: skills/domain-modeling/SKILL.md: byte-identical copy of the mattpocock file (same SHA-256). A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- leobbaroni/cockpit: skills/domain-modeling/SKILL.md: byte-identical copy of the mattpocock file (same SHA-256). A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- the-inconvenience-store/skills: skills/domain-modeling/SKILL.md: the mattpocock text with a different description line. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- Bossmann007/cursor-kit: skills/domain-modeling/SKILL.md: the mattpocock text with dashes reworded. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- CapitalCantrip/quality-gates: .claude/skills/domain-modeling/SKILL.md: the mattpocock text with CONTEXT.md renamed GLOSSARY.md and dashes reworded. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- helderberto/agent-skills: skills/domain-modeling/SKILL.md: condensed glossary skill. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- dougborg/harness-kit: claude-skills/domain-modeling/SKILL.md: rewritten glossary-and-ADR skill. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- mascah/skills: skills/domain-modeling/SKILL.md: short glossary-and-ADR skill. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- marinvch/Cortex: skills/domain-modeling/SKILL.md: expanded glossary-and-ADR skill adapted from mattpocock. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
- RollRollRoll/agent-toolkit: skills/development/domain-modeling/SKILL.md: Chinese translation of the mattpocock glossary text. A different job (naming terms, glossary, ADRs); read, and used in the domain-modeling skill, not here.
Left out for license (2)
No license is declared on these, so nothing was taken from them: Camgineer/cstack, oribarilan/97.
Same text, wherever you work
Use it where you work
Copy the folder into your skills directory, then ask for the job or type its start command.
- Download
SKILL.mdwith the button above, and put it in a folder nameddomain-model-design. - Zip the folder.
- In Claude, open Settings → Skills and upload the zip.
SKILL.md is plain Markdown. Copy the text and paste it as the system prompt or the first message. It will not trigger by itself there; you start it.