Claude
Skills
Sign in
Back

tactical-ddd

Included with Lifetime
$97 forever

Design, refactor, analyze, and review code by applying the principles and patterns of tactical domain-driven design. Triggers on: domain modeling, aggregate design, 'entity', 'value object', 'repository', 'bounded context', 'domain event', 'domain service', code touching domain/ directories, rich domain model discussions.

Design

What this skill does


# Tactical DDD

Design, refactor, analyze, and review code by applying the principles and patterns of tactical domain-driven design.

## Principles

1. **Isolate domain logic**
2. **Use rich domain language**
3. **Orchestrate with use cases**
4. **Avoid anemic domain model**
5. **Separate generic concepts**
6. **Make the implicit explicit... like your life depends on it**
7. **Design aggregates around invariants**
8. **Extract immutable value objects liberally**
9. **Repositories are for loading and saving full aggregates**

---

## 1. Isolate domain logic

**What:** Domain logic is not mixed with technical code like HTTP and database transactions.

**Why:** Easier to understand the most important part of the code, easier to validate with domain experts, easier to test and evolve, easier to plan and implement new features.

**Test:** Could a domain expert read the code? Can the code be unit tested without mocks or spinning up databases?

```typescript
// ❌ WRONG - domain polluted with infrastructure
class Delivery {
  async dispatch() {
    this.logger.info('Dispatching delivery', { id: this.id })  // Infrastructure!
    await this.db.beginTransaction()                           // Infrastructure!
    if (this.status !== 'ready') throw new Error('Not ready')
    this.status = 'dispatched'
    await this.db.save(this)                                   // Infrastructure!
    await this.db.commit()                                     // Infrastructure!
    await this.pushNotification.notifyDriver()                 // Infrastructure!
  }
}

// ✅ RIGHT - isolated domain logic
class Delivery {
  dispatch(): void {
    if (this.status !== DeliveryStatus.Ready) {
      throw new DeliveryNotReadyError(this.id)
    }
    this.status = DeliveryStatus.Dispatched
    this.dispatchedAt = new Date()
  }
}
```

---

## 2. Use rich domain language

**What:** Names in code match exactly what domain experts say. No programmer jargon. No generic names.

**Why:** Translation between code-speak and business-speak causes bugs. When a domain expert says "assess a claim" and the code says "processEntity", someone will misunderstand something.

**Test:** Would a domain expert recognize this name? If you'd need to translate it for them, it's wrong.

**Common generic terms to watch for:**
- `Manager`, `Handler`, `Processor`, `Helper`, `Util`
- `Data`, `Info`, `Item` (when domain terms exist)
- `process`, `handle`, `execute` (what does it actually DO?)

```typescript
// ❌ WRONG - programmer jargon
class ClaimHandler {
  processClaimData(claimData: ClaimDTO): ProcessingResult {
    return this.claimProcessor.handle(claimData)
  }
}

// ✅ RIGHT - domain language
class ClaimAssessor {
  assessClaim(claim: InsuranceClaim): AssessmentDecision {
    if (claim.exceedsCoverageLimit()) {
      return AssessmentDecision.deny(DenialReason.ExceedsCoverage)
    }
    return AssessmentDecision.approve()
  }
}
```

---

## 3. Orchestrate with use cases

**What:** A use case is a user goal—something a user would recognize as an action they can perform in your application.

**Why:** Use cases define the entry points to your domain. They answer "what can a user do?" If something isn't a user goal, it's supporting machinery that belongs elsewhere.

**Test (the menu test):** If you described your application's features to a user like a menu, would this be on it?

```
DELIVERY APP MENU:
├── Request Delivery     ← Use case: user goal
├── Track Delivery       ← Use case: user goal
├── Cancel Delivery      ← Use case: user goal
├── Calculate ETA        ← NOT a use case: internal machinery
└── Check Delivery Radius ← NOT a use case: domain rule
```

