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.
What is a Repository?
Section titled “What is a Repository?”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.
Repository Setup in DomusJS
Section titled “Repository Setup in DomusJS”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.
Example Use Case: User Repository
Section titled “Example Use Case: User Repository”Step 1: Define the Interface
Section titled “Step 1: Define the Interface”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>;}
Step 2: Create the Implementation
Section titled “Step 2: Create the Implementation”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.
Core Principles
Section titled “Core Principles”✅ 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.
Best Practices
Section titled “Best Practices”✅ 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.
Example: Using the Repository
Section titled “Example: Using the Repository”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.
Learn More
Section titled “Learn More”Let’s persist clean, rich domain models — one repository at a time 💥