← 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.
Syntax
typescript
interface Repository<T> {
  findById(id: type): Promise<T | undefined>;
  save(item: T): Promise<void>;
  delete(id: type): Promise<void>;
}

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));
Related Topics
{# common_mistakes/chapter_summary/browser_support: on Hindi pages the view already swaps in the hi_ translation fields (or blanks these out if untranslated), so this renders correctly for both languages without a lang_code check here. #}
Common Mistakes
  1. Leaking the storage details into callers, like exposing the internal array, which defeats the pattern.
  2. Returning User from findById when it may not exist, instead of User | undefined.
  3. Mixing business rules into the repository instead of keeping them in a service.
Chapter Summary
  • Creational patterns such as Builder, Factory, and Singleton control how objects are created in TypeScript.
  • Behavioral patterns such as Observer and Strategy organize how objects communicate and swap behavior.
  • The Decorator and Repository patterns structure how behavior is added and how data access is separated from logic.
🔒

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.