Hexagonal Architecture - Packmind Monorepo
Architecture Map
Every domain package follows a three-layer hexagonal (ports & adapters) architecture. Dependencies flow inward only: Infrastructure -> Application -> Domain.
packages/{domain}/src/
├── domain/ # Pure business logic, zero dependencies
│ ├── entities/ # Domain entities and value objects
│ ├── repositories/ # Repository interfaces (ports)
│ ├── jobs/ # Delayed job definitions
│ ├── errors/ # Domain-specific error classes
│ ├── utils/ # Domain-specific utility functions
│ └── types/ # Domain-specific type definitions
│
├── application/ # Orchestration & coordination
│ ├── useCases/{name}/ # One folder per use case
│ │ └── {name}.usecase.ts
│ ├── services/ # Domain services + service aggregator
│ ├── adapter/ # Outbound adapter (implements port)
│ ├── listeners/ # Domain event listeners
│ └── jobs/ # Delayed job implementations
│
├── infra/ # Concrete implementations
│ ├── repositories/ # Repository implementations (TypeORM)
│ ├── schemas/ # TypeORM EntitySchema definitions
│ └── jobs/ # Job factories
│
└── index.ts # Package exports + {Domain}Hexa facade
Layer Details
| Layer | Depends On | Purpose | Details |
|---|---|---|---|
| Domain | Nothing (pure TS + @packmind/types) |
Business rules, entity definitions, port interfaces | domain-layer.md |
| Application | Domain | Use case orchestration, services, adapters, events | application-layer.md |
| Infrastructure | Domain + Application | Persistence, schemas, job queues | infrastructure-layer.md |
Component Reference
Each component type has its own detailed guide with templates and examples:
| Component | Layer | File Pattern | Guide |
|---|---|---|---|
| Use Case | Application | application/useCases/{name}/{name}.usecase.ts |
usecase.md |
| Service | Application | application/services/{Name}Service.ts |
service.md |
| Adapter | Application | application/adapter/{Domain}Adapter.ts |
adapter.md |
| Port | Types package | packages/types/src/{domain}/ports/I{Domain}Port.ts |
port.md |
| Contract | Types package | packages/types/src/{domain}/contracts/{UseCaseName}.ts |
contract.md |
| Repository (interface) | Domain | domain/repositories/I{Entity}Repository.ts |
repository.md |
| Repository (impl) | Infrastructure | infra/repositories/{Entity}Repository.ts |
repository.md |
| Schema | Infrastructure | infra/schemas/{Entity}Schema.ts |
schema.md |
| Listener | Application | application/listeners/{Domain}Listener.ts |
listener.md |
| Event | Types package | packages/types/src/events/{EventName}.ts |
event.md |
| Hexa Facade | Root | {Domain}Hexa.ts + index.ts |
hexa-facade.md |
Dependency Flow
API (NestJS controllers)
│
▼
┌─────────────┐
│ HexaRegistry │ ── Central DI container
└──────┬──────┘
│ getAdapter<IXxxPort>(portName)
▼
┌─────────────┐
│ {Domain}Hexa │ ── Facade: wires repos, services, adapter
└──────┬──────┘
│
┌────┴────┐
▼ ▼
Adapter Services ── Application layer
│ │
▼ ▼
Use Cases ──► Domain entities + Repository interfaces
│
▼
Infra Repositories ── Concrete persistence (TypeORM)
Cross-Domain Communication
Domains talk to each other through two mechanisms:
Synchronous - Via ports injected through the
HexaRegistryduringinitialize(). Example:StandardsAdaptercallsIGitPort.commitToGit().Asynchronous - Via domain events emitted through
PackmindEventEmitterService. Example:StandardCreatedEventtriggersDeploymentsListener.handleStandardCreated().
See event.md and adapter.md for patterns.
Key Base Classes (from @packmind/node-utils)
| Class | Purpose |
|---|---|
BaseHexa<TOpts, TPort> |
Hexagon lifecycle (construct -> initialize -> destroy) |
HexaRegistry |
Central dependency injection container |
AbstractMemberUseCase<C, R> |
Use case with org member authorization |
AbstractSpaceMemberUseCase<C, R> |
Use case with org + space member authorization |
AbstractAdminUseCase<C, R> |
Use case with admin authorization |
AbstractRepository<T> |
Base TypeORM repository with soft delete support |
PackmindListener<TAdapter> |
Event subscription base class |
PackmindEventEmitterService |
Event bus for domain events |