Domain modeling principles: parse-don't-validate, make invalid states unrepresentable, primitive obsession detection, semantic types, and domain veto authority. Activate when designing types, reviewing code for domain integrity, or when domain review is needed in a TDD cycle.
Value: Communication -- domain types make code speak the language of the business. They turn implicit knowledge into explicit, compiler-verified contracts that humans and AI can reason about.
Teaches how to build rich domain models that prevent bugs at compile time rather than catching them at runtime. Covers primitive obsession detection, parse-don't-validate, making invalid states unrepresentable, and semantic type design. Independently useful for any code review or design task, and provides the principles that domain review checks for in the TDD cycle.
Do not use raw primitives (String, int, number) for domain concepts.
Create types that express business meaning.
Do:
fn transfer(from: AccountId, to: AccountId, amount: Money) -> Result<Receipt, TransferError>
Do not:
fn transfer(from: String, to: String, amount: i64) -> Result<(), String>
When reviewing code, flag every parameter, field, or return type where a primitive represents a domain concept. The fix is a newtype or value object that validates on construction.
Bool-as-state anti-pattern: A bool field whose name describes a domain
state (already_exists, is_initialized, is_published, has_been_reviewed)
is a state machine encoded as a primitive. Two states today become three
tomorrow, and the bool cannot represent the third.
// BAD: bool encodes a two-state machine as a primitive
struct Article { is_published: bool }
// GOOD: enum names the states and extends safely
enum ArticleState { Draft, Published, Archived }
Flag any bool field that answers "what state is this in?" rather than "is this condition true?" The fix is an enum whose variants name the domain states. This check is distinct from "make invalid states unrepresentable" -- that rule catches impossible combinations; this one catches domain concepts hiding inside a boolean.
Validate at the boundary. Use strong types internally. Never re-validate data that a type already guarantees.
# Boundary: parse raw input into domain type
email = Email(raw_input) # raises if invalid
# Interior: trust the type
def send_welcome(email: Email) -> None:
# No need to validate -- Email guarantees validity
If you find validation logic deep inside business logic, it belongs at the construction boundary instead.
Use the type system to make illegal combinations impossible to construct.
Problem -- boolean flags create invalid combinations:
struct User { email: Option<String>, email_verified: bool }
# Can have email_verified=true with email=None
Solution -- encode state in the type:
enum User {
Unverified { email: Email },
Verified { email: Email, verified_at: Timestamp },
}
When reviewing code, ask: "Can this type represent a state that is meaningless in the domain?" If yes, redesign it.
Name types for what they ARE in the domain, not what they are made of.
| Wrong (structural) | Right (semantic) |
|--------------------|------------------|
| NonEmptyString | UserName |
| PositiveInteger | OrderQuantity |
| ValidatedEmail | CustomerEmail |
The test: if two fields have the same structural type, the compiler cannot catch you swapping them. Semantic types prevent this.
// BAD: title and name are both NonEmptyString -- swappable
{ title: NonEmptyString, name: NonEmptyString }
// GOOD: distinct types catch mix-ups at compile time
{ title: UserTitle, name: UserName }
Structural types are useful as building blocks that semantic types wrap. The semantic type adds domain identity; the structural type provides reusable validation.
Every identifier gets its own type. Never use raw String or int for IDs.
struct AccountId(Uuid);
struct UserId(Uuid);
// Compiler catches: transfer(user_id, account_id) won't compile
fn transfer(from: AccountId, to: AccountId, user: UserId) -> Result<(), Error>
Make construction validated and extraction easy.
Display, AsRef, Into or equivalent so
the type is convenient to use. Getting the inner value out should be trivial.Never provide an automatic conversion FROM a primitive -- that bypasses validation and undermines parse-don't-validate.
Use enums with exhaustive match/switch to ensure all cases are handled. Never use a catch-all default for domain states -- it silently swallows new variants.
When reviewing code (whether in a TDD cycle or a standalone review), you have authority to reject designs that violate these principles. When exercising this authority:
email is
String, should be Email type").Do not back down from valid domain concerns to avoid conflict. Do not silently accept designs that violate these principles.
Hard constraints:
[RP] -- if
human explicitly overrides, record the override and the principle it violates.After applying domain modeling principles, verify:
String, int, number) used for domain conceptsIf any criterion is not met, create or refine the domain type before proceeding.
This skill works standalone. For enhanced workflows, it integrates with:
Missing a dependency? Install with:
npx skills add jwilger/agent-skills --skill tdd
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer