Skip to content

Repository Pattern

The Repository Pattern provides a consistent abstraction over data storage,
allowing you to persist and retrieve domain entities without coupling to specific database or persistence logic.

✅ Decouples domain logic from infrastructure concerns.
✅ Makes your domain layer testable and clean.
✅ Allows swapping storage implementations without breaking business code.


A repository:

  • Acts like an in-memory collection or data store.
  • Provides methods like save, findById, delete.
  • Works with entities, not raw database rows.

✅ The domain layer interacts only with the repository interface, not with the database or ORM.


DomusJS recommends defining interfaces for your repositories and providing infrastructure-specific implementations.

✅ Interface → inside the domain layer.
✅ Implementation → inside the infrastructure layer.

ℹ️ Note:
The repository interface belongs to the domain, but the implementation belongs to infrastructure.


import { User } from '../entities/user.entity';
export interface UserRepository {
save(user: User): Promise<void>;
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
}
import { PrismaClient } from '@prisma/client';
import { UserRepository } from '../domain/user-repository';
import { User } from '../domain/entities/user.entity';
export class PrismaUserRepository implements UserRepository {
constructor(private readonly prisma: PrismaClient) {}
async save(user: User): Promise<void> {
await this.prisma.user.upsert({
where: { id: user.id.toString() },
update: { email: user.email.value, password: user.password },
create: { id: user.id.toString(), email: user.email.value, password: user.password },
});
}
async findById(id: string): Promise<User | null> {
const record = await this.prisma.user.findUnique({ where: { id } });
return record ? mapRecordToUser(record) : null;
}
async findByEmail(email: string): Promise<User | null> {
const record = await this.prisma.user.findUnique({ where: { email } });
return record ? mapRecordToUser(record) : null;
}
}
function mapRecordToUser(record: any): User {
return new User({
email: Email.create(record.email),
password: record.password,
}, new UniqueEntityId(record.id));
}

✅ The domain layer depends only on the UserRepository interface, never the Prisma implementation.


✅ Define repositories per aggregate or entity (e.g., UserRepository, OrderRepository).
✅ Hide database details inside the implementation, not in the domain layer.
✅ Keep repository interfaces clean — focused on aggregate-level operations, not low-level queries.


✅ Avoid adding too many specialized methods to repositories (keep them focused).
✅ Use repositories to persist aggregates, not partial entities or loose data.
✅ Prefer transactional operations inside services, not directly inside repositories.
✅ Unit test domain logic using repository mocks, not real databases.


const userRepo = container.resolve<UserRepository>('UserRepository');
const user = await userRepo.findByEmail('test@example.com');
if (!user) {
throw new Error('User not found');
}
user.changePassword('newpassword');
await userRepo.save(user);

✅ This flow keeps the domain clean, with no knowledge of how the data is persisted.



Let’s persist clean, rich domain models — one repository at a time 💥