```typescript
// ❌ WRONG - not a user goal, this is internal machinery
// use-cases/calculate-eta.use-case.ts
async function calculateETA(deliveryId: DeliveryId) {
  const delivery = await deliveryRepository.find(deliveryId)
  const driver = await driverRepository.find(delivery.driverId)
  return routeService.estimateArrival(driver.location, delivery.destination)
}

// ✅ RIGHT - actual user goal (appears in menu)
// use-cases/cancel-delivery.use-case.ts
async function cancelDelivery(deliveryId: DeliveryId, reason: CancellationReason) {
  const delivery = await deliveryRepository.find(deliveryId)
  delivery.cancel(reason)
  await deliveryRepository.save(delivery)
}
```

---

## 4. Avoid anemic domain model

**What:** Domain logic lives in domain objects, not in use cases. Use cases orchestrate; domain objects decide.

**Why:** When business rules leak into use cases, they scatter across the codebase, duplicate, and diverge. The domain becomes a dumb data carrier.

**Test:** Is your use case making business decisions, or just coordinating? If the use case contains if/else business logic, you likely have an anemic model.

```typescript
// ❌ WRONG - business logic in use case (anemic domain)
async function confirmDropoff(deliveryId: DeliveryId, photo: ProofPhoto) {
  const delivery = await deliveryRepository.find(deliveryId)

  // Business rules leaked into use case!
  if (delivery.status !== 'in_transit') {
    throw new Error('Delivery not in transit')
  }
  if (!photo && delivery.requiresSignature) {
    throw new Error('Proof of delivery required')
  }

  delivery.status = 'delivered'
  delivery.proofPhoto = photo
  delivery.deliveredAt = new Date()
  await deliveryRepository.save(delivery)
}

// ✅ RIGHT - use case orchestrates, domain decides
async function confirmDropoff(deliveryId: DeliveryId, photo: ProofPhoto) {
  const delivery = await deliveryRepository.find(deliveryId)

  delivery.confirmDropoff(photo)  // Domain enforces the rules

  await deliveryRepository.save(delivery)
}
```

**Signs of anemic model:**
- Use cases full of if/else business logic
- Domain objects are just data with getters/setters
- Business rules duplicated across multiple use cases
- Validation logic outside the object being validated

---

## 5. Separate generic concepts

**What:** Generic capabilities that aren't specific to your domain live separately from domain-specific logic.

**Why:** A retry mechanism, a caching layer, a validation framework—these aren't YOUR domain. Mixing them with domain logic obscures what's actually specific to your business.

**Test:** Would this code exist in a completely different business domain? If yes, it's generic. If it's specific to YOUR business rules, it's domain.

```typescript
// ❌ WRONG - generic retry logic mixed with domain
// domain/driver-locator.ts
class DriverLocator {
  // Generic retry logic does not belong in domain!
  private async withRetry<T>(fn: () => Promise<T>, attempts: number): Promise<T> {
    for (let i = 0; i < attempts; i++) {
      try { return await fn() }
      catch (e) { if (i === attempts - 1) throw e }
    }
    throw new Error('Retry failed')
  }

  async findAvailableDriver(zone: Zone): Promise<Driver> {
    return this.withRetry(() => this.searchDriversInZone(zone), 3)
  }

  private async searchDriversInZone(zone: Zone): Promise<Driver> {
    // domain logic to find nearest available driver
  }
}

// ✅ RIGHT - same behavior, properly separated
// infra/retry.ts (generic, reusable in any project)
export async function withRetry<T>(fn: () => Promise<T>, attempts: number): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try { return await fn() }
    catch (e) { if (i === attempts - 1) throw e }
  }
  throw new Error('Retry failed')
}

// domain/driver-locator.ts (pure domain, no infra imports)
class DriverLocator {
  async findAvailableDriver(zone: Zone): Promise<Driver> {
    // domain logic to find nearest available driver
  }
}

// use-cases/dispatch-delivery.ts (orchestrates domain + infra)
async function dispatchDelivery(deliveryId: DeliveryId) {
  const delivery = await deliveryRepository.find(deliveryId)
  const driver = await withRetry(
    () => driverLocator.findAvailableDriver(delivery.zone), 3
  )
  delivery.assignDriver(driver)
  await deliveryRepository.save(delivery)
}
```

---

##
Files: 1
Size: 17.5 KB
Complexity: 28/100
Category: Design

Related in Design