/nw-fp-domain-modeling
Domain modeling with algebraic data types, smart constructors, and type-level error handling
Claude Code
$ npx -y skills add nWave-ai/nWave --skill nw-fp-domain-modeling --agent claude-code
Codex
$ npx skills add nWave-ai/nWave --skill nw-fp-domain-modeling -a codex -y
Cursor
$ npx skills add nWave-ai/nWave --skill nw-fp-domain-modeling -a cursor -y
Gemini CLI
$ npx skills add nWave-ai/nWave --skill nw-fp-domain-modeling -a gemini-cli -y
GitHub Copilot
$ npx skills add nWave-ai/nWave --skill nw-fp-domain-modeling -a github-copilot -y
OpenCode
$ npx skills add nWave-ai/nWave --skill nw-fp-domain-modeling -a opencode -y
Windsurf
$ npx skills add nWave-ai/nWave --skill nw-fp-domain-modeling -a windsurf -y
SKILL.md
nw-fp-domain-modeling.SKILL.mdname: nw-fp-domain-modeling
agent: nw-functional-software-crafter
description: Domain modeling with algebraic data types, smart constructors, and type-level error handling
user-invocable: false
disable-model-invocation: true
FP Domain Modeling
Domain modeling with types. Make illegal states unrepresentable, workflows as pipelines, error handling at the type level.
Cross-references: [fp-principles](../nw-fp-principles/SKILL.md) | [code-craftsmanship: FP Hexagonal Architecture](../nw-code-craftsmanship/SKILL.md) | [fp-algebra-driven-design](../nw-fp-algebra-driven-design/SKILL.md)
---
1. The Two Building Blocks
[STARTER]
All domain types compose from two operations:
- **AND (Record Types)**: Value has ALL of these fields. Order requires CustomerInfo AND ShippingAddress AND OrderLines.
- **OR (Choice Types)**: Value is ONE OF these alternatives. ProductCode is either WidgetCode OR GizmoCode.
Combined recursively, these express virtually any domain structure.
---
2. Domain Wrappers for Primitives
[STARTER]
Wrap a primitive when a demonstrated invalid state or a mixing risk exists that no native type and no existing invariant owner already excludes. Keep the native type when it suffices (`nw-code-craftsmanship` owns this sufficiency rule).
**What**: A wrapper lets the compiler distinguish CustomerId from OrderId. **When**: Two same-typed domain values can be confused at a real call site, or the value carries a validation rule with no existing owner. Never for template conformance alone. **Why**: Prevents accidental mixing (compiler rejects comparing CustomerId with OrderId). Each such wrapper carries its own validation rules. The type name IS the documentation.
---
3. Validated Construction (Smart Constructors)
[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.
---
4. Making Illegal States Unrepresentable
[STARTER]
The central design guideline. Instead of flags and runtime checks, model the domain so invalid states cannot be constructed.
Replace Flags with Distinct Types
Instead of `{ EmailAddress; IsVerified: bool }`, create separate `VerifiedEmailAddress` and `UnverifiedEmailAddress` types. Functions requiring verification take `VerifiedEmailAddress`, making misuse a compile error.
Replace Optional Fields with Choice Types
Instead of `{ Email: option; Address: option }` (where both could be None), create: `EmailOnly | AddressOnly | EmailAndAddress`. The "at least one required" rule becomes structurally enforced.
Normalize admitted optional wire forms once into `Absent | Present Work`; let operations consume that alternative rather than re-inspect raw keys. Setting absent work constructs it; clearing absent work returns its declared result. Every admitted form has a defined Result; malformed present work is an error, not absence. Missing/null/empty are equivalent only where the contract says so. Basis: Sandy Maguire, *Thinking with Types*, The Algebra Behind Types → Sum, Product and Exponential Types / Canonical Representations; *Certainty by Construction*, An Exploration of Numbers → Constructing Evidence.
NonEmptyList for "At Least One" Rules
Define a type guaranteeing at least one element. Order with `OrderLines: NonEmptyList<OrderLine>` cannot have zero lines.
---
5. Workflows as Functions
[STARTER]
Every business workflow is a single function: Command in, Events out.
PlaceOrderWorkflow : PlaceOrderCommand -> AsyncResult<PlaceOrderEvent list>
Pipeline Composition
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.
For mixed validation dependencies, load `nw-fp-principles` §5 rather than require whole-document success too early. For a zero-construction-on-refusal contract, load `nw-certainty-by-construction`: independently validate customer/items, price validated items, then allocate/render the ready order. A later invalid operation cannot follow an already-issued order ID. These examples apply those owners' composition and semantic-closure laws, not a new mandatory pipeline framework.
---
6. Document Lifecycle as State Types
[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.
---
7. State Machines with Types
[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.
---
8. Error-Track Pipelines (Railway Pattern)
[INTERMEDIATE]
Each function returns a Result type. Pipeline short-circuits on first failure.
rawInput
|> va
Read more
name: nw-fp-domain-modeling agent: nw-functional-software-crafter description: Domain modeling with algebraic data types, smart constructors, and type-level error handling user-invocable: false disable-model-invocation: true
FP Domain Modeling
Domain modeling with types. Make illegal states unrepresentable, workflows as pipelines, error handling at the type level.
Cross-references: [fp-principles](../nw-fp-principles/SKILL.md) | [code-craftsmanship: FP Hexagonal Architecture](../nw-code-craftsmanship/SKILL.md) | [fp-algebra-driven-design](../nw-fp-algebra-driven-design/SKILL.md)
---
1. The Two Building Blocks
[STARTER]
All domain types compose from two operations:
- **AND (Record Types)**: Value has ALL of these fields. Order requires CustomerInfo AND ShippingAddress AND OrderLines.
- **OR (Choice Types)**: Value is ONE OF these alternatives. ProductCode is either WidgetCode OR GizmoCode.
Combined recursively, these express virtually any domain structure.
---
2. Domain Wrappers for Primitives
[STARTER]
Wrap a primitive when a demonstrated invalid state or a mixing risk exists that no native type and no existing invariant owner already excludes. Keep the native type when it suffices (`nw-code-craftsmanship` owns this sufficiency rule).
**What**: A wrapper lets the compiler distinguish CustomerId from OrderId. **When**: Two same-typed domain values can be confused at a real call site, or the value carries a validation rule with no existing owner. Never for template conformance alone. **Why**: Prevents accidental mixing (compiler rejects comparing CustomerId with OrderId). Each such wrapper carries its own validation rules. The type name IS the documentation.
---
3. Validated Construction (Smart Constructors)
[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.
---
4. Making Illegal States Unrepresentable
[STARTER]
The central design guideline. Instead of flags and runtime checks, model the domain so invalid states cannot be constructed.
Replace Flags with Distinct Types
Instead of `{ EmailAddress; IsVerified: bool }`, create separate `VerifiedEmailAddress` and `UnverifiedEmailAddress` types. Functions requiring verification take `VerifiedEmailAddress`, making misuse a compile error.
Replace Optional Fields with Choice Types
Instead of `{ Email: option; Address: option }` (where both could be None), create: `EmailOnly | AddressOnly | EmailAndAddress`. The "at least one required" rule becomes structurally enforced.
Normalize admitted optional wire forms once into `Absent | Present Work`; let operations consume that alternative rather than re-inspect raw keys. Setting absent work constructs it; clearing absent work returns its declared result. Every admitted form has a defined Result; malformed present work is an error, not absence. Missing/null/empty are equivalent only where the contract says so. Basis: Sandy Maguire, *Thinking with Types*, The Algebra Behind Types → Sum, Product and Exponential Types / Canonical Representations; *Certainty by Construction*, An Exploration of Numbers → Constructing Evidence.
NonEmptyList for "At Least One" Rules
Define a type guaranteeing at least one element. Order with `OrderLines: NonEmptyList<OrderLine>` cannot have zero lines.
---
5. Workflows as Functions
[STARTER]
Every business workflow is a single function: Command in, Events out.
PlaceOrderWorkflow : PlaceOrderCommand -> AsyncResult<PlaceOrderEvent list>
Pipeline Composition
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.
For mixed validation dependencies, load `nw-fp-principles` §5 rather than require whole-document success too early. For a zero-construction-on-refusal contract, load `nw-certainty-by-construction`: independently validate customer/items, price validated items, then allocate/render the ready order. A later invalid operation cannot follow an already-issued order ID. These examples apply those owners' composition and semantic-closure laws, not a new mandatory pipeline framework.
---
6. Document Lifecycle as State Types
[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.
---
7. State Machines with Types
[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.
---
8. Error-Track Pipelines (Railway Pattern)
[INTERMEDIATE]
Each function returns a Result type. Pipeline short-circuits on first failure.
rawInput |> va
AI agents that guide you from idea to working code, with human judgment at every gate. nWave runs inside Claude Code. It breaks feature delivery into seven waves (discover, diverge, discuss, design, devops, distill, deliver).
Other skills on nwave.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about nw-fp-domain-modeling.
- Which coding agents can use nw-fp-domain-modeling?
- nw-fp-domain-modeling installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

