ddd-typescript

Domain-Driven Design tactical patterns for TypeScript. Value Objects, Entities, Aggregates, Domain Services, Domain Events, Ubiquitous Language, and Bounded Contexts. Use when modeling domain logic in TypeScript backend services.

8 stars

Best use case

ddd-typescript is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Domain-Driven Design tactical patterns for TypeScript. Value Objects, Entities, Aggregates, Domain Services, Domain Events, Ubiquitous Language, and Bounded Contexts. Use when modeling domain logic in TypeScript backend services.

Teams using ddd-typescript should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/ddd-typescript/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/ddd-typescript/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/ddd-typescript/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How ddd-typescript Compares

Feature / Agentddd-typescriptStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Domain-Driven Design tactical patterns for TypeScript. Value Objects, Entities, Aggregates, Domain Services, Domain Events, Ubiquitous Language, and Bounded Contexts. Use when modeling domain logic in TypeScript backend services.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Domain-Driven Design for TypeScript

Tactical DDD patterns for rich, behavior-driven domain models in TypeScript.

> **vs `hexagonal-typescript`:** This skill focuses on **domain modeling** — Value Objects, Entities, Aggregates, Domain Events, and Ubiquitous Language. Use `hexagonal-typescript` when you need **package structure and dependency direction** — how to organize ports, adapters, and use case classes.

## When to Activate

- Modeling a new domain concept (entity or value object?)
- Deciding whether logic belongs in domain, use case, or adapter
- Identifying aggregate boundaries and consistency rules
- Designing domain events and their dispatch
- Reviewing for anemic domain model (objects with no behavior)
- Naming modules, types, and functions (ubiquitous language)

---

## Building Block 1: Value Objects

**Identity**: none — equal when all fields are equal.
**Rule**: Immutable. Use readonly properties. Use factory functions or classes with private constructors.

```typescript
// domain/model/Money.ts
export interface Money {
  readonly amount: number
  readonly currency: string
}

export function money(amount: number, currency: string): Money {
  if (amount < 0) throw new InvalidMoneyError('amount must be non-negative')
  if (!currency) throw new InvalidMoneyError('currency required')
  return Object.freeze({ amount, currency })
}

export function addMoney(a: Money, b: Money): Money {
  if (a.currency !== b.currency) throw new CurrencyMismatchError(a.currency, b.currency)
  return money(a.amount + b.amount, a.currency)
}

export function isZero(m: Money): boolean {
  return m.amount === 0
}
```

### Branded Types for Typed IDs

Prevents passing a `UserId` where a `MarketId` is expected — zero runtime cost:

```typescript
// domain/model/MarketId.ts
export type MarketId = string & { readonly _brand: 'MarketId' }

export function marketId(value: string): MarketId {
  if (!value.trim()) throw new Error('MarketId cannot be empty')
  return value as MarketId
}

// domain/model/UserId.ts
export type UserId = string & { readonly _brand: 'UserId' }
export function userId(value: string): UserId { return value as UserId }
```

**Common Value Objects**: `Money`, `Email`, `Slug`, `DateRange`, typed IDs (`MarketId`, `UserId`).

---

## Building Block 2: Entities

**Identity**: defined by a unique ID — two entities with the same ID are the same object.
**Rule**: Has behavior (domain functions), not just data. Immutable updates via spread.

