Bounded Contexts

Pure domain logic with ports and use cases

Pure domain logic, completely framework-free.


Structure

bounded-contexts/{bc-name}/
├── bootstrap/                   ← Dependency wiring (at BC root!)
│   ├── index.ts                 ← Main orchestration
│   ├── adapters.bootstrap.ts
│   ├── use-cases.bootstrap.ts
│   ├── validators.bootstrap.ts
│   ├── controller.bootstrap.ts
│   └── routes.bootstrap.ts
├── app/
│   ├── ports/
│   │   ├── inbound/
│   │   │   ├── {resource}/      ← Ports grouped by resource
│   │   │   │   ├── create-{resource}.command.port.ts
│   │   │   │   ├── get-{resource}.query.port.ts
│   │   │   │   └── list-{resources}.query.port.ts
│   │   └── outbound/            ← Repository interfaces
│   └── use-cases/
│       └── {resource}/          ← Use cases grouped by resource
│           ├── create-{resource}.command.ts
│           ├── get-{resource}.query.ts
│           └── list-{resources}.query.ts
├── domain/
│   ├── aggregates/
│   │   └── {aggregate}/
│   │       └── policies/
│   ├── entities/
│   ├── value-objects/
│   ├── events/                  ← Domain events
│   ├── services/                ← Domain services
│   └── exceptions/
├── infra/
│   └── outbound-adapters/       ← Port implementations
│       └── persistence/
│           └── drizzle/
│               ├── mappers/     ← Domain ↔ persistence mappers
│               └── {resource}/  ← Repository implementations
└── presentation/                ← BC-specific HTTP layer
    └── http/
        ├── service.metadata.ts
        ├── resources/           ← Resource metadata
        ├── endpoints/           ← Endpoint metadata
        └── {resource}/{endpoint}/
            ├── dtos.ts          ← Request/Response DTOs
            ├── schemas.ts       ← Validation schemas
            ├── mappers.ts       ← DTO transformations
            └── endpoint.metadata.ts

Info:

The bootstrap folder is at the BC root, not under presentation. See Bootstrap Pattern for details.


Golden Rules

  1. NO framework imports
  2. NO environment variables
  3. NO direct cross-BC imports
  4. CAN import from domain (VOs, aggregates) into infra

Inbound Ports

Interfaces for use cases. Implemented by queries and commands.

ports/inbound/queries/find-user-by-id.port.ts
interface FindUserByIdQueryInboundPort {
  execute(input: FindUserByIdInputDto): Promise<FindUserByIdOutputDto>;
}

Naming Conventions

Two patterns are acceptable:

Full naming (explicit):

TypeInterfaceFile
Query{Name}QueryInboundPort{resource}/{name}.query.port.ts
Command{Name}CommandInboundPort{resource}/{name}.command.port.ts

Short naming (concise):

TypeInterfaceFile
Query{Name}Port{resource}/{name}.query.port.ts
Command{Name}Port{resource}/{name}.command.port.ts

Info:

Choose one convention and use it consistently throughout your project. The short naming works well when file names already indicate the type (.query.port.ts vs .command.port.ts).


Outbound Ports

Interfaces for external dependencies. Implemented by infrastructure.

ports/outbound/user.repository.outbound.ts
interface UserRepositoryOutboundPort {
  findById(id: UserId): Promise<User | null>;
  save(user: User): Promise<void>;
}

Use Cases

Implement inbound ports using BaseInboundAdapter for automatic error handling.

use-cases/queries/find-user-by-id.use-case.ts
import { BaseInboundAdapter, NotFoundError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';

class FindUserByIdQuery
  extends BaseInboundAdapter<FindUserByIdInputDto, FindUserByIdOutputDto>
  implements FindUserByIdQueryInboundPort
{
  constructor(private readonly userRepo: UserRepositoryOutboundPort) {
    super();
  }

  protected async handle(input: FindUserByIdInputDto): Promise<FindUserByIdOutputDto> {
    const user = await this.userRepo.findById(UserId.create(input.data.userId));
    if (!user) {
      throw new NotFoundError({ message: `User ${input.data.userId} not found` });
    }
    return FindUserByIdOutputDto.create({
      id: user.id.value,
      email: user.email,
      name: user.name,
    });
  }
}

Note: Implement handle() (protected) instead of execute(). The BaseInboundAdapter provides execute() with automatic error wrapping.


Domain

Aggregates

class UserAggregate {
  private constructor(
    private readonly _id: UserId,
    private _email: string,
    private _name: string,
  ) {}

  static create(data: CreateUserData): UserAggregate { ... }
  static reconstitute(data: UserData): UserAggregate { ... }
}

Value Objects

class UserId {
  private constructor(private readonly _value: string) {}
  static create(value: string): UserId { ... }
  static generate(): UserId { ... }
  get value(): string { return this._value; }
}

See Aggregates and Value Objects for details.