nWave/skills/nw-fp-domain-modeling/SKILL.md
Domain modeling with algebraic data types, smart constructors, and type-level error handling
npx skillsauth add nwave-ai/nwave nw-fp-domain-modelingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Domain modeling with types. Make illegal states unrepresentable, workflows as pipelines, error handling at the type level.
Cross-references: fp-principles | fp-hexagonal-architecture | fp-algebra-driven-design
[STARTER]
All domain types compose from two operations:
Combined recursively, these express virtually any domain structure.
[STARTER]
Never use primitives directly in the domain model. Each domain concept gets its own wrapper type.
What: Wrap primitives so the compiler distinguishes CustomerId from OrderId. When: Every primitive with domain meaning. Why: Prevents accidental mixing (compiler rejects comparing CustomerId with OrderId). Each wrapper carries its own validation rules. The type name IS the documentation.
[STARTER]
Raw constructor is private. A create function validates input and returns a Result type, making validation failure explicit.
Pattern: UnitQuantity must be between 1 and 1000. Its create function rejects values outside that range. A companion value function provides read access to the inner primitive.
When: Every domain wrapper with validation rules. Why: Once constructed, a value is guaranteed valid. No defensive checks deeper in the code.
[STARTER]
The central design guideline. Instead of flags and runtime checks, model the domain so invalid states cannot be constructed.
Instead of { EmailAddress; IsVerified: bool }, create separate VerifiedEmailAddress and UnverifiedEmailAddress types. Functions requiring verification take VerifiedEmailAddress, making misuse a compile error.
Instead of { Email: option; Address: option } (where both could be None), create: EmailOnly | AddressOnly | EmailAndAddress. The "at least one required" rule becomes structurally enforced.
Define a type guaranteeing at least one element. Order with OrderLines: NonEmptyList<OrderLine> cannot have zero lines.
[STARTER]
Every business workflow is a single function: Command in, Events out.
PlaceOrderWorkflow : PlaceOrderCommand -> AsyncResult<PlaceOrderEvent list>
Workflows decompose into steps, each transforming one document type into the next:
UnvalidatedOrder -> ValidatedOrder -> PricedOrder -> Events
Each step is stateless, pure, has single input/output type, and is independently testable. The workflow assembles by piping steps together.
Why: Pipeline makes the business process visible. Each step name is a domain concept.
[INTERMEDIATE]
Rather than one Order type with flags, create separate types for each lifecycle stage:
UnvalidatedOrder (raw input, all fields strings)ValidatedOrder (all fields checked)PricedOrder (prices calculated)A top-level Order choice type unifies all states. New states (e.g., Refunded) added without breaking existing code.
When: Domain entities with distinct lifecycle stages where different data is available at each stage.
[INTERMEDIATE]
When an entity has distinct states with different data and different allowed operations, model each state as a separate type.
ShoppingCart = EmptyCart | ActiveCart of ActiveCartData | PaidCart of PaidCartData
Transition functions take the choice type, pattern-match on current state, return new state.
Benefits: All states explicit | each state has own data | invalid transitions prevented by types | pattern matching warnings reveal unhandled edge cases.
[INTERMEDIATE]
Each function returns a Result type. Pipeline short-circuits on first failure.
rawInput
|> validateOrder -- Result<ValidOrder, Error>
|> bind calculateTotal -- Result<PricedOrder, Error>
|> bind checkInventory -- Result<ConfirmedOrder, Error>
|> bind chargePayment -- Result<PaidOrder, Error>
|> map generateReceipt -- Result<Receipt, Error>
Key combinators:
| Category | Examples | Strategy | |---|---|---| | Domain Errors | Validation failure, out of stock | Model as types, return via Result | | Panics | Out of memory, null reference | Throw exceptions, catch at top level | | Infrastructure Errors | Network timeout, auth failure | Case-by-case |
Each step may have its own error type. Define a common error choice type and use mapError to lift step errors before composing.
[ADVANCED]
Standard bind short-circuits on first error. For validation where you want ALL errors, use Applicative style -- runs all validations and accumulates errors into a list. See fp-principles section 5.
When: Form validation | batch input checking | any place user needs all errors at once.
[INTERMEDIATE]
Each workflow step declares exactly the functions it needs as parameters:
CheckProductCodeExists : ProductCode -> bool
GetProductPrice : ProductCode -> Price
Convention: Dependencies first in parameter list, primary input last. Enables partial application (functional DI).
Public vs. internal: Top-level workflow hides dependencies from callers. Internal steps make them explicit.
See fp-hexagonal-architecture section 5 for full DI decision tree.
[INTERMEDIATE]
Domain model has no awareness of databases. Two distinct type hierarchies:
JSON -> deserialize -> DTO -> toDomain -> [WORKFLOW] -> fromDomain -> DTO -> serialize -> JSON
Domain workflow never sees JSON or DTOs directly.
[ADVANCED]
Each context has its own dialect of domain language. Contexts communicate only through events and DTOs. Design for autonomy.
Trust boundaries: Input gate validates and converts incoming DTOs to domain types. Output gate converts domain types to DTOs, deliberately dropping private information.
Inter-context relationships: Shared Kernel (shared design) | Customer/Supplier (downstream defines contract) | Anti-Corruption Layer (translator preventing external model from corrupting internal domain).
Is it a simple value with validation rules?
YES --> Domain Wrapper + Smart Constructor
NO -->
Does it represent one of several alternatives?
YES --> Choice Type (sum type)
NO -->
Does it group several values together?
YES --> Record Type (product type)
NO -->
Does it have distinct lifecycle stages?
YES --> State Machine with Types (section 7)
NO -->
Is it an operation that transforms data through stages?
YES --> Workflow Pipeline (section 5)
NO --> Evaluate whether it needs modeling at all
testing
Acceptance test creation methodology for the DISTILL wave. Domain knowledge for the acceptance designer agent: port-to-port principle, prior wave reading, wave-decision reconciliation, graceful degradation, and document back-propagation.
testing
Methodology for minimizing test count while maximizing behavioral coverage - behavior definition, anti-pattern catalog, consolidation patterns, stopping criterion, coverage-preserving validation
testing
Methodology for minimizing test count while maximizing behavioral coverage - behavior definition, anti-pattern catalog, consolidation patterns, stopping criterion, coverage-preserving validation
development
Design mandates for acceptance tests - hexagonal boundary, business language abstraction, user journey completeness, pure function extraction, 3 Pillars (domain language / chained narrative / production composition), and the layered ATD discipline (Universe-bound assertion, layer-dependent PBT mode, two-tier acceptance, example-based sad paths)