# Domain

> Use when creating domain layer artifacts for a NestJS bounded context — entities (AggregateRoot), value objects, domain events, repository interfaces, domain services, validators, or data builders. Covers Hexagonal Architecture + DDD patterns with NestJS CQRS native event support.

- **Type:** Skill
- **Install:** `agentstack add skill-softtor-nestjs-hexagonal-domain`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Softtor](https://agentstack.voostack.com/s/softtor)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Softtor](https://github.com/Softtor)
- **Source:** https://github.com/Softtor/nestjs-hexagonal/tree/main/skills/domain
- **Website:** https://github.com/Softtor/nestjs-hexagonal#readme

## Install

```sh
agentstack add skill-softtor-nestjs-hexagonal-domain
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Domain Layer

The domain layer contains the pure business logic of a bounded context. It has zero framework dependencies except `@nestjs/cqrs` for `AggregateRoot` (entity base class). No `@Injectable`, no `PrismaService`, no HTTP concerns.

---

## Decision Tree

What are you creating?

```
Domain artifact needed?
│
├── An entity (aggregate root with lifecycle methods)
│   └── Go to: Entity Pattern
│
├── A simple value (email, amount, name, phone)
│   └── Go to: VO — Scalar
│
├── A grouped value (address, bank details, money)
│   └── Go to: VO — Composed
│
├── A fixed set of values (payment method, channel type)
│   └── Go to: VO — Enum
│
├── A status with allowed transitions (order status, withdrawal status)
│   └── Go to: VO — State Machine
│
├── The persistence contract for the entity
│   └── Go to: Repository Interface
│
├── Something that happened in the domain
│   └── Go to: Domain Events
│
├── Logic that belongs to the domain but not to one entity
│   └── Go to: Domain Services
│
├── A domain-specific error with context
│   └── Go to: Domain Errors
│
├── Entity invariant enforcement (field-level rules)
│   └── Go to: Validators
│
└── Fake data for tests
    └── Go to: Data Builders
```

---

## Entity Pattern

Full reference: `references/entity-patterns.md`

An entity is identified by its `UniqueEntityID`, not by its value. It extends `AggregateRoot` from `@nestjs/cqrs` so that events flow through the NestJS `EventBus` once committed.

**Two factory methods are mandatory:**

- `static create(props, id?)` — for new instances; calls `this.apply(new XCreatedEvent(...))`
- `static restore(props, id)` — for DB hydration; emits no events

**Event lifecycle** (every entity follows this):
```
entity.apply(event)            // 1. records the event in memory
publisher.mergeObjectContext(entity)  // 2. wires EventBus (in handler)
await repo.save(entity)        // 3. persists
entity.commit()                // 4. publishes events to EventBus
```

**Condensed template:**

```typescript
// domain/entities/.entity.ts
import { Entity } from '@/shared/base-classes/entity';
import { CreatedEvent } from '../events/-created.event';

export interface Props {
  tenantId: string;
  // ... business fields
  createdAt: Date;
  updatedAt?: Date;
}

export class Entity extends EntityProps> {
  private constructor(props: Props, id?: string) {
    super(props, id);
  }

  static create(
    input: OmitProps, 'createdAt' | 'updatedAt'>,
    id?: string,
  ): Entity {
    const entity = new Entity({ ...input, createdAt: new Date() }, id);
    entity.apply(new CreatedEvent(entity.id));
    return entity;
  }

  static restore(props: Props, id: string): Entity {
    return new Entity(props, id);
  }

  // Mutating method
  update(name: string): void {
    this.props.name = name;
    this.touch();
    this.apply(new UpdatedEvent(this.id, name));
  }

  get tenantId(): string { return this.props.tenantId; }
  // ... other getters

  get createdAt(): Date { return this.props.createdAt; }
  get updatedAt(): Date | undefined { return this.props.updatedAt; }
}
```

See `references/entity-patterns.md` for: child entities, `validate()` integration, `toJSON()` override, and the full test template.

---

## Value Object Variants

Full reference: `references/value-object-patterns.md`

All VOs extend `ValueObject` from `@/shared/base-classes/value-object`. Rules:
- Constructor is `private` — only static factories create instances
- `validate()` throws on invariant violations; it is called automatically in the constructor
- Never import `@nestjs/*` or `class-validator` in a VO

### Scalar VO (single value)

```typescript
// domain/value-objects/email.vo.ts
import { ValueObject } from '@/shared/base-classes/value-object';
import { InvalidArgumentError } from '@/shared/domain-errors/errors';

export class EmailVO extends ValueObject {
  private constructor(value: string) { super(value); }

  protected validate(): void {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this._value)) {
      throw new InvalidArgumentError(`Invalid email: "${this._value}"`);
    }
  }

  static create(email: string): EmailVO {
    return new EmailVO(email.toLowerCase().trim());
  }

  get domain(): string { return this._value.split('@')[1]; }
}
```

### Composed VO (multiple props)

```typescript
// domain/value-objects/address.vo.ts
import { ValueObject } from '@/shared/base-classes/value-object';
import { InvalidArgumentError } from '@/shared/domain/errors';

interface AddressProps { street: string; city: string; country: string; }

export class AddressVO extends ValueObject {
  private constructor(props: AddressProps) { super(props); }

  protected validate(): void {
    if (!this._value.street) throw new InvalidArgumentError('Street is required');
    if (!this._value.city)   throw new InvalidArgumentError('City is required');
    if (!this._value.country) throw new InvalidArgumentError('Country is required');
  }

  static create(props: AddressProps): AddressVO {
    return new AddressVO(props);
  }

  get street(): string  { return this._value.street; }
  get city(): string    { return this._value.city; }
  get country(): string { return this._value.country; }
}
```

### Enum VO (fixed set of named values)

```typescript
// domain/value-objects/payment-method.vo.ts
import { ValueObject } from '@/shared/base-classes/value-object';

export enum PaymentMethodEnum { CREDIT = 'CREDIT', DEBIT = 'DEBIT', PIX = 'PIX' }

export class PaymentMethodVO extends ValueObject {
  private constructor(value: PaymentMethodEnum) { super(value); }

  protected validate(): void {
    if (!Object.values(PaymentMethodEnum).includes(this._value)) {
      throw new InvalidArgumentError(`Unknown payment method: ${this._value}`);
    }
  }

  static credit(): PaymentMethodVO { return new PaymentMethodVO(PaymentMethodEnum.CREDIT); }
  static debit():  PaymentMethodVO { return new PaymentMethodVO(PaymentMethodEnum.DEBIT); }
  static pix():    PaymentMethodVO { return new PaymentMethodVO(PaymentMethodEnum.PIX); }

  static from(value: string): PaymentMethodVO {
    return new PaymentMethodVO(value as PaymentMethodEnum);
  }

  isCredit(): boolean { return this._value === PaymentMethodEnum.CREDIT; }
  isPix(): boolean    { return this._value === PaymentMethodEnum.PIX; }
}
```

### State Machine VO (controlled transitions)

```typescript
// domain/value-objects/order-status.vo.ts
import { ValueObject } from '@/shared/base-classes/value-object';

export enum OrderStatusEnum {
  PENDING = 'PENDING', CONFIRMED = 'CONFIRMED', SHIPPED = 'SHIPPED', COMPLETED = 'COMPLETED',
}

const ALLOWED_TRANSITIONS: Record = {
  [OrderStatusEnum.PENDING]:    [OrderStatusEnum.CONFIRMED],
  [OrderStatusEnum.CONFIRMED]:  [OrderStatusEnum.SHIPPED],
  [OrderStatusEnum.SHIPPED]:    [OrderStatusEnum.COMPLETED],
  [OrderStatusEnum.COMPLETED]:  [],
};

export class OrderStatusVO extends ValueObject {
  private constructor(value: OrderStatusEnum) { super(value); }

  protected validate(): void {
    if (!Object.values(OrderStatusEnum).includes(this._value)) {
      throw new InvalidArgumentError(`Invalid order status: ${this._value}`);
    }
  }

  static pending():   OrderStatusVO { return new OrderStatusVO(OrderStatusEnum.PENDING); }
  static confirmed(): OrderStatusVO { return new OrderStatusVO(OrderStatusEnum.CONFIRMED); }

  static from(value: string): OrderStatusVO {
    return new OrderStatusVO(value as OrderStatusEnum);
  }

  canTransitionTo(target: OrderStatusEnum): boolean {
    return ALLOWED_TRANSITIONS[this._value].includes(target);
  }

  transitionTo(target: OrderStatusEnum): OrderStatusVO {
    if (!this.canTransitionTo(target)) {
      throw new InvalidArgumentError(
        `Cannot transition from ${this._value} to ${target}`,
      );
    }
    return new OrderStatusVO(target);
  }

  isPending():   boolean { return this._value === OrderStatusEnum.PENDING; }
  isCompleted(): boolean { return this._value === OrderStatusEnum.COMPLETED; }
}
```

See `references/value-object-patterns.md` for full VO test templates and edge-case guidance.

---

## Repository Interface

Full reference: `references/repository-interface.md`

The repository interface is the domain's **port** for persistence. It lives in `domain/repositories/`. The implementation lives in `infrastructure/`. No `@Injectable` or Prisma imports here.

```typescript
// domain/repositories/.repository.ts
import type { Entity } from '../entities/.entity';
import {
  SearchParams as DefaultSearchParams,
  SearchResult as DefaultSearchResult,
  SearchableRepositoryInterface,
} from '@/shared/repository-contracts/searchable-repository-contracts';

export namespace Repository {
  export type Filter = string;
  export class SearchParams extends DefaultSearchParams {}
  export class SearchResult extends DefaultSearchResultEntity, Filter> {}

  export interface Repository
    extends SearchableRepositoryInterfaceEntity, Filter, SearchParams, SearchResult
    > {
    findById(id: string): PromiseEntity | null>;
    findByTenant(tenantId: string): PromiseEntity[]>;
    save(entity: Entity): Promise;
    delete(id: string): Promise;
  }
}

export const _REPOSITORY = Symbol('Repository');
```

See `references/repository-interface.md` for: extended SearchParams with custom filters, domain-specific query methods, and the in-memory test double pattern.

---

## Domain Events

Full reference: `references/domain-event-patterns.md`

Domain events capture **what happened** in the domain. They implement `IEvent` from `@nestjs/cqrs`.

**Naming:** `Event` (e.g., `OrderPlacedEvent`, `CustomerActivatedEvent`)

```typescript
// domain/events/-created.event.ts
import { IEvent } from '@nestjs/cqrs';

export class CreatedEvent implements IEvent {
  constructor(
    public readonly Id: string,
    public readonly tenantId: string,
    // include all data handlers will need
  ) {}
}
```

Rules:
- Events are applied via `entity.apply(event)` inside entity methods
- The handler calls `publisher.mergeObjectContext(entity)` then `entity.commit()` to publish
- Event handlers use `@EventsHandler` + `IEventHandler` — never `@OnEvent` for new code
- Include enough payload so handlers do not need to re-fetch the entity

See `references/domain-event-patterns.md` for: full event catalog pattern, testing uncommitted events, and handler template.

---

## Domain Services

Full reference: `references/domain-service-patterns.md`

Use a domain service when a domain operation involves multiple entities or rules that do not belong to a single entity. Domain services are **pure classes** — no decorators, no DI, no I/O.

```typescript
// domain/services/.service.ts
export class FeeCalculationService {
  static calculate(amount: number, feePercentage: number): number {
    if (amount .validator.ts
import { IsString, IsNotEmpty, IsUUID, IsOptional, MaxLength } from 'class-validator';
import { ClassValidatorFields } from '@/shared/base-classes/class-validator-fields';
import type { Props } from '../entities/.entity';

class Rules {
  @IsUUID() @IsNotEmpty() tenantId: string;
  @IsString() @IsNotEmpty() @MaxLength(255) name: string;
  @IsOptional() @IsString() description?: string;

  constructor(props: Props) { Object.assign(this, props); }
}

class Validator extends ClassValidatorFieldsRules> {
  validate(data: Props): boolean {
    return super.validate(new Rules(data));
  }
}

export class ValidatorFactory {
  static create(): Validator { return new Validator(); }
}
```

Usage in entity:

```typescript
static validate(props: Props): void {
  const validator = ValidatorFactory.create();
  if (!validator.validate(props)) {
    throw new EntityValidationError(validator.errors);
  }
}
```

See `references/validator-patterns.md` for: the `ClassValidatorFields` base, nested validation, and full test template.

---

## Data Builders

Full reference: `references/data-builder-patterns.md`

Data builders provide deterministic fake data for unit tests. They live in `domain/testing/helpers/`.

```typescript
// domain/testing/helpers/.data-builder.ts
import { faker } from '@faker-js/faker';
import type { Props } from '../../entities/.entity';

export function DataBuilder(overrides?: PartialProps>): Props {
  return {
    tenantId: overrides?.tenantId ?? faker.string.uuid(),
    name:     overrides?.name     ?? faker.commerce.productName(),
    createdAt: overrides?.createdAt ?? new Date(),
    updatedAt: overrides?.updatedAt,
    ...overrides,
  };
}
```

See `references/data-builder-patterns.md` for: composed builders, state-specific variants (Active, Pending, Rejected), and how to wire builders into test `describe` blocks.

---

## Domain Errors

Specialized errors that extend the shared error hierarchy (`DomainError`, `NotFoundError`, `ConflictError`, etc.) with domain-specific context.

Live in `/domain/errors/`. Name convention: `Error`.

```typescript
// domain/errors/order-not-found.error.ts
import { NotFoundError } from '@/shared/domain/errors';

export class OrderNotFoundError extends NotFoundError {
  constructor(public readonly orderId: string) {
    super(`Order ${orderId} not found`);
  }
}

// domain/errors/invalid-order-status-transition.error.ts
import { DomainError } from '@/shared/domain/errors';

export class InvalidOrderStatusTransitionError extends DomainError {
  constructor(
    public readonly currentStatus: string,
    public readonly targetStatus: string,
  ) {
    super(`Cannot transition from ${currentStatus} to ${targetStatus}`);
  }
}
```

**When to specialize:** When the error carries domain context (IDs, amounts, statuses) that helps debugging. Don't specialize if the generic message is sufficient.

See `references/domain-error-patterns.md` for: full error catalog, naming conventions, error filter mapping, testing patterns.

---

## Rules and Anti-Patterns

### What belongs in the domain layer

- Entity classes, VO classes, domain events, repository interfaces, domain service classes, validators
- Types and interfaces that describe domain concepts

### What does NOT belong here

| Anti-pattern | Correct approach |
|---|---|
| `import { Injectable } from '@nestjs/common'` | Only allowed in `infrastructure/` and `application/services/` |
| `import { PrismaService }` | Only in Prisma repository implementations |
| `import { IsEmail } from 'class-validator'` | Only in validators (not in VOs) |
| `new PrismaClient()` inside an entity | Repositories handle persistence |
| Calling another aggregate's repository | Use a domain service or application service |
| Throwing HTTP exceptions (`NotFoundException`) | Throw domain errors; HTTP mapping is in `infrastructure/` |
| `entity.addDomainEvent()` / `entity.pullDomainEvents()` | Use `entity.apply(event)` (NestJS CQRS native) |

### Event invariants

- `create()` ALWAYS calls `this.apply(new XCreatedEvent(...))`
- `restore()` NEVER emits events
- Mutating methods call `this.touch()` then `this.apply(new XUpdatedEvent(...))`
- The repository saves the entity; the CQRS handler commits events

### VO invariants

- VOs are immutable — mutating methods return a NEW VO instance
- State Machine VOs always validate transition legality in `transitionTo()`
- Enum VOs provide named static factories (`static pending()`) — callers never pass raw strings

---

## Testing

### Entity spec skeleton

```typescript
// domain/entities/__tests__/unit/.entity.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Entity } from '../../.entity';
import { DataBuilder } from '../../../testing/helpers/.data-builder';
import { CreatedEvent } from '../../../events/-created.event';

describe('Entity', () => {
  describe('create()', () => {
    it('should create entity with valid props', () => {
      const props = DataBuilder();
      const entity = Entity.create(props);

      expect(entity.id).toBeDefined();
      expect(entity.name).toBe(props.name);
      expect(entity.createdAt).toBeInstanceOf(Date);
    });

    it('should apply Crea

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Softtor](https://github.com/Softtor)
- **Source:** [Softtor/nestjs-hexagonal](https://github.com/Softtor/nestjs-hexagonal)
- **License:** MIT
- **Homepage:** https://github.com/Softtor/nestjs-hexagonal#readme

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-softtor-nestjs-hexagonal-domain
- Seller: https://agentstack.voostack.com/s/softtor
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