```typescript
// domain/model/market.ts
import type { MarketId } from './MarketId'
import type { MarketPublishedEvent } from '../event/MarketPublishedEvent'

export type MarketStatus = 'DRAFT' | 'ACTIVE' | 'SUSPENDED'

export interface Market {
  readonly id: MarketId | null
  readonly name: string
  readonly slug: string
  readonly status: MarketStatus
}

// Factory — enforces creation invariants
export function createMarket(name: string, slug: string): Market {
  if (!name || name.trim() === '') throw new InvalidMarketError('name required')
  if (!slug || !/^[a-z0-9-]+$/.test(slug)) throw new InvalidMarketError('invalid slug')
  return Object.freeze({ id: null, name: name.trim(), slug, status: 'DRAFT' })
}

// Behavior — returns new entity + events (no mutation)
export function publishMarket(market: Market): { market: Market; events: MarketPublishedEvent[] } {
  if (market.status !== 'DRAFT') throw new MarketAlreadyPublishedError(market.slug)
  return {
    market: { ...market, status: 'ACTIVE' },
    events: [{ type: 'MarketPublished', marketId: market.id!, name: market.name, occurredAt: new Date() }],
  }
}
```

---

## Building Block 3: Aggregates & Aggregate Root

An **Aggregate** is a cluster of domain objects treated as a unit for data changes.
The **Aggregate Root** is the only entry point — external code never holds references to internal entities.

### Rules
- **One transaction = one aggregate** — never modify two aggregates in one async flow
- **Reference other aggregates by ID only** — never by object reference
- **One repository per Aggregate Root** — no repository for child entities
- **Invariants enforced inside the aggregate**

```typescript
// domain/model/order.ts — Aggregate Root
import type { OrderId } from './OrderId'
import type { CustomerId } from './CustomerId'
import type { Money } from './Money'

export interface OrderLine {
  readonly productId: string
  readonly quantity: number
  readonly unitPrice: Money
}

export interface Order {
  readonly id: OrderId | null
  readonly customerId: CustomerId   // reference by ID, not Customer object
  readonly lines: readonly OrderLine[]
  readonly status: 'DRAFT' | 'PLACED'
}

export function createOrder(customerId: CustomerId): Order {
  return Object.freeze({ id: null, customerId, lines: [], status: 'DRAFT' })
}

// Aggregate method — enforces internal invariant, returns new state
export function addLineToOrder(order: Order, line: OrderLine): Order {
  if (order.status !== 'DRAFT') throw new OrderAlreadyPlacedError(order.id)
  return { ...order, lines: [...order.lines, line] }
}

export function placeOrder(order: Order): Order {
  if (order.lines.length === 0) throw new EmptyOrderError(order.id)
  return { ...order, status: 'PLACED' }
}

export function orderTotal(order: Order): Money {
  return order.lines.reduce(
    (acc, line) => addMoney(acc, { amount: line.unitPrice.amount * line.quantity, currency: line.unitPrice.currency }),
    money(0, order.lines[0]?.unitPrice.currency ?? 'EUR'),
  )
}
```

---

## Building Block 4: Domain Services

**When**: Logic belongs in the domain but doesn't fit a single entity.
**Rule**: Stateless functions. No framework imports. Named after domain verbs.

```typescript
// domain/service/pricingPolicy.ts
import type { Order } from '../model/order'
import type { DiscountCode } from '../model/DiscountCode'
import type { Money } from '../model/Money'
import { orderTotal, addMoney, money } from '../model'

export function calculateFinalPrice(order: Order, discountCode: DiscountCode): Money {
  const base = orderTotal(order)
  if (isValidDiscount(discountCode) && discountAppliesTo(discountCode, order)) {
    return subtractMoney(base, discountAmount(discountCode, base))
  }
  return base
}
```

**Domain Service vs Application Service (Use Case):**

| | Domain Service | Application Service (Use Case) |
|---|---|---|
| Location | `domain/service/` | `application/usecase/` |
| Depends on | Domain model only | Ports (in + out), domain services |
| Async/await | Usually not | Yes (DB calls, external services) |
| Framework imports | Never | Can use types from config |
| Example | `pricingPolicy`, `slugGenerator` | `CreateOrderService`, `PlaceOrderService` |

---

## Building Block 5: Domain Events

Domain events represent something that **happened**. Immutable facts.

