Repository Pattern: pros, cons, skip vaj
repository pattern backend tutorials law' default recommend (dependency injection rap), skip wrong obvious legh. pagh. specific problems vaD useful pattern, 'ej habit adopt applications law' vaD unnecessary overhead.
nuq
repository domain/business logic 'ej data access code Dung abstraction. service layer ORM pagh SQL direct call pagh - repository interface call, repository neH data stored chay' Sov.
interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
}
concrete implementation ORM lo' laH:
class PostgresUserRepository implements UserRepository {
constructor(private db: Pool) {}
async findById(id: string): Promise<User | null> {
const { rows } = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
return rows[0] ? mapRowToUser(rows[0]) : null;
}
async findByEmail(email: string): Promise<User | null> {
const { rows } = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
return rows[0] ? mapRowToUser(rows[0]) : null;
}
async save(user: User): Promise<void> {
await this.db.query(
'INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET email = $2, name = $3',
[user.id, user.email, user.name],
);
}
}
service code UserRepository neH depend, Pool pagh SQL direct pagh:
class RegisterUserService {
constructor(private users: UserRepository) {}
async register(email: string, name: string): Promise<User> {
const existing = await this.users.findByEmail(email);
if (existing) throw new Error('email already registered');
const user = new User(crypto.randomUUID(), email, name);
await this.users.save(user);
return user;
}
}
pro
real database pagh testability. UserRepository fake implementation write trivial, test containers pagh, migrations pagh, network pagh:
class InMemoryUserRepository implements UserRepository {
private users = new Map<string, User>();
async findById(id: string) {
return this.users.get(id) ?? null;
}
async findByEmail(email: string) {
return [...this.users.values()].find((u) => u.email === email) ?? null;
}
async save(user: User) {
this.users.set(user.id, user);
}
}
RegisterUserService tests milliseconds Daq Qu', infrastructure poQ pagh.
storage really choH vaj real seam. Postgres DynamoDB migrate pagh caching layer reads qaSpa' chel vaj repository interface wa' place choH - callers Hoch unaffected (storage mechanism depend pagh).
query conventions enforce single place. soft-delete filtering, tenant scoping multi-tenant, consistent column mapping - repository centralize vaj caller Hoch automatically (call site Hoch qaw poQ pagh).
con
change rarely happen abstraction. applications few database engine swap. repository pattern biggest selling point ("persistence choH business logic touch pagh") insurance event (projects law' never occur). cost day Hoch pay (extra interface, extra implementation, extra layer) benefit never collect.
modern ORMs raw SQL isolate already. repository pattern original motivation: SQL business logic vo' keep out. ORM query builder already. ORM call repository method wrap (forward neH) layer chel, isolation chel pagh:
// This "repository" method isn't abstracting anything the ORM wasn't already abstracting.
async findById(id: string) {
return this.db.user.findUnique({ where: { id } });
}
query name hide cost. orderRepository.findActiveWithLineItems() call site Daq innocent. wa' indexed query pagh N+1 loop line items implementation laD qaSpa' invisible. abstraction reason easier supposed performance characteristics harder see.
test suites law' database swap poQ pagh. test containers real Postgres Docker Daq fast reliable - "test real database pagh" weaker argument. real thing test bad index, constraint violation, real query plan catch (in-memory fake catch laHbe').
middle ground
small-to-medium application wa' database (swap pagh) vaD thin data-access layer ORM query builder directly build (repository interface pagh) organizational benefit most (queries centralized) indirection extra pagh:
// db/users.ts — plain functions, no interface, no DI required
export async function findUserByEmail(db: PrismaClient, email: string) {
return db.user.findUnique({ where: { email } });
}
export async function saveUser(db: PrismaClient, user: User) {
return db.user.upsert({
where: { id: user.id },
update: user,
create: user,
});
}
tests test database swap pagh ORM transaction-rollback-per-test pattern lo' (swapping enable interface pagh).
nuq lo'
- library pagh module multiple backends support poQ (self-hosted app Postgres 'ej SQLite).
- domain logic complex, zero I/O unit tests neH, real test database slow pagh unavailable CI Daq.
- Domain-Driven Design deliberately rich domain model (persistence exist Sov pagh).
pIm: repository interface exactly wa' implementation - complexity credit pay, future (projects law' arrive pagh).