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)