```typescript
// domain/event/DomainEvent.ts
export interface DomainEvent {
  readonly type: string
  readonly occurredAt: Date
}

// domain/event/MarketPublishedEvent.ts
export interface MarketPublishedEvent extends DomainEvent {
  readonly type: 'MarketPublished'
  readonly marketId: MarketId
  readonly name: string
}
```

### Dispatching Domain Events

Collect events from aggregate operations, dispatch after successful save:

```typescript
// application/usecase/PublishMarketService.ts
import type { PublishMarketUseCase } from '../../domain/port/in/PublishMarketUseCase'
import type { MarketRepository } from '../../domain/port/out/MarketRepository'
import type { EventBus } from '../../domain/port/out/EventBus'
import { publishMarket } from '../../domain/model/market'

export class PublishMarketService implements PublishMarketUseCase {
  constructor(
    private readonly marketRepository: MarketRepository,
    private readonly eventBus: EventBus,
  ) {}

  async execute(marketId: MarketId): Promise<void> {
    const market = await this.marketRepository.findById(marketId)
    if (!market) throw new MarketNotFoundError(marketId)

    const { market: updated, events } = publishMarket(market)  // domain logic
    await this.marketRepository.save(updated)
    await Promise.all(events.map(e => this.eventBus.publish(e)))  // dispatch after save
  }
}
```

---

## Ubiquitous Language

Use the **same terms** in code as domain experts use. Never translate.

```typescript
// ❌ Technical naming — no domain meaning
async function processMarketData(input: MarketInput) {}

// ✅ Ubiquitous language — mirrors domain expert speech
async function publishMarket(market: Market): Promise<{ market: Market; events: DomainEvent[] }>
async function suspendMarket(market: Market, reason: SuspensionReason): Promise<Market>
async function resolveMarket(market: Market, outcome: ResolutionOutcome): Promise<Market>
```

---

## Bounded Contexts

Each service/module corresponds to one Bounded Context. The same word means different things in different contexts.

```plantuml
@startuml
!include <C4/C4_Container>
System_Boundary(oc, "Order Context") {
  Container(oc_ord, "Order", "Aggregate Root", "")
  Container(oc_cust, "Customer", "by ID ref", "")
  Container(oc_prod, "Product", "by ID ref", "")
  Container(oc_line, "OrderLine", "Type", "")
}
System_Boundary(pc, "Payment Context") {
  Container(pc_inv, "Invoice", "Aggregate Root", "")
  Container(pc_cust, "Customer", "different model!", "")
  Container(pc_pm, "PaymentMethod", "Type", "")
}
@enduml
```

### Anti-Corruption Layer Between Contexts

```typescript
// adapter/out/client/PaymentContextAdapter.ts
import type { PaymentPort } from '../../../domain/port/out/PaymentPort'
import type { Order } from '../../../domain/model/order'
import type { Money } from '../../../domain/model/Money'

export class PaymentContextAdapter implements PaymentPort {
  constructor(private readonly httpClient: PaymentHttpClient) {}

  async initiatePayment(order: Order, amount: Money): Promise<PaymentResult> {
    // Translate Order domain model → Payment API request
    const request = {
      orderId: order.id!,
      amount: amount.amount,
      currency: amount.currency,
    }
    const response = await this.httpClient.charge(request)
    // Translate Payment API response → domain PaymentResult
    return { success: response.ok, transactionId: response.txId }
  }
}
```

---

## Anti-Patterns to Avoid

### Anemic Domain Model

```typescript
// ❌ Data container — no behavior, all logic in use case
interface Market {
  status: string  // mutable
  setStatus(s: string): void  // plain setter
}

// ❌ Use case doing domain work it shouldn't
async function publishMarket(id: string) {
  const market = await repo.findById(id)
  if (market.status !== 'DRAFT') throw new Error('not a draft')  // domain rule leaked out!
  market.setStatus('ACTIVE')  // mutation with no intent
  await repo.save(market)
}

// ✅ Domain logic in domain
export function publishMarket(market: Market) {
  if (market.status !== 'DRAFT') throw new MarketAlreadyPublishedError(market.slug)
  return { ...market, status: 'ACTIVE' as const }
}
```

