Repository Pattern
In this page:
Basic Repository
A repository represents a collection of domain objects and exposes operations such as finding and adding data, hiding whether that data actually lives in a SQL database, a file, or an in-memory array.
Example: Basic Repository
interface User {
id: number;
name: string;
}
class UserRepository {
private users: User[] = [];
add(user: User) { this.users.push(user); }
findById(id: number) { return this.users.find((u) => u.id === id); }
}
const repo = new UserRepository();
repo.add({ id: 1, name: "Ravi" });
console.log(repo.findById(1));
Repository Interface
A repository interface separates what the application expects (find by id, save, delete) from the concrete storage implementation behind it, so the storage technology can change without touching business logic.
Example: Repository Interface
interface User { id: number; name: string; }
interface UserRepository {
findById(id: number): User | undefined;
save(user: User): void;
}
class InMemoryUserRepository implements UserRepository {
private users: User[] = [];
findById(id: number) { return this.users.find((u) => u.id === id); }
save(user: User) { this.users.push(user); }
}
const repo: UserRepository = new InMemoryUserRepository();
repo.save({ id: 1, name: "Ravi" });
console.log(repo.findById(1));
CRUD Operations
Repositories commonly expose create, read, update, and delete operations for one domain entity, giving every part of the app a single, consistent way to interact with that entity's persistence.
Example: CRUD Operations
interface Todo { id: number; title: string; }
class TodoRepository {
private todos: Todo[] = [];
create(todo: Todo) { this.todos.push(todo); }
read(id: number) { return this.todos.find((t) => t.id === id); }
update(id: number, title: string) {
const todo = this.read(id);
if (todo) todo.title = title;
}
delete(id: number) { this.todos = this.todos.filter((t) => t.id !== id); }
}
const repo = new TodoRepository();
repo.create({ id: 1, title: "Buy milk" });
repo.update(1, "Buy bread");
console.log(repo.read(1));
Repository with Service
A service can depend on a repository interface and focus purely on business rules without knowing storage details — it just asks the repository for data and trusts it to handle the mechanics.
Example: Repository with Service
interface User { id: number; name: string; }
interface UserRepository {
findById(id: number): User | undefined;
}
class UserService {
constructor(private repo: UserRepository) {}
getName(id: number): string {
return this.repo.findById(id)?.name ?? "Unknown";
}
}
class FakeRepo implements UserRepository {
findById(id: number) { return { id, name: "Ravi" }; }
}
console.log(new UserService(new FakeRepo()).getName(1));
Testing with Repositories
Repository abstractions make testing much easier because a fake or in-memory implementation can stand in for a real database in unit tests, avoiding slow, flaky I/O while still exercising the same code paths.
Example: Testing with Repositories
interface User { id: number; name: string; }
interface UserRepository {
findById(id: number): User | undefined;
}
class InMemoryTestRepo implements UserRepository {
constructor(private users: User[]) {}
findById(id: number) { return this.users.find((u) => u.id === id); }
}
const testRepo = new InMemoryTestRepo([{ id: 1, name: "Test User" }]);
console.log(testRepo.findById(1));
Chapter Quiz — Complete all 7 topics to unlock
0/7 topics done
Complete these topics first: