Domain modeling with algebraic data types, smart constructors, and type-level error handling
npx skills add https://github.com/nWave-ai/nWave --skill nw-fp-domain-modeling
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
Integration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Take nwave-ai/nw-fp-domain-modeling from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.