← Back to TypeScript Course | Chapter 16: Advanced Patterns | Lesson 7 of 7

Repository Pattern

The Repository Pattern separates application logic from data-access details. A repository provides a clean interface for storing and retrieving objects while hiding the underlying storage mechanism.

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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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:

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.