# ADR 003: Zero Dependencies in Domain Layer

**Status**: Accepted
**Date**: 2024-10-12
**Context**: Crafted Level - Domain Layer Isolation

## Context

The domain layer contains our core business logic - the heart of the application. This logic should:
- Be completely independent of frameworks and libraries
- Survive technology changes
- Be testable without any infrastructure
- Represent pure business rules

## Decision

The domain layer (`packages/domain/`) has **ZERO runtime dependencies**.

```json
// packages/domain/package.json
{
  "dependencies": {},  // ← ZERO runtime dependencies
  "devDependencies": {
    "typescript": "^5.6.3",
    "vitest": "^2.1.8"  // Testing only
  }
}
```

### What's Allowed

✅ **TypeScript** (compiles away)
✅ **Vitest** (dev dependency, testing only)
✅ **Pure TypeScript interfaces and types**
✅ **Native JavaScript/TypeScript features**

### What's NOT Allowed

❌ Prisma (database)
❌ tRPC (API framework)
❌ Zod (validation - use at boundaries only)
❌ Any npm package with runtime code
❌ Node.js-specific APIs (fs, path, etc.)

## Rationale

### Why Zero Dependencies?

1. **Longevity**: Business logic outlives frameworks
   - React 15 → 16 → 17 → 18 → 19
   - Express → Koa → Fastify → tRPC
   - REST → GraphQL → tRPC
   - Domain logic stays stable

2. **Portability**: Can move to any platform
   - Web → Mobile → Desktop → CLI
   - Node.js → Deno → Bun
   - Same domain logic everywhere

3. **Testability**: No mocks needed
   ```typescript
   // No need to mock database, HTTP, filesystem
   const user = User.create({ ... });
   expect(user.isSuccess).toBe(true);
   ```

4. **Clarity**: Only business concepts
   ```typescript
   // ✅ Clear domain concept
   class Product {
     publish(): Result<Product, ProductInvalidError>
   }

   // ❌ Infrastructure leaking in
   class Product {
     async publishToStripe(): Promise<void>  // Stripe = infrastructure!
   }
   ```

### Example: Email Value Object

```typescript
// ✅ GOOD: Zero dependencies
export class Email {
  private constructor(private value: string) {}

  static create(email: string): Result<Email, EmailInvalidError> {
    // Pure validation logic
    if (!email.includes('@')) {
      return failure(new EmailInvalidError(email));
    }
    return success(new Email(email.toLowerCase()));
  }

  getValue(): string {
    return this.value;
  }
}

// ❌ BAD: External dependency
import { z } from 'zod';  // ← Zod dependency!

export class Email {
  static schema = z.string().email();  // ← Validation framework
  // ...
}
```

### Dependency Direction

```
Infrastructure Layer (Prisma, Stripe, etc.)
      ↓ depends on
Domain Layer (ZERO dependencies)
      ↑ depends on
Application Layer (tRPC routers)
```

**Rule**: Dependencies point INWARD to domain, never outward.

## Consequences

### Positive
- Domain logic survives framework changes
- Extremely fast tests (no infrastructure to mock)
- Can use domain in any context (web, mobile, CLI)
- Forces clear separation of concerns
- Easier to reason about business logic

### Negative
- Can't use convenient libraries (Zod, date-fns, etc.)
- Must implement validation logic manually
- More initial effort to set up boundaries

### Neutral
- Need repository interfaces in domain, implementations in infrastructure
- Validation happens at boundaries (API layer uses Zod, domain is pure)

## How to Enforce

### 1. package.json Check
```bash
# Fail CI if domain has runtime dependencies
if [ "$(jq '.dependencies | length' packages/domain/package.json)" -ne 0 ]; then
  echo "ERROR: Domain layer has dependencies"
  exit 1
fi
```

### 2. Architecture Tests
```typescript
// Use dependency-cruiser or similar
it('should have zero dependencies', () => {
  const pkg = require('../packages/domain/package.json');
  expect(Object.keys(pkg.dependencies || {})).toHaveLength(0);
});
```

### 3. Code Review
- Every PR touching domain layer is reviewed for dependencies
- Question: "Could this logic exist without [library X]?"

## Validation Strategy

Since we can't use Zod in domain:

```typescript
// ❌ Domain layer
import { z } from 'zod';  // NO!

// ✅ Application layer (API routers)
import { z } from 'zod';
const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

// ✅ Domain layer (pure validation)
class Email {
  static create(email: string): Result<Email, EmailInvalidError> {
    if (!this.isValid(email)) {
      return failure(new EmailInvalidError(email));
    }
    return success(new Email(email));
  }

  private static isValid(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}
```

**Two-layer validation**:
1. **Input validation** (API layer, Zod): Syntax and format
2. **Business validation** (Domain layer, pure): Business rules

## Alternatives Considered

1. **Allow Zod in Domain**
   - Rejected: Creates dependency on validation library
   - If Zod changes/dies, domain breaks

2. **Allow "Safe" Libraries** (lodash, date-fns)
   - Rejected: Where do we draw the line?
   - Better to have clear zero-dependency rule

3. **Shared Validation Library**
   - Could work but adds complexity
   - Pure TypeScript is sufficient

## References

- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/)
- [Dependency Rule](https://martinfowler.com/articles/badri-hexagonal/)
