New Bounded Context

Step-by-step guide to creating a new Bounded Context

Structure

bounded-contexts/{bc-name}/
├── app/
│   ├── ports/
│   │   ├── inbound/
│   │   └── outbound/
│   └── use-cases/
│       ├── queries/
│       └── commands/
├── domain/
│   ├── aggregates/
│   ├── entities/
│   ├── value-objects/
│   └── exceptions/
└── infra/

Steps

1. Create Folder Structure

Create all directories under bounded-contexts/{bc-name}/.

2. Define Domain Model

Start with aggregates and value objects using the library base classes:

domain/value-objects/user-id.vo.ts
import { BaseUuidV4Vo } from '@cosmneo/onion-lasagna/backend/core/onion-layers';

export class UserId extends BaseUuidV4Vo {
  static override generate(): UserId {
    return new UserId(crypto.randomUUID());
  }

  static override create(value: string): UserId {
    BaseUuidV4Vo.create(value); // Validates UUID format
    return new UserId(value);
  }
}
domain/aggregates/user.aggregate.ts
import { BaseAggregateRoot } from '@cosmneo/onion-lasagna/backend/core/onion-layers';

interface UserProps {
  email: string;
  name: string;
}

export class UserAggregate extends BaseAggregateRoot<UserId, UserProps> {
  static create(data: CreateUserData): UserAggregate { ... }
  static reconstitute(data: UserData, version: number): UserAggregate { ... }
}

3. Define Domain Exceptions

Domain exceptions extend DomainError for consistent error handling:

domain/exceptions/user-not-found.error.ts
import { DomainError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';

export class UserNotFoundError extends DomainError {
  constructor(userId: string) {
    super({
      message: `User not found: ${userId}`,
      code: 'USER_NOT_FOUND',
    });
  }
}

Info:

Domain errors are masked (return 500) in HTTP responses. Translate them to use case errors (NotFoundError, ConflictError, UnprocessableError) in your use cases if you want clients to see meaningful error messages.

4. Define Outbound Ports

app/ports/outbound/user.repository.outbound.ts
interface UserReadRepositoryOutboundPort {
  findById(id: UserId): Promise<UserAggregate | null>;
}

interface UserWriteRepositoryOutboundPort {
  save(user: UserAggregate): Promise<void>;
}

5. Define Inbound Ports

app/ports/inbound/create-user.command.inbound.ts
interface CreateUserCommandInboundPort {
  execute(input: CreateUserInput): Promise<CreateUserOutput>;
}

6. Implement Use Cases

app/use-cases/commands/create-user.command.ts
class CreateUserCommand implements CreateUserCommandInboundPort {
  constructor(
    private readonly readRepo: UserReadRepositoryOutboundPort,
    private readonly writeRepo: UserWriteRepositoryOutboundPort,
  ) {}

  async execute(input: CreateUserInput): Promise<CreateUserOutput> {
    const user = UserAggregate.create(input);
    await this.writeRepo.save(user);
    return { id: user.id.value };
  }
}

7. Implement Infrastructure (if BC-scoped)

infra/implementations/user/user.repository.ts
class UserRepository implements 
  UserReadRepositoryOutboundPort, 
  UserWriteRepositoryOutboundPort 
{
  constructor(private readonly persistence: UserPersistence) {}
  // ...
}

Checklist

  • Folder structure created
  • Value objects defined
  • Aggregates defined
  • Domain exceptions created
  • Outbound ports defined
  • Inbound ports defined
  • Use cases implemented
  • Infrastructure implemented (if BC-scoped)