### Primitive Obsession

```typescript
// ❌ Strings everywhere — no type safety
async function createOrder(userId: string, marketId: string): Promise<void>

// ✅ Branded types
async function createOrder(userId: UserId, marketId: MarketId): Promise<void>
```

### Repository per Entity (not per Aggregate Root)

```typescript
// ❌ Direct access to internal entities
await orderLineRepository.save(orderLine)  // bypasses Order invariants!

// ✅ Only via aggregate root
const updatedOrder = addLineToOrder(order, newLine)
await orderRepository.save(updatedOrder)
```

---

## DDD Checklist for New Projects

- [ ] Identify Bounded Contexts (one per service/module)
- [ ] Define Ubiquitous Language (glossary per context)
- [ ] Model Aggregate Roots (what enforces consistency?)
- [ ] Identify Value Objects (use branded types for IDs)
- [ ] Define Domain Events (what facts must be communicated?)
- [ ] Locate Domain Services (stateless logic not fitting an entity)
- [ ] Ensure one repository per Aggregate Root
- [ ] Avoid anemic models (entities have behavior functions)
- [ ] Reference other aggregates by ID only

## Reference

- **Strategic DDD** (Bounded Contexts, Context Map, Subdomain classification, Event Storming): see skill `strategic-ddd`
- **Hexagonal Architecture** (package structure, adapters): see skill `hexagonal-typescript`

Related Skills

typescript-testing

8
from marvinrichter/clarc

TypeScript testing patterns: Vitest for unit/integration, Playwright for E2E, MSW for API mocking, Testing Library for React components. Core TDD methodology for TypeScript/JavaScript projects.

typescript-patterns

8
from marvinrichter/clarc

TypeScript patterns — type system best practices, strict mode, utility types, generics, discriminated unions, error handling with Result types, and module organization. Core patterns for production TypeScript.

typescript-patterns-advanced

8
from marvinrichter/clarc

Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.

typescript-monorepo-patterns

8
from marvinrichter/clarc

TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.

hexagonal-typescript

8
from marvinrichter/clarc

Hexagonal architecture (ports & adapters) for TypeScript Node.js backends. Package structure, port definitions, use case implementation, adapter patterns, DI wiring, and testing strategy. Use when structuring or reviewing TypeScript backend services.

hexagonal-typescript-advanced

8
from marvinrichter/clarc

Advanced Hexagonal Architecture anti-patterns for TypeScript — domain importing framework dependencies, use cases depending on concrete adapters, HTTP handlers bypassing use cases, Zod validation inside the domain model. Each anti-pattern includes wrong/correct comparison with explanation.

typescript-coding-standards

8
from marvinrichter/clarc

TypeScript, JavaScript, React, and Node.js coding standards — naming conventions, immutability, error handling, async patterns, component structure, API design.

zero-trust-patterns

8
from marvinrichter/clarc

Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.

wireframing

8
from marvinrichter/clarc

Wireframing and prototyping workflow: fidelity levels (lo-fi sketch → mid-fi wireframe → hi-fi prototype), tool selection (Figma, Excalidraw, Balsamiq), user flow diagrams, wireframe annotation standards, information architecture (IA) mapping, and the handoff from wireframe to visual design. For developers who need to communicate UI structure before writing code.

webrtc-patterns

8
from marvinrichter/clarc

WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.

webhook-patterns

8
from marvinrichter/clarc

Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.

web-performance

8
from marvinrichter/clarc

Web performance optimization: Core Web Vitals (LCP, CLS, INP), Lighthouse CI with budget configuration, bundle analysis (webpack-bundle-analyzer, vite-bundle-visualizer), hydration performance, network waterfall reading, image optimization (WebP/AVIF, srcset), and font performance.