Types
| Type | Returns | Used In |
|---|---|---|
| Query Repository | Read Models | Queries (display) |
| Read Repository | Aggregates | Commands (before mutation) |
| Write Repository | void / ID | Commands (persist) |
Port Definition (Outbound)
ports/outbound/user.repository.outbound.ts
interface UserQueryRepositoryOutboundPort {
findPaginated(options: PaginationOptions): Promise<PaginatedResult<UserReadModel>>;
}
interface UserReadRepositoryOutboundPort {
findById(id: UserId): Promise<UserAggregate | null>;
findByEmail(email: Email): Promise<UserAggregate | null>;
}
interface UserWriteRepositoryOutboundPort {
save(user: UserAggregate): Promise<void>;
delete(id: UserId): Promise<void>;
}
Implementation
Extend BaseOutboundAdapter for automatic error wrapping:
infra/outbound-adapters/user/user.repository.ts
import { BaseOutboundAdapter } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class UserRepository
extends BaseOutboundAdapter
implements
UserQueryRepositoryOutboundPort,
UserReadRepositoryOutboundPort,
UserWriteRepositoryOutboundPort
{
constructor(private readonly persistence: UserPersistence) {
super();
}
async findById(id: UserId): Promise<UserAggregate | null> {
const row = await this.persistence.findById(id.value);
if (!row) return null;
return UserAggregate.reconstitute(row);
}
async save(user: UserAggregate): Promise<void> {
await this.persistence.upsert({
id: user.id.value,
email: user.email.value,
name: user.name,
});
}
}
Note: BaseOutboundAdapter automatically wraps all methods with error handling, converting any thrown errors to InfraError.
Usage in Use Cases
// Query use case
class FindUsersQuery {
constructor(private readonly queryRepo: UserQueryRepositoryOutboundPort) {}
async execute(input: FindUsersInput): Promise<FindUsersOutput> {
return this.queryRepo.findPaginated(input.pagination);
}
}
// Command use case
class CreateUserCommand {
constructor(
private readonly readRepo: UserReadRepositoryOutboundPort,
private readonly writeRepo: UserWriteRepositoryOutboundPort,
) {}
async execute(input: CreateUserInput): Promise<void> {
const existing = await this.readRepo.findByEmail(Email.create(input.email));
if (existing) throw new EmailAlreadyExistsError(input.email);
const user = UserAggregate.create(input);
await this.writeRepo.save(user);
}
}
Rules
- ✅ Define ports in BC, implement in infrastructure
- ✅ Return Aggregates from Read Repository
- ✅ Return Read Models from Query Repository
- ✅ Hydrate aggregates in repository (not caller)
- ❌ Don't put business logic in repositories