
Domain Driven Design
- 63 installs
- 52 repo stars
- Updated March 4, 2026
- bfollington/terma
domain-driven-design is a Claude Code skill that guides type-driven and data-driven domain modeling based on Rich Hickey's and Scott Wlaschin's principles.
About
domain-driven-design is a Claude Code skill that guides domain modeling using Rich Hickey's data-oriented design and Scott Wlaschin's type-driven design. A developer uses it when designing types, modeling business domains, or refactoring domain logic. It helps make illegal states unrepresentable and builds a ubiquitous language, and it can produce Mermaid, Graphviz/DOT, and ASCII diagrams to communicate the model.
- Guides type-driven and data-driven domain modeling
- Applies Rich Hickey and Scott Wlaschin design principles
- Produces Mermaid, Graphviz/DOT and ASCII domain diagrams
Domain Driven Design by the numbers
- 63 all-time installs (skills.sh)
- Ranked #3,136 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
domain-driven-design capabilities & compatibility
- Capabilities
- refactoring · documentation · api development
- Use cases
- refactoring · documentation · api development
What domain-driven-design says it does
It provides specialized guidance for type-driven and data-driven design based on Rich Hickey and Scott Wlaschin's principles.
Focus on building systems that make illegal states unrepresentable, prioritize data and transformations over objects and methods
Use this skill when designing types, modeling business domains, refactoring domain logic, or ensuring domain consistency across a codebase.
npx skills add https://github.com/bfollington/terma --skill domain-driven-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 63 |
|---|---|
| repo stars | ★ 52 |
| Last updated | March 4, 2026 |
| Repository | bfollington/terma ↗ |
What it does
Model a business domain with type-driven design so illegal states are unrepresentable.
Who is it for?
Designing domain types, aggregates, and bounded contexts for a business system.
When should I use this skill?
You are designing types, modeling a business domain, or refactoring domain logic.
What you get
A consistent domain model with types that make illegal states unrepresentable and clear ubiquitous language.
- domain model
- type definitions
- Mermaid/Graphviz/ASCII diagrams
By the numbers
- Draws on 5 core Hickey principles and multiple Wlaschin type-driven patterns
Files
Domain-Driven Design
Overview
This skill provides guidance for domain modeling based on Rich Hickey's data-oriented design principles and Scott Wlaschin's type-driven design approach. Focus on building systems that make illegal states unrepresentable, prioritize data and transformations over objects and methods, and establish a ubiquitous language that bridges technical implementation and business domain.
Core Principles
Rich Hickey's Data-Oriented Design
Simplicity over Ease
- Favor simple constructs that can be understood independently
- Avoid complecting (intertwining) unrelated concerns
- Separate policy from mechanism, data from behavior
Data is King
- Model the domain using pure data structures, not objects with behavior
- Prefer generic data structures (maps, sets, vectors) over custom classes when appropriate
- Data should be self-describing and inspectable
- Functions transform data; data does not execute behavior
Value of Values
- Use immutable values to represent facts
- Values enable local reasoning and simple equality
- Values can be freely shared without coordination
- Consider: what are the immutable facts in this domain?
Decomplecting
- Identify what is truly essential to the domain vs. incidental complexity
- Separate when-it-happens from what-happens
- Separate mechanism from policy
- Question: are these concerns actually separate, or have we tangled them?
Scott Wlaschin's Type-Driven Design
Make Illegal States Unrepresentable
- Use the type system to eliminate invalid states at compile time
- Model optional values explicitly (Option/Maybe types)
- Use sum types (discriminated unions) for states that are mutually exclusive
- Avoid primitive obsession; create domain-specific types
Domain Modeling Made Functional
- Model workflows as data transformations: Input → Process → Output
- Explicitly model business rules as functions
- Separate validation from business logic
- Think in terms of: What can happen? What are the valid transitions?
Railway-Oriented Programming
- Model success and failure paths explicitly (Result types)
- Chain operations that can fail using bind/flatMap
- Keep the happy path clear and linear
- Handle errors at appropriate boundaries
Types as Documentation
- Type signatures should communicate intent
- Use newtype wrappers for semantic clarity (UserId, EmailAddress, Timestamp)
- Constrain inputs to valid ranges using types
- Let the type system guide API design
DDD Building Blocks
Entities vs Value Objects
Entities are defined by identity, not attributes:
- Have a unique identifier (ID, account number, etc.)
- Can change over time while maintaining identity
- Two entities with same attributes but different IDs are distinct
- Used when domain experts refer to things by name/ID
Value Objects are defined entirely by attributes:
- No unique identifier
- Immutable
- Two value objects with same attributes are interchangeable
- Used when only the value matters, not identity
Decision Guide:
- Ask: Do domain experts refer to this by ID/name? → Entity
- Ask: Can I replace it with an equivalent copy? → If yes: Value Object
Aggregates and Aggregate Roots
Aggregate: A cluster of entities and value objects treated as a single unit for data changes.
Aggregate Root: The single entity through which all external access to the aggregate must pass.
Purpose:
- Define transactional consistency boundaries
- Enforce invariants that span multiple objects
- Simplify the model by grouping related concepts
Rules:
- External references go only to the aggregate root (use ID references)
- Root enforces all invariants for the entire aggregate
- Transactions don't cross aggregate boundaries (use eventual consistency)
- Keep aggregates small for better performance and scalability
When NOT to create an aggregate:
- Objects can be modified independently
- No shared invariants requiring transactional consistency
- Different objects have different lifecycles
Bounded Contexts
Definition: An explicit boundary within which a domain model applies.
Purpose:
- Divide large domains into manageable pieces
- Allow same term to have different meanings in different contexts
- Prevent model corruption from mixing incompatible concepts
Key Insight: Ubiquitous language is only ubiquitous within a context. "Customer" in Sales context may be different from "Customer" in Shipping context.
When modeling:
- Identify which bounded context you're in
- Make context boundaries explicit in code structure (separate modules/namespaces)
- Use anti-corruption layers when integrating across contexts
- Document relationships between contexts (context map)
Domain Events
Definition: Something important that happened in the domain.
Characteristics:
- Named in past tense (OrderPlaced, PaymentProcessed, UserRegistered)
- Immutable facts
- Domain experts care about them
- Can trigger reactions within or across bounded contexts
Uses:
- Decouple domain logic
- Enable eventual consistency between aggregates
- Integration between bounded contexts
- Event sourcing (store events as source of truth)
Repositories
Purpose: Provide illusion of an in-memory collection of aggregates, abstracting persistence.
Characteristics:
- Operate at aggregate boundaries (load/save whole aggregates)
- Provide lookup by ID
- Hide database implementation details
- Return domain entities, not database rows
Pattern: Application layer uses repository to get/save aggregates; domain layer remains pure.
Domain Modeling Workflow
1. Discover the Ubiquitous Language
Start by identifying the domain concepts, using terminology from domain experts:
Action Items:
- List nouns (entities, value objects) and verbs (operations, events) from the domain
- Document domain terms with precise definitions
- Identify synonyms and resolve ambiguity
- Ask: What does the business call this? What are the boundaries of this concept?
Output Format: Create a glossary section documenting each term:
**Term** (Type: Entity/ValueObject/Event/Command)
- Definition: [Clear, domain-expert-approved definition]
- Examples: [Concrete examples]
- Invariants: [Rules that must always hold]2. Analyze the Existing Domain Model
Before making changes, understand the current state:
Exploration Steps:
- Identify where domain concepts are currently modeled (types, schemas, tables)
- Map out relationships between domain entities
- Find where business logic lives (services, functions, stored procedures)
- Document implicit rules and constraints
- Note inconsistencies in naming or modeling
Questions to Answer:
- What types/classes represent domain concepts?
- What are the invariants? Where are they enforced?
- Which concepts are tangled together that should be separate?
- Are there phantom types or states that shouldn't exist?
3. Identify Inconsistencies and Smells
Common problems to surface:
Naming Inconsistencies
- Same concept with different names (User vs Account vs Customer)
- Different concepts with same name (Order as entity vs Order as command)
- Technical names bleeding into domain language (DTO, DAO suffixes)
Structural Problems
- Illegal states being representable (e.g.,
status: "approved" | "rejected"with separateapproved_atandrejected_atfields that can both be set) - Primitive obsession (strings for email, numbers for money)
- Optional fields that are actually required in certain states
- Null/undefined used to represent multiple distinct states
Complected Concerns
- Domain logic mixed with infrastructure (DB access in business logic)
- Multiple responsibilities in one type/module
- Temporal coupling (must call A before B or system breaks)
Missing Concepts
- Domain concepts that exist in conversations but not in code
- Implicit states that should be explicit
- Business rules enforced through comments or conventions rather than types
4. Design the Domain Model
Apply type-driven and data-driven principles:
Data Modeling:
- Start with the data shape; what are the facts?
- Use immutable values for facts that don't change
- Model state transitions explicitly
- Separate identity from attributes
- Consider: what varies together? What varies independently?
Type Design:
- Create sum types for mutually exclusive states:
type PaymentStatus =
| Pending
| Approved { approvedAt: Timestamp, approvedBy: UserId }
| Rejected { rejectedAt: Timestamp, reason: string }- Use product types to ensure all required data is present
- Create semantic wrappers for primitives:
type EmailAddress = EmailAddress of string // with validation
type Money = { amount: Decimal, currency: Currency }- Make impossible states unrepresentable
Workflow Modeling:
- Model each business workflow as a clear pipeline:
ValidateInput → ExecuteBusinessLogic → HandleResult → Persist → Notify- Identify decision points and model them explicitly
- Separate pure business logic from effects (IO, time, randomness)
- Use clear function signatures that document intent
5. Build and Maintain Ubiquitous Language
Consistency Rules:
- Use identical terminology in code, documentation, conversations, and UI
- When domain language changes, update all representations
- Avoid technical jargon in domain code (no "factory", "manager", "handler" unless domain terms)
- Resist the temptation to rename domain concepts for technical convenience
Code Conventions:
- Domain types should mirror domain language exactly
- Function names should use domain verbs
- Module boundaries should follow domain boundaries
- Comments should explain domain rules, not implementation details
Documentation:
- Keep the glossary up to date
- Document why decisions were made (especially constraints and invariants)
- Link code to domain documentation
- Make implicit domain rules explicit
6. Visualize the Domain Model
Use diagrams to communicate domain structure and relationships:
Mermaid for Relationships:
classDiagram
Order --> Customer
Order --> OrderLine
OrderLine --> Product
Order --> PaymentStatus
class Order {
+OrderId id
+CustomerId customerId
+List~OrderLine~ lines
+PaymentStatus status
}
class PaymentStatus {
<<enumeration>>
Pending
Approved
Rejected
}Mermaid for Workflows:
graph LR
A[Receive Order] --> B{Valid?}
B -->|Yes| C[Calculate Total]
B -->|No| D[Return Validation Error]
C --> E[Process Payment]
E --> F{Payment Success?}
F -->|Yes| G[Fulfill Order]
F -->|No| H[Cancel Order]Mermaid for State Transitions:
stateDiagram-v2
[*] --> Draft
Draft --> Submitted: submit()
Submitted --> Approved: approve()
Submitted --> Rejected: reject()
Approved --> Fulfilled: fulfill()
Fulfilled --> [*]
Rejected --> [*]Graphviz/DOT for Complex Relationships:
digraph domain {
rankdir=LR;
node [shape=box];
Customer -> Order [label="places"];
Order -> OrderLine [label="contains"];
OrderLine -> Product [label="references"];
Order -> Payment [label="requires"];
Payment -> PaymentMethod [label="uses"];
}ASCII for Quick Sketches:
Customer
└─> Order (1:N)
├─> OrderLine (1:N)
│ └─> Product
└─> Payment (1:1)
└─> PaymentMethodWhen to Use Each:
- Mermaid classDiagram: Entity relationships and type structures
- Mermaid graph/flowchart: Business workflows and decision trees
- Mermaid stateDiagram: State transitions and lifecycle
- Graphviz/DOT: Complex dependency graphs, module boundaries
- ASCII: Quick sketches during discussion, simple hierarchies
Domain Modeling Anti-Patterns
Anemic Domain Model
- Symptom: Data structures with getters/setters, all logic in separate services
- Problem: Violates data-orientation by adding ceremony without encapsulation benefits
- Solution: Keep data as data; put related transformations in same module but separate from data definition
Entity Services Anti-Pattern
- Symptom: Classes like
UserService,OrderManager,ProductFactory - Problem: Hides actual operations; lacks ubiquitous language
- Solution: Name functions after domain operations:
approveOrder,cancelSubscription,calculateDiscount
Primitive Obsession
- Symptom: String for email, number for money, boolean flags for states
- Problem: No type safety; invalid values representable
- Solution: Create semantic types with validation
Accidental Complexity
- Symptom: Complex abstractions, design patterns without clear domain benefit
- Problem: Adds layers that obscure domain meaning
- Solution: Simplify; prefer composition over inheritance; avoid premature abstraction
Hidden Temporal Coupling
- Symptom: Must call methods in specific order or system breaks
- Problem: Complects workflow with state management
- Solution: Make workflow explicit; use types to enforce valid transitions
Boolean Blindness
- Symptom: Boolean flags to represent states (isApproved, isActive, isDeleted)
- Problem: Multiple booleans can represent impossible states
- Solution: Use sum types for mutually exclusive states
Contextualizing Within Existing Models
When adding to or changing an existing domain model:
1. Map Current State: Document existing types, relationships, and patterns 2. Identify Affected Concepts: Which existing concepts does this change touch? 3. Check Consistency: Does new design follow existing patterns? If not, why? 4. Assess Impact: What breaks if we make this change? 5. Migration Path: How do we evolve from current to desired state? 6. Update Ubiquitous Language: Ensure all usage points are updated 7. Visualize Before/After: Create diagrams showing current and proposed models
Key Questions:
- Does this change align with existing domain boundaries?
- Are we using consistent terminology?
- Does this introduce new concepts or reuse existing ones?
- Are we fixing an inconsistency or introducing a new one?
- Can we make this change incrementally?
Checklist for Domain Modeling
Before completing domain modeling work:
Language & Communication:
- [ ] All domain concepts are named using ubiquitous language
- [ ] Domain glossary is updated with new/changed terms
- [ ] All code, docs, and conversations use identical terminology
- [ ] Bounded context is clearly identified and documented
Type Design:
- [ ] Types make illegal states unrepresentable
- [ ] No primitive obsession; semantic types are used appropriately
- [ ] Entities have clear identity; value objects are immutable
- [ ] Sum types used for mutually exclusive states
Domain Logic:
- [ ] Business rules are explicit and testable
- [ ] Data and behavior are appropriately separated
- [ ] Workflows are modeled as clear data transformations
- [ ] Domain logic is pure (no side effects)
- [ ] Temporal coupling is eliminated or made explicit
Aggregates & Boundaries:
- [ ] Aggregate boundaries are explicit
- [ ] Aggregates enforce their invariants
- [ ] External references to aggregates use IDs only
- [ ] Aggregates are kept small and focused
- [ ] Transactional boundaries are appropriate
Consistency & Integration:
- [ ] Inconsistencies with existing model are resolved or documented
- [ ] Cross-aggregate consistency strategy is defined (transactional vs eventual)
- [ ] Domain events are used for important occurrences
- [ ] Integration between bounded contexts uses anti-corruption layers
Documentation:
- [ ] Visualization diagrams clearly communicate the design
- [ ] Key decisions and invariants are documented
- [ ] Context map shows relationships between bounded contexts
Resources
references/
This skill includes reference documentation for deeper exploration:
- ddd_foundations_and_patterns.md: Eric Evans' foundational DDD concepts (entities, value objects, aggregates, bounded contexts, repositories, domain events), Martin Fowler's Ubiquitous Language guidance, and practical Clojure/functional patterns. Essential reading for understanding DDD building blocks and how to apply them.
- rich_hickey_principles.md: Core concepts from Rich Hickey's talks including Simple Made Easy, Value of Values, and The Language of the System. Focus on data-oriented design, simplicity, decomplecting, and the power of immutable values.
- wlaschin_patterns.md: Scott Wlaschin's type-driven design patterns, domain modeling recipes, functional architecture guidance, and railway-oriented programming. Emphasis on making illegal states unrepresentable and designing with types.
- visualization_examples.md: Comprehensive examples of Mermaid, Graphviz, and ASCII diagram patterns for domain modeling. Includes entity relationships, workflows, state machines, aggregate boundaries, and bounded context maps.
Load these references when deeper context is needed on specific principles or patterns.
DDD Foundations and Practical Patterns
This reference combines Eric Evans' foundational Domain-Driven Design concepts with practical patterns from Clojure and functional programming approaches, plus Martin Fowler's guidance on Ubiquitous Language.
Eric Evans' Core DDD Concepts
Ubiquitous Language (Fowler & Evans)
Definition: A language structured around the domain model and used by all team members to connect all activities of the team with the software.
Key Principles:
- Same language in conversations, documentation, diagrams, and code
- Developed through collaboration between developers and domain experts
- Evolves with the model; changes in language reflect changes in understanding
- No translation between business and technical discussions
Building Ubiquitous Language: 1. Listen to how domain experts speak 2. Extract nouns (concepts) and verbs (operations) 3. Challenge vague terms - ask for precision 4. Document terms in a glossary 5. Use these exact terms in code - class names, function names, module names 6. When language feels awkward in code, it reveals model problems
Red Flags:
- Developers use different terms than domain experts
- Translation happens between "business language" and "technical language"
- Terms have different meanings in different contexts (without bounded contexts)
- Code uses generic names like "Manager", "Handler", "Processor" instead of domain terms
Example - Good Ubiquitous Language:
;; Domain expert: "We post a transfer to debit one account and credit another"
(defn post-transfer [transfer-number debit credit]
...)
;; NOT:
(defn create-transaction [id source dest] ; "transaction" is technical jargon
...)Entities
Definition: An object defined primarily by its identity, rather than its attributes.
Characteristics:
- Has a unique identifier
- Identity persists through time
- Attributes may change while identity remains constant
- Two entities with same attributes but different IDs are distinct
When to Use:
- Domain experts refer to things by name/ID
- The thing continues to exist even as its properties change
- Need to track the thing over time or across system boundaries
Clojure Pattern:
;; Entity: Account (identity = account number)
(s/def :account/number
(s/and string? #(re-matches #"[1-9]{12}" %)))
(s/def :account/account
(s/keys :req-un [:account/number ; The identity
:balance/balance])) ; Mutable state
(defn make-account [account-number balance]
(s/assert :account/account
{:number account-number
:balance balance}))
;; Same account, even as balance changes:
(def account-1 (make-account "123456789012" (make-balance 1000 :usd)))
(def account-2 (make-account "123456789012" (make-balance 500 :usd)))
;; account-1 and account-2 represent the same account entityImmutable Entities: Not all entities are mutable. Some are created once and never change:
;; Transfer is immutable but still an entity (has identity: transfer number)
(s/def :transfer/number
(s/and string? #(re-matches #"[A-Z]{3}[1-9]{8}" %)))
(s/def :transfer/transfer
(s/keys :req-un [:transfer/id
:transfer/number ; Identity
:debit/debit
:credit/credit
:transfer/creation-date]))Value Objects
Definition: An object defined entirely by its attributes, with no identity.
Characteristics:
- Defined by what it is, not who it is
- No unique identifier
- Two value objects with same attributes are equivalent
- Typically immutable
- Can be freely shared and replaced
When to Use:
- Domain experts describe things by their properties, not by name
- Identity doesn't matter - only the value matters
- Can be replaced with an equivalent value without concern
Clojure Pattern:
;; Value Object: Amount
(s/def :amount/currency #{:usd :cad})
(s/def :amount/value (s/and number? pos?))
(s/def :amount/amount
(s/keys :req-un [:amount/currency
:amount/value]))
(defn make-amount [value currency]
(s/assert :amount/amount
{:currency currency
:value value}))
;; Two amounts with same value are equivalent:
(= (make-amount 100 :usd) (make-amount 100 :usd)) ; => true
;; No identity - only the value mattersEntity vs Value Object - Decision Guide:
| Question | Entity | Value Object |
|---|---|---|
| Do domain experts refer to it by ID/name? | Yes | No |
| Does it change over time? | Often yes | No |
| Do two instances with same attributes mean the same thing? | No | Yes |
| Can you replace it with an equivalent copy? | No | Yes |
Aggregates and Aggregate Roots
Definition: A cluster of domain objects (entities and value objects) that can be treated as a single unit for data changes.
Aggregate Root: The single entity through which all operations on the aggregate must pass.
Purpose:
- Define transactional consistency boundaries
- Simplify the domain model by grouping related objects
- Enforce invariants that span multiple objects
Rules: 1. External references go only to the aggregate root
- Other systems/aggregates can only hold references to the root
- Never hold a reference to an internal entity
2. Root enforces all invariants
- Only the root can directly change internal entities
- Internal entities can exist, but external code can't access them directly
3. Delete removes everything
- Deleting the root should delete all internal entities
4. Transactions don't span aggregates
- One transaction = one aggregate
- Cross-aggregate consistency is eventual
Example - Order Aggregate:
Traditional OOP approach would have:
Order (root)
├─ OrderLine (internal entity)
├─ ShippingAddress (value object)
└─ PaymentInfo (value object)Clojure/Functional Approach:
;; In Clojure, aggregates are often just nested data with root-level invariants
(s/def ::order-line
(s/keys :req-un [::product-id ::quantity ::price]))
(s/def ::order
(s/and
(s/keys :req-un [::order-id ; Root identity
::customer-id
::order-lines ; Collection of internal entities
::shipping-address
::status])
;; Aggregate-level invariant:
(fn [order]
(and (seq (:order-lines order)) ; Must have at least one line
(every-price-matches-product (:order-lines order))))))
;; Operations go through the root:
(defn add-order-line [order line]
;; Add line and validate aggregate invariants
(let [updated-order (update order :order-lines conj line)]
(s/assert ::order updated-order)))
;; External references by ID only:
(s/def ::customer-reference
(s/keys :req-un [::customer-id])) ; Reference to Customer aggregate by ID
;; NOT: embedding full customer aggregateWhen NOT to Use Aggregates:
From the Clojure example:
;; Transfer involves two accounts, but they don't form an aggregate
;; because accounts can be modified independently of transfers.
;; Instead, transfer-money is a domain service.Not everything that's related should be an aggregate. Ask:
- Do these objects need to change together in a single transaction?
- Do invariants span these objects?
- Can one exist without the other?
Size Guideline:
- Keep aggregates small
- Prefer smaller aggregates with eventual consistency between them
- Large aggregates create contention and performance issues
Domain Services
Definition: Operations that don't naturally belong to any single entity or value object.
When to Use:
- Operation involves multiple aggregates or entities
- Operation doesn't conceptually belong to one object
- Named after an activity/operation, not a thing
Clojure Pattern:
;; Domain Service: transfer-money
;; Involves: two Account entities + one Transfer entity
;; Doesn't belong exclusively to any one of them
(defn transfer-money
"Domain service for transferring money between accounts.
Pure function that returns domain events describing the changes."
[transfer-number from-account to-account amount]
(let [debit (dm/make-debit (:number from-account) amount)
credit (dm/make-credit (:number to-account) amount)
debited-account (dm/debit-account from-account debit)
credited-account (dm/credit-account to-account credit)
posted-transfer (dm/post-transfer transfer-number debit credit)]
;; Return domain event describing all changes
{:debited-account debited-account
:credited-account credited-account
:posted-transfer posted-transfer}))Domain Service vs Application Service:
| Domain Service | Application Service |
|---|---|
| Pure function | Coordinates effects |
| Domain logic | Orchestration |
| Returns events/new states | Persists changes |
| Part of domain model | Uses domain model |
| No dependencies on infrastructure | Uses repositories, external services |
Repositories
Definition: Provides the illusion of an in-memory collection of aggregates, abstracting persistence.
Purpose:
- Encapsulate data access logic
- Provide aggregate-oriented persistence
- Separate domain model from persistence concerns
Key Characteristics:
- Operate at aggregate boundaries (save/load whole aggregates)
- Provide lookup by ID
- Hide database/storage implementation
- Return domain entities, not data structures
Clojure Pattern:
;; Repository provides aggregate-level persistence
(defn get-account
"Returns Account entity by account-number, nil if not found."
[account-number]
(when-let [account-row (fetch-from-db account-number)]
(account-row->domain-entity account-row)))
(defn save-account
"Persists Account aggregate."
[account]
(let [account-row (domain-entity->account-row account)]
(persist-to-db account-row)))
;; Application Service orchestrates:
(defn transfer-money-use-case [transfer-number from-id to-id amount]
;; Get aggregates
(let [from-account (repository/get-account from-id)
to-account (repository/get-account to-id)
;; Execute domain logic (pure)
result (domain-service/transfer-money transfer-number
from-account
to-account
amount)]
;; Persist changes
(repository/commit-transfer-event result)
result))Repository vs DAO/Active Record:
- Repository: Aggregate-oriented, domain-driven
- DAO: Table-oriented, database-driven
Bounded Contexts
Definition: An explicit boundary within which a domain model applies. The same term can mean different things in different contexts.
Purpose:
- Manage complexity by dividing the domain
- Allow different models in different parts of the system
- Prevent model corruption from mixing concepts
Key Insights:
- Ubiquitous language is only ubiquitous within a context
- Same word can have different meanings in different contexts
- Make boundaries explicit and protect them
Example:
Bounded Context: Sales
- "Customer": Entity with sales history, credit limit
- "Product": Catalog item with pricing
Bounded Context: Shipping
- "Customer": Just name and shipping address
- "Product": Weight and dimensions for shipping
Bounded Context: Accounting
- "Customer": Billing entity with payment terms
- "Product": Revenue recognition rulesContext Mapping:
Relationships between bounded contexts:
1. Shared Kernel: Two contexts share a subset of the model 2. Customer/Supplier: Downstream context depends on upstream 3. Conformist: Downstream accepts upstream model as-is 4. Anti-Corruption Layer: Translate between contexts to protect model 5. Separate Ways: Contexts are completely independent
Clojure Organization:
;; Each bounded context as a separate namespace or library
;; sales/
;; domain_model.clj
;; domain_services.clj
;; application_service.clj
;; repository.clj
;; shipping/
;; domain_model.clj
;; domain_services.clj
;; application_service.clj
;; repository.clj
;; Integration via anti-corruption layer:
(defn sales-customer->shipping-customer [sales-customer]
{:name (:name sales-customer)
:shipping-address (:default-address sales-customer)})Domain Events
Definition: Something important that happened in the domain that domain experts care about.
Characteristics:
- Named in past tense (OrderPlaced, PaymentProcessed, AccountDebited)
- Immutable facts
- Contain data about what happened
- Can trigger reactions in same or different bounded contexts
Uses: 1. Within a bounded context: Decouple domain logic 2. Between bounded contexts: Integration and eventual consistency 3. Event Sourcing: Store events as source of truth
Clojure Pattern:
;; Domain events describe state changes
(s/def :debited-account/event #{:debited-account})
(s/def :account/debited-account
(s/keys :req-un [:debited-account/event
:account/number
:debited-account/amount-value]))
(defn debit-account [account debit]
;; Returns domain event describing the change
(if (valid-debit? account debit)
{:event :debited-account
:number (:number account)
:amount-value (-> debit :amount :value)}
(throw (ex-info "Can't debit account" {...}))))
;; Application service interprets events:
(defn handle-transfer [result]
(let [{:keys [debited-account credited-account posted-transfer]} result]
;; Persist events
(persist-event debited-account)
(persist-event credited-account)
(persist-event posted-transfer)
;; Trigger side effects
(send-notification (:number debited-account))
(update-balance-cache debited-account credited-account)))Functional DDD Architecture
Layered Architecture (Functional Style)
Domain Layer (Functional Core):
- Pure functions
- No IO, no side effects
- Specs for entities, value objects, aggregates
- Domain services as pure transformations
- Returns new values or domain events
Application Layer (Imperative Shell):
- Orchestrates use cases
- Fetches data via repositories
- Calls pure domain functions
- Interprets domain events
- Performs side effects
Infrastructure Layer:
- Repositories (persistence)
- External services (HTTP, message queues)
- Framework integrations
Example Structure:
;; Domain Layer (pure)
(ns my-app.domain.model
(:require [clojure.spec.alpha :as s]))
(s/def ::entity ...)
(defn make-entity [...] ...)
(defn update-entity [entity change] ...)
;; Application Layer (orchestration + effects)
(ns my-app.application
(:require [my-app.domain.model :as model]
[my-app.infrastructure.repository :as repo]))
(defn use-case [inputs]
(let [entity (repo/get-entity (:id inputs))
updated (model/update-entity entity inputs)]
(repo/save-entity updated)
updated))
;; Infrastructure Layer (IO)
(ns my-app.infrastructure.repository)
(defn get-entity [id]
;; Database access
...)
(defn save-entity [entity]
;; Database write
...)Functional Core, Imperative Shell
Principle: Maximize pure functional code, minimize and isolate side effects.
Pattern: 1. Shell reads inputs (IO) 2. Shell calls pure Core with data 3. Core returns results (pure computation) 4. Shell performs effects based on results
;; CORE: Pure domain logic
(defn calculate-order-total [order]
(reduce + (map :price (:items order))))
(defn apply-discount [total discount-rules customer]
;; Pure calculation
...)
;; SHELL: Application service with effects
(defn process-order [order-request]
;; Read (effect)
(let [customer (db/get-customer (:customer-id order-request))
products (db/get-products (:product-ids order-request))
;; Pure domain logic
order (domain/create-order customer products order-request)
total (domain/calculate-order-total order)
final-total (domain/apply-discount total @discount-rules customer)
;; Write (effects)
saved-order (db/save-order (assoc order :total final-total))]
;; More effects
(email/send-confirmation customer saved-order)
(analytics/track-order saved-order)
saved-order))Practical Patterns from Clojure DDD
Using Specs for Invariants
;; Spec defines valid states
(s/def ::transfer
(s/and
(s/keys :req-un [::id ::number ::debit ::credit ::creation-date])
;; Invariants as predicates:
(fn [{:keys [debit credit]}]
(and
;; Same amount debited and credited
(= (:amount debit) (:amount credit))
;; Different accounts
(not= (:number debit) (:number credit))))))
;; Constructor validates on creation
(defn make-transfer [transfer-number debit credit]
(s/assert ::transfer
{:id (random-uuid)
:number transfer-number
:debit debit
:credit credit
:creation-date (java.util.Date.)}))Event-Driven State Changes
Instead of mutating entities, return events describing changes:
;; Instead of: (set! account.balance new-balance)
;; Return event:
(defn debit-account [account debit]
(if (can-debit? account debit)
{:event :debited-account
:account-number (:number account)
:amount (:value debit)
:timestamp (now)}
(throw (ex-info "Cannot debit" {...}))))
;; Repository interprets event:
(defn commit-debit-event [event]
(swap! state update-in
[:accounts (:account-number event)]
apply-debit
(:amount event)))Eventual Consistency Trade-offs
From the Clojure example:
;; With eventual consistency:
;; - Can process 2000 concurrent transfers
;; - Never double-spend (total money is conserved)
;; - BUT: Account can go temporarily negative
;; Business decision: Is this acceptable?
;; - Maybe: charge overdraft fee
;; - Maybe: customer can cover temporarily
;; - Trade-off: massive scalability for eventual consistency
;; If not acceptable: Use strong consistency (transactions, locks)
;; Trade-off: Lower scalability but immediate consistencyAnti-Patterns to Avoid
Anemic Domain Model
Problem: Entities are just data holders; all logic in services.
;; ANEMIC (Bad):
(s/def ::account (s/keys :req-un [::number ::balance]))
(defn debit-account [account amount]
(update account :balance - amount)) ; No validation!
;; Service does all validation:
(defn debit-with-validation [account amount]
(if (>= (:balance account) amount)
(debit-account account amount)
(throw ...)))Better: Put invariants in domain model
;; Domain model validates:
(s/def ::account
(s/and
(s/keys :req-un [::number ::balance])
#(>= (:balance %) 0))) ; Invariant: balance never negative
(defn debit-account [account amount]
(let [new-account (update account :balance - amount)]
(s/assert ::account new-account))) ; Validates invariantGod Aggregates
Problem: Massive aggregates that do everything.
Better: Keep aggregates small and focused.
Missing Bounded Contexts
Problem: One model trying to serve all use cases.
Better: Separate models for separate contexts.
Checklist for DDD Implementation
- [ ] Ubiquitous Language: Same terms in code and conversations
- [ ] Entities have clear identity: Can track over time
- [ ] Value objects are immutable: Defined by attributes
- [ ] Aggregates enforce invariants: Consistency boundaries are clear
- [ ] Operations through aggregate roots: No direct access to internals
- [ ] Domain services for multi-aggregate operations: Pure functions
- [ ] Repositories at aggregate level: Load/save whole aggregates
- [ ] Application services orchestrate: Thin layer calling domain
- [ ] Bounded contexts are explicit: Clear boundaries and integration
- [ ] Domain events capture important happenings: Past tense, immutable
Key Takeaways
1. Start with language: Listen to domain experts, extract ubiquitous language 2. Entities vs Value Objects: Identity vs attributes 3. Aggregates are consistency boundaries: Keep them small 4. Domain logic is pure: No side effects in domain model/services 5. Repository abstracts persistence: Aggregate-oriented 6. Bounded contexts divide complexity: Different models for different contexts 7. Domain events decouple: Integration and eventual consistency 8. Functional core, imperative shell: Maximize purity, isolate effects
Always ask:
- What does the domain expert call this?
- Does this have identity or just value?
- What are the invariants?
- What's the consistency boundary?
- Which context are we in?
Rich Hickey's Design Principles
This reference compiles core concepts from Rich Hickey's talks and writings on software design, with focus on simplicity, data-orientation, and decomplecting.
Simple Made Easy
Talk: "Simple Made Easy" (Strange Loop 2011)
Definitions
Simple (opposite: Complex)
- From "simplex" - one fold/braid
- Objective measure: few interleaved concepts
- About lack of interleaving, not cardinality
- Can have many simple things (like individual strands)
Easy (opposite: Hard)
- Near at hand, familiar, within capability
- Subjective: varies by person, time, and context
- About convenience and familiarity, not inherent quality
Key Insight: Simple != Easy
Making something familiar (easy) doesn't make it simple. Choosing familiar constructs that are complex makes systems harder to understand and change over time.
Trade-off: Simple may be unfamiliar (initially harder), but pays dividends in comprehension and maintainability.
Complecting (Interleaving/Braiding)
Complecting is the act of entwining/braiding things together. Once complected:
- Cannot reason about parts independently
- Cannot change one without affecting others
- Cannot reuse parts separately
- Difficult to understand cause and effect
Examples of Complecting:
- State and identity complected in mutable objects
- Value and time complected in variables
- Syntax and semantics in many languages
- Policy and mechanism in frameworks
Simple vs Complex Constructs
| Simple | Complex |
|---|---|
| Values | State, Objects |
| Functions | Methods (tied to class) |
| Namespaces | Hierarchical namespaces |
| Data | Objects with behavior |
| Declarative data | Imperative code |
| Rules | Conditional logic |
| Consistency | Eventual consistency |
| Queues | Actors (complect what and how) |
Making Things Simple
Strategies: 1. Choose simple constructs - Prefer inherently simple building blocks 2. Design by subtraction - Remove, don't add 3. Separate concerns - Identify what is truly independent 4. Use abstraction wisely - Draw boundaries at natural seams
Questions to Ask:
- What is complected here?
- Can these concerns exist independently?
- Am I braiding together things that could be separate?
- Will I be able to change this later?
The Value of Values
Talk: "The Value of Values" (JaxConf 2012)
What is a Value?
A value is an immutable magnitude, quantity, or number:
- The number 42
- The string "hello"
- The date 2024-01-15
- A tuple/record of values
Crucially: Values don't change. "Changing a value" is nonsense (like "changing the number 42").
Properties of Values
Immutable
- Never change, ever
- No notion of time built in
- Can be freely shared without concern
Semantically Transparent
- Same value everywhere, always
- No hidden context or identity
- Can be compared for equality directly
Language Independent
- 42 is 42 in any language
- Can be transmitted, stored, compared across boundaries
Values Enable Local Reasoning
Because values don't change:
- Can reason about code by substitution
- No spooky action at a distance
- No defensive copying needed
- Equality is simple and meaningful
Facts are Values
In domain modeling, facts about the world are values:
- "Order #123 was placed on 2024-01-15"
- "User's email is user@example.com as of yesterday"
- "The price was $42.00 when we recorded it"
Key Insight: Facts don't change. New facts may supersede old facts, but old facts remain true about the past.
Identity vs State
Identity
- A stable logical entity (e.g., "User #42")
- Persists through time
State
- Value of an identity at a point in time
- "User #42's email address was X on Monday"
Traditional OOP Problem: Conflates identity and state in mutable objects. Changing state loses history; can't compare past and present.
Value-Based Approach:
- Identity: stable reference
- State: succession of immutable values over time
- Can compare any two states
- Can maintain history naturally
Memory is Not a Place
Traditional view: Memory is a place where values live and get changed.
Value-oriented view: Memory is a storage mechanism for values. "Updating" means associating a new value with a name; old value unchanged.
Benefits:
- Retain history
- Compare snapshots
- Simpler concurrency (values never change)
- Undo/redo naturally
Applying Values to Domain Modeling
Model facts, not objects:
- Don't:
user.setEmail("new@example.com")(destroys history) - Do:
events.append({type: "EmailChanged", userId: 42, newEmail: "new@example.com", timestamp: now})
Use persistent data structures:
- Immutable collections that share structure
- Efficient "updates" that create new versions
- All versions remain accessible
Separate identity from state:
- Identity: stable reference (ID, key)
- State: series of immutable values
- Functions: transformations from state to state
Decomplecting
Separating Concerns
Not just "separation of concerns" but identifying what CAN be separated:
Real separation requires: 1. Components have clear, independent purpose 2. Components can be understood in isolation 3. Components can be changed without affecting others 4. Components can be reused in different contexts
Common Complections to Separate
Value and Time
- Complected: Mutable variables (value changes over time)
- Separated: Immutable values + explicit time/version
What and How
- Complected: Imperative code (what you want mixed with how to get it)
- Separated: Declarative specification + separate execution strategy
What and When
- Complected: Synchronous calls (what to do tied to when it happens)
- Separated: Queues, streams (what is independent of when)
What and Who
- Complected: Methods (what you can do tied to who you are / what class)
- Separated: Functions (what you can do independent of caller)
Mechanism and Policy
- Complected: Frameworks that dictate both structure and behavior
- Separated: Libraries/functions (mechanism) + application code (policy)
Domain and Infrastructure
- Complected: Business logic mixed with DB access, HTTP, etc.
- Separated: Pure domain functions + separate persistence/transport layer
Process for Decomplecting
1. Identify the Complection
- What seems unnecessarily tangled?
- What can't I change independently?
2. Find the Seams
- Where do concerns actually divide?
- What's essential vs. incidental?
3. Factor Apart
- Create separate constructs for separate concerns
- Use composition to recombine when needed
4. Validate Independence
- Can each part be understood alone?
- Can each part be tested alone?
- Can each part be reused elsewhere?
Data Orientation
Talks: "The Language of the System" (Clojure/conj 2012), "Spec-ulation" (Clojure/conj 2016)
Data > Objects
Objects:
- Complect data, behavior, and identity
- Hide information behind APIs
- Create proprietary representations
- Require learning specific APIs
Data:
- Open, inspectable, generic
- Can use common tools (map, filter, reduce)
- Self-describing
- Easier to transmit, store, and reason about
Generic Data Structures
Prefer generic collections (maps, vectors, sets) over custom classes when:
- Data is primarily informational (facts, records, events)
- Behavior is not identity-specific
- Multiple systems/components will interact with the data
Benefits:
- Unified tools and functions work across all data
- Easy to extend with new fields
- Easy to serialize and transmit
- Easy to inspect and debug
Information vs. Mechanism
Information:
- Facts about the domain
- Should be data
- Open and accessible
Mechanism:
- How things are computed or achieved
- Can be functions/code
- Encapsulated implementation details
Anti-pattern: Hiding domain information behind abstraction barriers.
Systems are Data Flows
Model systems as data flowing through transformations: 1. Receive data (events, requests) 2. Transform data (functions) 3. Produce data (responses, events, state changes)
Each stage: data in → function → data out.
Benefits:
- Easy to test (data in, data out)
- Easy to compose (output of one = input of another)
- Easy to reason about (trace data flow)
- Easy to parallelize (data is immutable)
Language of the System
Talk: "The Language of the System" (Clojure/conj 2012)
Systems Communicate with Data
When components/services communicate, they exchange data. The format and semantics of that data IS the interface.
Don't:
- Send proprietary objects
- Rely on shared class definitions
- Use binary formats without schema
Do:
- Send data (maps, records)
- Use self-describing formats (JSON, EDN, Transit)
- Version schemas explicitly
Accretion, Not Breaking Changes
Growth Strategies:
Accretion (Good):
- Add new fields (optional)
- Add new types/variants
- Extend enums with new cases
- Provide additional operations
Breaking Changes (Bad):
- Remove fields
- Rename fields
- Change types
- Remove operations
Requiring (Neutral):
- Make optional things required
- Add constraints
- Acceptable if versioned
Versioning
When breaking changes are necessary:
- Create a new version (v2, v3)
- Support both old and new simultaneously
- Allow consumers to migrate at their pace
- Eventually deprecate old versions
Never: Change meaning of existing version.
Applying to Domain Modeling
Modeling Domain Data
Prefer:
- Plain data structures for domain entities
- Immutable records for domain facts
- Generic collections over custom containers
- Functions that transform domain data
Avoid:
- Mutable domain objects
- Behavior attached to entities
- Getters/setters (ceremony without benefit)
- Hidden state
Example: Order Domain
Complex (Complected):
class Order {
private lines: OrderLine[] = [];
private status: Status = Status.Draft;
addLine(product: Product, qty: number) {
this.lines.push(new OrderLine(product, qty));
}
submit() {
if (this.lines.length === 0) throw new Error("Empty order");
this.status = Status.Submitted;
}
}Simple (Decomplected):
type Order = {
id: OrderId;
customerId: CustomerId;
lines: readonly OrderLine[];
status: OrderStatus;
createdAt: Timestamp;
};
function addLine(order: Order, line: OrderLine): Order {
return { ...order, lines: [...order.lines, line] };
}
function submitOrder(order: Order): Result<Order, ValidationError> {
if (order.lines.length === 0) {
return Err({ type: "EmptyOrder" });
}
return Ok({ ...order, status: { type: "Submitted", submittedAt: now() } });
}Why simpler:
- Data and functions separate
- Immutable values
- Explicit about time (createdAt, submittedAt)
- No hidden state
- Easy to test, inspect, transmit
Key Takeaways
1. Simplicity is objective - Measured by lack of interleaving, not familiarity 2. Values don't change - Model facts as immutable values; identity is separate 3. Data is better than objects - Generic, open data beats proprietary encapsulation 4. Separate concerns rigorously - Identify what can truly be independent 5. Accrete, don't break - Grow systems by adding, not changing 6. Systems are data flows - Model as transformations of immutable data
When domain modeling, constantly ask:
- What have I complected here?
- Can I use values instead of mutable state?
- Can I use data instead of objects?
- What can be separated?
- Am I making a breaking change, or accreting?
Domain Modeling Visualization Examples
This reference provides comprehensive examples of using Mermaid, Graphviz/DOT, and ASCII diagrams for domain modeling visualization.
When to Use Each Format
| Format | Best For | Strengths | Limitations |
|---|---|---|---|
| Mermaid | Quick diagrams, workflows, state machines | Easy syntax, widely supported, good for communication | Limited layout control |
| Graphviz/DOT | Complex relationships, dependency graphs | Powerful layout algorithms, precise control | More verbose syntax |
| ASCII | Quick sketches, simple hierarchies, inline docs | Immediately readable in any editor, minimal | Limited visual appeal, simple structures only |
Mermaid Diagrams
Class Diagrams for Domain Entities
Use for: Showing entity relationships and structure
classDiagram
Customer "1" --> "*" Order : places
Order "1" --> "*" OrderLine : contains
OrderLine "*" --> "1" Product : references
Order "1" --> "1" PaymentStatus : has
class Customer {
+CustomerId id
+EmailAddress email
+CustomerName name
+MembershipLevel level
}
class Order {
+OrderId id
+CustomerId customerId
+List~OrderLine~ lines
+PaymentStatus status
+Timestamp createdAt
+calculateTotal() Money
}
class OrderLine {
+ProductId productId
+Quantity quantity
+Money unitPrice
+calculateSubtotal() Money
}
class Product {
+ProductId id
+ProductName name
+Money price
+Category category
}
class PaymentStatus {
<<enumeration>>
Pending
Approved
Rejected
Refunded
}With generics and constraints:
classDiagram
class Result~T, E~ {
<<interface>>
+isOk() boolean
+isErr() boolean
+map(fn) Result~U, E~
+flatMap(fn) Result~U, E~
}
class Ok~T~ {
+value T
}
class Err~E~ {
+error E
}
Result~T, E~ <|-- Ok~T~
Result~T, E~ <|-- Err~E~Flowcharts for Business Workflows
Use for: Showing decision points and process flow
flowchart TD
Start([Receive Order Request]) --> Parse[Parse Request]
Parse --> Valid{Valid Format?}
Valid -->|No| ReturnError[Return 400 Bad Request]
Valid -->|Yes| Validate[Validate Business Rules]
Validate --> Rules{Rules Pass?}
Rules -->|No| ReturnValidation[Return Validation Errors]
Rules -->|Yes| CheckInventory[Check Inventory]
CheckInventory --> InStock{In Stock?}
InStock -->|No| OutOfStock[Return Out of Stock Error]
InStock -->|Yes| CalculatePrice[Calculate Price]
CalculatePrice --> ProcessPayment[Process Payment]
ProcessPayment --> PaymentOk{Payment Success?}
PaymentOk -->|No| PaymentError[Return Payment Error]
PaymentOk -->|Yes| CreateOrder[Create Order Record]
CreateOrder --> SendConfirmation[Send Confirmation Email]
SendConfirmation --> End([Return 201 Created])
ReturnError --> End
ReturnValidation --> End
OutOfStock --> End
PaymentError --> EndRailway-oriented programming pattern:
flowchart LR
Input[Unvalidated Order] --> Validate
Validate --> ValidResult{Result}
ValidResult -->|Ok| Price[Price Order]
ValidResult -->|Err| Error1[Validation Error]
Price --> PriceResult{Result}
PriceResult -->|Ok| Save[Save Order]
PriceResult -->|Err| Error2[Pricing Error]
Save --> SaveResult{Result}
SaveResult -->|Ok| Notify[Send Notification]
SaveResult -->|Err| Error3[Database Error]
Notify --> Success[Order Placed]
Error1 --> ErrorHandler[Handle Error]
Error2 --> ErrorHandler
Error3 --> ErrorHandler
ErrorHandler --> FailureOutput[Error Response]State Diagrams for Lifecycle Modeling
Use for: Showing valid state transitions
stateDiagram-v2
[*] --> Draft
Draft --> Submitted : submit()
Draft --> Abandoned : timeout()
Submitted --> UnderReview : startReview()
UnderReview --> Approved : approve()
UnderReview --> Rejected : reject()
UnderReview --> NeedsRevision : requestChanges()
NeedsRevision --> Submitted : resubmit()
NeedsRevision --> Abandoned : cancel()
Approved --> Published : publish()
Approved --> Archived : archive()
Rejected --> [*]
Abandoned --> [*]
Published --> Archived : archive()
Archived --> [*]
note right of UnderReview
Review must complete
within 48 hours
end noteWith nested states:
stateDiagram-v2
[*] --> Active
state Active {
[*] --> Free
Free --> Trial : startTrial()
Trial --> Paid : subscribe()
Trial --> Free : trialExpires()
Paid --> Free : cancel()
state Paid {
[*] --> Monthly
Monthly --> Annual : upgrade()
Annual --> Monthly : downgrade()
}
}
Active --> Suspended : suspend()
Suspended --> Active : reactivate()
Suspended --> Closed : permanentClose()
Active --> Closed : closeAccount()
Closed --> [*]Sequence Diagrams for Interactions
Use for: Showing message flow between components
sequenceDiagram
participant Client
participant API
participant Domain
participant DB
participant EmailService
Client->>API: POST /orders
API->>API: Parse request
API->>Domain: validateOrder(data)
Domain-->>API: Result<ValidatedOrder>
alt validation failed
API-->>Client: 400 Bad Request
else validation succeeded
API->>Domain: priceOrder(validatedOrder)
Domain-->>API: PricedOrder
API->>DB: saveOrder(pricedOrder)
DB-->>API: OrderId
API->>EmailService: sendConfirmation(orderId)
EmailService-->>API: Async confirmation
API-->>Client: 201 Created
endEntity Relationship Diagrams
Use for: Database schema or aggregate boundaries
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_LINE : contains
PRODUCT ||--o{ ORDER_LINE : "ordered in"
ORDER ||--|| PAYMENT : requires
PAYMENT ||--|| PAYMENT_METHOD : uses
CUSTOMER {
uuid id PK
string email
string name
enum membership_level
timestamp created_at
}
ORDER {
uuid id PK
uuid customer_id FK
enum status
decimal total
timestamp created_at
timestamp updated_at
}
ORDER_LINE {
uuid id PK
uuid order_id FK
uuid product_id FK
int quantity
decimal unit_price
}
PRODUCT {
uuid id PK
string name
decimal price
enum category
int stock_quantity
}
PAYMENT {
uuid id PK
uuid order_id FK
uuid payment_method_id FK
decimal amount
enum status
timestamp processed_at
}Gantt Charts for Project Planning
Use for: Timeline and dependency visualization
gantt
title Domain Model Implementation Timeline
dateFormat YYYY-MM-DD
section Core Types
Define value objects :done, types1, 2024-01-01, 3d
Define entities :done, types2, after types1, 2d
Define aggregates :active, types3, after types2, 3d
section Validation
Input validation :valid1, after types3, 2d
Business rule validation :valid2, after valid1, 3d
section Workflows
Order placement workflow :wf1, after valid2, 5d
Payment workflow :wf2, after wf1, 4d
Fulfillment workflow :wf3, after wf2, 4d
section Testing
Unit tests :test1, after types1, 15d
Integration tests :test2, after wf1, 10dGraphviz/DOT Diagrams
Complex Dependency Graphs
Use for: Module dependencies, complex relationships
digraph dependencies {
rankdir=LR;
node [shape=box, style=rounded];
// Layers
subgraph cluster_domain {
label="Domain Layer";
style=filled;
color=lightblue;
types [label="Domain Types"];
validation [label="Validation"];
logic [label="Business Logic"];
}
subgraph cluster_application {
label="Application Layer";
style=filled;
color=lightgreen;
workflows [label="Workflows"];
commands [label="Commands"];
queries [label="Queries"];
}
subgraph cluster_infrastructure {
label="Infrastructure Layer";
style=filled;
color=lightgrey;
db [label="Database"];
http [label="HTTP Client"];
email [label="Email Service"];
}
// Dependencies
validation -> types;
logic -> types;
logic -> validation;
workflows -> logic;
workflows -> validation;
commands -> workflows;
queries -> workflows;
workflows -> db [style=dashed, label="port"];
workflows -> http [style=dashed, label="port"];
workflows -> email [style=dashed, label="port"];
}Aggregate Boundaries
Use for: DDD aggregates and bounded contexts
digraph aggregates {
rankdir=TB;
node [shape=box];
subgraph cluster_order_aggregate {
label="Order Aggregate";
style=filled;
color=lightblue;
Order [shape=box, style="rounded,filled", fillcolor=gold, label="Order\n(Root)"];
OrderLine [label="OrderLine"];
ShippingAddress [label="ShippingAddress"];
Order -> OrderLine [label="contains"];
Order -> ShippingAddress [label="has"];
}
subgraph cluster_customer_aggregate {
label="Customer Aggregate";
style=filled;
color=lightgreen;
Customer [shape=box, style="rounded,filled", fillcolor=gold, label="Customer\n(Root)"];
Address [label="Address"];
Customer -> Address [label="has many"];
}
subgraph cluster_product_aggregate {
label="Product Aggregate";
style=filled;
color=lightyellow;
Product [shape=box, style="rounded,filled", fillcolor=gold, label="Product\n(Root)"];
Price [label="Price"];
Inventory [label="Inventory"];
Product -> Price [label="has"];
Product -> Inventory [label="tracks"];
}
// Cross-aggregate references (by ID only)
Order -> Customer [style=dashed, label="customerId"];
OrderLine -> Product [style=dashed, label="productId"];
}Layered Architecture
Use for: Showing architectural layers and flow
digraph architecture {
rankdir=TB;
node [shape=box, width=3];
subgraph cluster_presentation {
label="Presentation Layer";
style=filled;
color=lightblue;
API [label="REST API"];
WebUI [label="Web UI"];
}
subgraph cluster_application {
label="Application Layer";
style=filled;
color=lightgreen;
Workflows [label="Workflows & Use Cases"];
}
subgraph cluster_domain {
label="Domain Layer";
style=filled;
color=gold;
DomainLogic [label="Domain Logic"];
DomainTypes [label="Domain Types"];
}
subgraph cluster_infrastructure {
label="Infrastructure Layer";
style=filled;
color=lightgrey;
Database [label="Database"];
ExternalAPIs [label="External APIs"];
MessageQueue [label="Message Queue"];
}
API -> Workflows;
WebUI -> Workflows;
Workflows -> DomainLogic;
DomainLogic -> DomainTypes;
Workflows -> Database [style=dashed];
Workflows -> ExternalAPIs [style=dashed];
Workflows -> MessageQueue [style=dashed];
{rank=same; API; WebUI}
{rank=same; Database; ExternalAPIs; MessageQueue}
}Data Flow Diagrams
Use for: Showing how data moves through the system
digraph dataflow {
rankdir=LR;
node [shape=circle];
Input [label="External\nInput"];
Parse [label="Parse"];
Validate [label="Validate"];
Transform [label="Transform"];
Execute [label="Execute\nLogic"];
Persist [label="Persist"];
Output [label="External\nOutput"];
Input -> Parse [label="raw data"];
Parse -> Validate [label="structured data"];
Validate -> Transform [label="validated data"];
Transform -> Execute [label="domain types"];
Execute -> Persist [label="results"];
Persist -> Output [label="response"];
// Error paths
Parse -> Output [label="parse error", style=dashed, color=red];
Validate -> Output [label="validation error", style=dashed, color=red];
Execute -> Output [label="business error", style=dashed, color=red];
Persist -> Output [label="persistence error", style=dashed, color=red];
}ASCII Diagrams
Simple Hierarchies
Use for: Quick sketches, documentation, inline comments
Domain Model Hierarchy
======================
Order (Aggregate Root)
├─> OrderId (Value Object)
├─> CustomerId (Value Object)
├─> OrderStatus (Enum)
│ ├─ Draft
│ ├─ Submitted
│ ├─ Approved
│ └─ Fulfilled
├─> OrderLines (Collection)
│ └─> OrderLine
│ ├─> ProductId
│ ├─> Quantity
│ └─> UnitPrice
└─> PaymentInfo
├─> PaymentMethod
└─> PaymentStatusRelationships
Customer Relationships
=====================
Customer (1) ────places────> (*) Order
│
├── contains ──> (*) OrderLine
│ │
│ └── references ──> (1) Product
│
└── requires ──> (1) Payment
│
└── uses ──> (1) PaymentMethodState Transitions
Order Lifecycle
===============
┌───────┐
│ Draft │
└───┬───┘
│ submit()
v
┌─────────┐
│Submitted│
└────┬────┘
│
├── approve() ────> ┌────────┐
│ │Approved│
│ └────┬───┘
│ │ fulfill()
│ v
│ ┌─────────┐
│ │Fulfilled│────> [END]
│ └─────────┘
│
└── reject() ─────> ┌────────┐
│Rejected│────> [END]
└────────┘Data Flow
Order Placement Pipeline
========================
Unvalidated Validated Priced Saved
Order => Order => Order => Order => Event
│ │ │ │ │
└─ validate() ──┘ │ │ │
└─ priceOrder() ┘ │ │
└─ saveOrder() ─┘ │
└─ notify() ┘
Errors:
ValidationError ─┐
PricingError ────├──> ErrorHandler ──> ErrorResponse
DatabaseError ───┘Component Boxes
┌─────────────────────────────────────────────┐
│ Order Management Domain │
├─────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Commands │ │ Queries │ │
│ ├──────────────┤ ├─────────────────┤ │
│ │ PlaceOrder │ │ GetOrder │ │
│ │ CancelOrder │ │ ListOrders │ │
│ │ ApproveOrder │ │ GetOrderHistory │ │
│ └──────────────┘ └─────────────────┘ │
│ │ │ │
│ v v │
│ ┌──────────────────────────────────────┐ │
│ │ Domain Logic │ │
│ │ ┌────────────┐ ┌───────────────┐ │ │
│ │ │ Validation │ │ Business Rules│ │ │
│ │ └────────────┘ └───────────────┘ │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘Matrix/Table Layouts
State Transition Matrix
=======================
From/To │ Draft │ Submitted │ Approved │ Rejected │ Fulfilled
────────────┼───────┼───────────┼──────────┼──────────┼──────────
Draft │ - │ Yes │ No │ No │ No
Submitted │ No │ - │ Yes │ Yes │ No
Approved │ No │ No │ - │ No │ Yes
Rejected │ No │ No │ No │ - │ No
Fulfilled │ No │ No │ No │ No │ -Layered Architecture
Layered Architecture
====================
┌────────────────────────────────────────┐
│ Presentation Layer (API) │ ← HTTP, JSON, Auth
├────────────────────────────────────────┤
│ Application Layer │ ← Workflows, Use Cases
│ ┌──────────────┐ ┌────────────────┐ │
│ │ Order Mgmt │ │ Customer Mgmt │ │
│ └──────────────┘ └────────────────┘ │
├────────────────────────────────────────┤
│ Domain Layer │ ← Pure Business Logic
│ ┌──────┐ ┌────────┐ ┌───────────┐ │
│ │Types │ │Validation│ │ Business │ │
│ │ │ │ │ │ Rules │ │
│ └──────┘ └────────┘ └───────────┘ │
├────────────────────────────────────────┤
│ Infrastructure Layer │ ← DB, External APIs
│ ┌──────────┐ ┌──────────┐ ┌──────┐│
│ │ Database │ │ HTTP │ │Email ││
│ └──────────┘ └──────────┘ └──────┘│
└────────────────────────────────────────┘Choosing the Right Visualization
Decision Matrix
| Need | Recommended Format | Reason |
|---|---|---|
| Show entity relationships | Mermaid classDiagram or ER diagram | Clear, standard notation |
| Show business workflow | Mermaid flowchart | Easy to follow decision paths |
| Show state machine | Mermaid stateDiagram | Built-in state diagram support |
| Show message flow | Mermaid sequence diagram | Temporal ordering clear |
| Show module dependencies | Graphviz/DOT | Better layout for complex graphs |
| Show architectural layers | Graphviz/DOT or ASCII | Clear separation of concerns |
| Quick sketch in docs | ASCII | Works everywhere, minimal |
| Inline code comments | ASCII (simple) | Readable in source code |
Complexity Guidelines
Simple (1-5 entities/states):
- ASCII often sufficient
- Quick to create and modify
Medium (6-15 entities/states):
- Mermaid recommended
- Good balance of features and simplicity
Complex (15+ entities/states):
- Graphviz/DOT for maximum control
- Or break into multiple simpler diagrams
Communication Context
For developers:
- Any format works
- Prefer precision over aesthetics
- Include technical details
For stakeholders:
- Mermaid flowcharts (intuitive)
- Avoid technical jargon in labels
- Focus on business concepts
For documentation:
- Mermaid (renders in Markdown viewers)
- ASCII for inline code comments
- Include both high-level and detailed views
Tips and Best Practices
General Principles
1. Start simple - Add complexity only as needed 2. Use consistent naming - Match code/domain language exactly 3. Show relationships clearly - Label edges meaningfully 4. Group related concepts - Use subgraphs/clusters 5. Highlight important paths - Use color/style for emphasis 6. Keep it readable - Don't cram too much in one diagram
Mermaid Tips
- Use meaningful IDs (not just A, B, C)
- Add notes for important details
- Use subgraphs for grouping
- Set direction (TD, LR) for best layout
Graphviz Tips
- Use
rankdirto control flow direction - Use
subgraph cluster_*for grouping - Use
style=filledandcolorfor visual hierarchy - Use edge styles (solid, dashed, dotted) to show relationship types
ASCII Tips
- Use box-drawing characters for cleaner look: ┌─┐│└┘├┤┬┴┼
- Keep lines aligned for readability
- Use indentation to show hierarchy
- Add whitespace for visual separation
Version Control Friendly
All three formats are text-based and work well with git:
- Easy to diff
- Easy to review in PRs
- Easy to search
- No binary files to worry about
Templates
Quick Reference Template
Domain: [Domain Name]
====================
Key Entities:
- [Entity1]: [Brief description]
- [Entity2]: [Brief description]
Relationships:
[Entity1] ──> [Entity2]: [relationship description]
States:
[Entity] can be in: [State1] → [State2] → [State3]
Workflows:
1. [Workflow Name]: [Input] → [Step1] → [Step2] → [Output]Use these visualization patterns to clearly communicate domain models and facilitate shared understanding among team members and stakeholders.
Scott Wlaschin's Type-Driven Design Patterns
This reference compiles patterns and principles from Scott Wlaschin's work on domain modeling, functional architecture, and type-driven design.
Core Philosophy
"Make Illegal States Unrepresentable"
Use the type system to eliminate entire categories of bugs at compile time. If a state shouldn't exist in your domain, make it impossible to construct.
Domain Modeling Made Functional
Understanding the Domain
Key Questions: 1. What are the inputs and outputs? 2. What can happen? (scenarios, workflows) 3. What can go wrong? (errors, edge cases) 4. What are the business rules and constraints? 5. What are the invariants that must always hold?
Process: 1. Talk to domain experts using their language 2. Document workflows as transformations 3. Identify the things (nouns) and actions (verbs) 4. Model the lifecycle and state transitions 5. Capture rules and constraints as types
Workflows as Pipelines
Model business workflows as data transformation pipelines:
Input → Validate → Execute Business Logic → Persist → OutputEach step:
- Takes data as input
- Performs a transformation
- Produces data as output
- May fail (use Result types)
Benefits:
- Clear separation of concerns
- Easy to test each step
- Easy to compose steps
- Makes the happy path obvious
Example: Order Placement Workflow
UnvalidatedOrder
→ ValidateOrder
→ ValidatedOrder
→ PriceOrder
→ PricedOrder
→ PlaceOrder
→ PlacedOrderEventEach arrow is a function. Each type in between represents a distinct state with its own invariants.
Type-Driven Design
Making Illegal States Unrepresentable
Problem: Optional fields that create invalid combinations
Bad:
type Order = {
id: string;
// Both can be null, or both can be set - illegal!
approvedAt: Date | null;
rejectedAt: Date | null;
}Good:
type Order = {
id: OrderId;
status:
| { type: "Pending" }
| { type: "Approved"; approvedAt: Date }
| { type: "Rejected"; rejectedAt: Date };
}Now impossible to be both approved and rejected, or to have dates without corresponding status.
Constrained Types
Create types that can only hold valid values:
Bad: Primitive obsession
function createUser(email: string, age: number) { ... }
// Can pass invalid values: createUser("not-an-email", -5)Good: Constrained types
type EmailAddress = EmailAddress & { __brand: "EmailAddress" };
type Age = Age & { __brand: "Age" };
function createEmailAddress(s: string): Result<EmailAddress, ValidationError> {
if (isValidEmail(s)) return Ok(s as EmailAddress);
return Err({ error: "Invalid email format" });
}
function createAge(n: number): Result<Age, ValidationError> {
if (n >= 0 && n <= 150) return Ok(n as Age);
return Err({ error: "Age must be between 0 and 150" });
}
function createUser(email: EmailAddress, age: Age) { ... }
// Can only pass validated values!Single Case Unions (Wrapper Types)
Use wrapper types to give semantic meaning to primitives:
type CustomerId = { readonly value: string };
type ProductId = { readonly value: string };
type OrderId = { readonly value: string };
// Now cannot confuse these:
function getCustomer(id: CustomerId): Customer { ... }
// getCustomer(productId) // Type error!Benefits:
- Type safety
- Self-documenting code
- Cannot confuse similar primitives
- Compiler catches errors
Exhaustive Pattern Matching
Use discriminated unions and let the compiler ensure you handle all cases:
type PaymentMethod =
| { type: "CreditCard"; cardNumber: string; cvv: string }
| { type: "PayPal"; email: EmailAddress }
| { type: "BankTransfer"; accountNumber: string; routingNumber: string };
function processPayment(method: PaymentMethod): Result<Receipt, PaymentError> {
switch (method.type) {
case "CreditCard":
return processCreditCard(method.cardNumber, method.cvv);
case "PayPal":
return processPayPal(method.email);
case "BankTransfer":
return processBankTransfer(method.accountNumber, method.routingNumber);
// If we add a new payment method, compiler will error here
}
}States and Transitions
Model entity states explicitly, not as flags:
Bad:
type Order = {
isPaid: boolean;
isShipped: boolean;
isCancelled: boolean;
// Can be paid and cancelled? Shipped but not paid?
}Good:
type Order =
| { state: "Unpaid"; items: OrderLine[] }
| { state: "Paid"; items: OrderLine[]; paidAt: Date; paymentMethod: PaymentMethod }
| { state: "Shipped"; items: OrderLine[]; paidAt: Date; shippedAt: Date; trackingNumber: string }
| { state: "Cancelled"; reason: string };Now impossible to be in multiple states or to be shipped without being paid.
Railway-Oriented Programming
The Problem
Functions that can fail complicate the happy path:
function placeOrder(unvalidatedOrder: UnvalidatedOrder) {
const validatedOrder = validateOrder(unvalidatedOrder);
if (validatedOrder.isError) return validatedOrder.error;
const pricedOrder = priceOrder(validatedOrder.value);
if (pricedOrder.isError) return pricedOrder.error;
const placedOrder = saveOrder(pricedOrder.value);
if (placedOrder.isError) return placedOrder.error;
return placedOrder.value;
}Problem: Error handling obscures the happy path.
Result Type
Model success and failure explicitly:
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };Chaining with bind/flatMap
Chain operations that return Results:
function placeOrder(unvalidatedOrder: UnvalidatedOrder): Result<PlacedOrder, OrderError> {
return validateOrder(unvalidatedOrder)
.flatMap(priceOrder)
.flatMap(saveOrder);
}The Railway Metaphor:
- Two tracks: Success and Failure
- Functions switch from success to failure track on error
- Once on failure track, stay on failure track
- Clear separation: happy path is just composition
Combining Results
When you need multiple independent validations:
function createUser(
emailStr: string,
ageNum: number,
nameStr: string
): Result<User, ValidationErrors> {
const email = createEmail(emailStr);
const age = createAge(ageNum);
const name = createName(nameStr);
// Collect all errors, not just first
return combineResults([email, age, name], (email, age, name) => ({
email,
age,
name
}));
}Handling Errors at Boundaries
Keep the domain pure; handle effects at edges:
Inside domain (pure):
function validateOrder(order: UnvalidatedOrder): Result<ValidatedOrder, ValidationError> {
// Pure validation logic, no IO
}At boundary (effects):
async function handleOrderRequest(req: Request): Promise<Response> {
const result = validateOrder(req.body)
.flatMap(priceOrder)
.flatMap(saveOrder); // IO happens here
if (result.ok) {
return { status: 200, body: result.value };
} else {
return { status: 400, body: { error: result.error } };
}
}Designing with Types
Start with the Types
Before writing any implementation:
1. Define the types that represent domain concepts 2. Define the function signatures (inputs and outputs) 3. Implement the functions (fill in the logic)
Benefits:
- Types guide implementation
- Types serve as documentation
- Types catch mismatches early
- Refactoring is safer
Example: Designing a Discount System
Step 1: Define domain types
type Product = { id: ProductId; price: Money; category: Category };
type Customer = { id: CustomerId; membershipLevel: MembershipLevel };
type Category = "Electronics" | "Clothing" | "Books";
type MembershipLevel = "Bronze" | "Silver" | "Gold";
type DiscountRule =
| { type: "Percentage"; percentage: number }
| { type: "FixedAmount"; amount: Money }
| { type: "BuyXGetY"; buyQuantity: number; getQuantity: number };
type Discount = {
rule: DiscountRule;
applicableTo: Category[];
minimumMembershipLevel: MembershipLevel | null;
};Step 2: Define function signatures
function calculateDiscount(
product: Product,
quantity: number,
customer: Customer,
discounts: Discount[]
): Money;
function findApplicableDiscounts(
product: Product,
customer: Customer,
discounts: Discount[]
): Discount[];
function applyDiscount(
price: Money,
quantity: number,
discount: Discount
): Money;Step 3: Implement (signatures guide what's needed)
Use Types to Model Business Rules
Encode business rules in types:
Rule: "An order must have at least one item"
type NonEmptyList<T> = {
head: T;
tail: T[];
};
type Order = {
id: OrderId;
items: NonEmptyList<OrderLine>; // Cannot be empty!
};Rule: "Refunds require a reason if amount exceeds $100"
type Refund =
| { amount: Money; reason: null } // reason not needed
| { amount: Money; reason: string }; // reason required
// Factory function enforces rule:
function createRefund(amount: Money, reason: string | null): Refund {
if (amount.value > 100 && reason === null) {
throw new Error("Refund over $100 requires a reason");
}
return { amount, reason };
}Functional Architecture Patterns
Onion/Hexagonal Architecture
Layers (inside-out): 1. Domain - Pure business logic, no dependencies 2. Application - Workflows, orchestration, still pure 3. Infrastructure - IO, databases, external services
Dependency Rule: Outer layers depend on inner layers, never reverse.
Domain Layer:
- Pure functions
- Domain types
- Business rules
- No IO, no frameworks
Application Layer:
- Composes domain functions into workflows
- Still mostly pure
- Defines interfaces (ports) for infrastructure
Infrastructure Layer:
- Implements ports (adapters)
- Handles IO (database, HTTP, file system)
- Deals with frameworks and libraries
Dependency Injection via Function Parameters
Pass dependencies as function parameters:
// Domain function defines what it needs
function placeOrder(
validateAddress: (address: Address) => Result<ValidatedAddress, ValidationError>,
checkInventory: (productId: ProductId) => Promise<boolean>,
saveOrder: (order: Order) => Promise<Result<void, DbError>>,
order: UnvalidatedOrder
): Promise<Result<OrderPlaced, OrderError>> {
// Implementation uses provided functions
}
// At composition root, provide implementations:
const result = await placeOrder(
addressValidator.validate,
inventory.check,
orderRepository.save,
incomingOrder
);Benefits:
- No hidden dependencies
- Easy to test (pass mock functions)
- Explicit about requirements
- No magic or DI container
Command/Query Separation
Commands: Change state, return void or Result<void, Error>
placeOrder(order): Result<void, OrderError>cancelSubscription(id): Result<void, CancellationError>updateProfile(profile): Result<void, ValidationError>
Queries: Read state, return data, never change state
getOrder(id): Option<Order>findCustomersByEmail(email): Customer[]getTotalRevenue(): Money
Benefits:
- Clear separation of reads and writes
- Easier to optimize (cache queries)
- Easier to reason about (commands have effects, queries don't)
Event Sourcing Pattern
Instead of storing current state, store sequence of events:
type OrderEvent =
| { type: "OrderPlaced"; orderId: OrderId; items: OrderLine[]; at: Timestamp }
| { type: "OrderPaid"; orderId: OrderId; amount: Money; at: Timestamp }
| { type: "OrderShipped"; orderId: OrderId; trackingNumber: string; at: Timestamp }
| { type: "OrderCancelled"; orderId: OrderId; reason: string; at: Timestamp };
function applyEvent(state: Order | null, event: OrderEvent): Order {
switch (event.type) {
case "OrderPlaced":
return { id: event.orderId, items: event.items, status: "Placed" };
case "OrderPaid":
return { ...state!, status: "Paid" };
case "OrderShipped":
return { ...state!, status: "Shipped", trackingNumber: event.trackingNumber };
case "OrderCancelled":
return { ...state!, status: "Cancelled", reason: event.reason };
}
}
function reconstruct(events: OrderEvent[]): Order {
return events.reduce(applyEvent, null)!;
}Benefits:
- Complete history
- Audit trail
- Can replay to any point in time
- Events are facts (immutable)
Practical Patterns
Option Type for Missing Values
Don't use null/undefined for domain concepts:
type Option<T> = { some: true; value: T } | { some: false };
function findCustomer(id: CustomerId): Option<Customer> {
// ...
}
// Forces handling:
const result = findCustomer(customerId);
if (result.some) {
console.log(result.value.name);
} else {
console.log("Customer not found");
}Newtype Pattern for Type Safety
Create distinct types from same underlying representation:
type UserId = string & { __brand: "UserId" };
type ProductId = string & { __brand: "ProductId" };
type OrderId = string & { __brand: "OrderId" };
// Can't mix up IDs:
function getUser(id: UserId): User { ... }
const productId: ProductId = ...;
// getUser(productId); // Type error!Builder Pattern for Complex Construction
For complex objects with many fields:
class OrderBuilder {
private order: Partial<Order> = {};
withCustomer(id: CustomerId): this {
this.order.customerId = id;
return this;
}
addItem(item: OrderLine): this {
this.order.items = [...(this.order.items || []), item];
return this;
}
build(): Result<Order, ValidationError> {
if (!this.order.customerId) return Err({ error: "Customer required" });
if (!this.order.items?.length) return Err({ error: "Items required" });
// ... validate all required fields present
return Ok(this.order as Order);
}
}
const result = new OrderBuilder()
.withCustomer(customerId)
.addItem(item1)
.addItem(item2)
.build();Active Pattern / Parser Pattern
Transform external data into domain types:
function parseOrderRequest(json: unknown): Result<UnvalidatedOrder, ParseError> {
// Parse and validate structure
if (!isObject(json)) return Err({ error: "Expected object" });
if (!hasProperty(json, "items")) return Err({ error: "Missing items" });
// ... more parsing
return Ok({ items: json.items, customerId: json.customerId });
}
function handleRequest(req: Request): Response {
return parseOrderRequest(req.body)
.flatMap(validateOrder)
.flatMap(placeOrder)
.match(
success => ({ status: 200, body: success }),
error => ({ status: 400, body: { error } })
);
}Domain Modeling Recipes
Recipe: Modeling a Workflow
1. Name the workflow using ubiquitous language 2. Define the input (unvalidated/raw data) 3. Define the output (result of successful execution) 4. Define errors (what can go wrong) 5. Break into steps (validation, execution, persistence) 6. Type each intermediate state 7. Implement as pipeline
Recipe: Modeling State Transitions
1. List all possible states 2. For each state, determine what data is available 3. Create a discriminated union type 4. Define transition functions (State → Event → Result<State, Error>) 5. Ensure illegal transitions are unrepresentable
Recipe: Modeling Optional Fields
1. Ask: Is this really optional in all cases? 2. If optional in all cases: Use Option type 3. If required in some states, optional in others: Use discriminated union with separate states 4. Never use null/undefined for domain optionality
Recipe: Modeling Business Rules
1. Express rule in English 2. Identify what makes the rule satisfied 3. Use types to enforce: Constrained types, discriminated unions, or validation functions 4. Make it impossible to violate: Better in types than in runtime checks
Key Takeaways
1. Use types to make illegal states unrepresentable 2. Model workflows as pipelines of data transformations 3. Use Result types for operations that can fail 4. Keep domain pure, handle effects at boundaries 5. Design with types first, implementation second 6. Use wrapper types to add semantic meaning 7. Separate commands (writes) from queries (reads) 8. Model events as immutable facts 9. Use Option type instead of null/undefined 10. Let the compiler be your friend - exhaustive pattern matching
When domain modeling, continuously ask:
- What illegal states can I eliminate?
- What can go wrong here?
- Is this type signature telling the truth?
- Can I make this constraint explicit in the type?
- What's the simplest type that captures this domain concept?
Related skills
FAQ
What design principles does this skill use?
It uses Rich Hickey's data-oriented design and Scott Wlaschin's type-driven design principles.
Can it produce diagrams?
Yes, it creates Mermaid, Graphviz/DOT, and ASCII diagrams to communicate domain concepts.