Problem Statement
The Repository Pattern is a design pattern that acts as a middleman between your application's business logic and your data storage, giving you a clean, consistent interface to access and manipulate data without caring whether it lives in a database, a file, or an API. You've probably run into that codebase where data access logic is scattered everywhere—SQL queries in controllers, raw database calls in services, and tests that break because they can't mock the database. It's messy, hard to change, and even harder to test. If you've ever spent a week untangling spaghetti just to swap out a database, you already know why this pattern exists.
Core Explanation
The Repository Pattern works by creating a clean abstraction layer that hides the messy details of data storage. Think of it like a restaurant menu—you order a "burger" (the what), and the kitchen handles the "how" (grilling, sourcing ingredients, plating). You don't need to know if the beef came from a local farm or a freezer. Similarly, a repository says "here are methods to get and save data" (like findUser(id) or saveOrder(order)), while hiding whether that data lives in PostgreSQL, MongoDB, or a simple in-memory list.
Key components:
-
Repository interface — A contract that defines all data operations (e.g.,
getAll(),getById(),save(),delete()). Your business logic depends on this interface, not on any concrete implementation. - Concrete repository — The actual implementation that talks to your specific data source. Could use raw SQL, an ORM, or a third-party API. This is the only part that changes when you switch storage backends.
- Client code — Your services, controllers, or use cases that call the repository methods. They never import a database driver—only the repository interface.
The core idea: your application logic doesn't know or care where data comes from. It just asks the repository for what it needs.
Practical Context
Use the Repository Pattern when:
- You need to unit test business logic without a real database (mock the repository instead).
- You're building an app that might switch databases (e.g., from SQLite in dev to PostgreSQL in production, or from MySQL to a REST API).
- You want to centralize data access logic (caching, logging, retries) in one place instead of sprinkling it across controllers.
Don't use it when:
- Your app is a simple CRUD wrapper with no complex business logic—a basic ORM is already your repository.
- You're building a prototype or small app that won't live long enough to justify the abstraction overhead.
- Your data source is already simple and stable (like reading from a local JSON file with no transformations).
Why you should care: Without a repository, every change to your storage layer (new database, new ORM, different API) forces you to hunt down and rewrite dozens of files. With a repository, you change exactly one file. That's it.
Quick Example
Before (no repository) — your controller directly uses an ORM:
// ordersController.ts
import { db } from './database';
function getOrder(id: number) {
return db.query('SELECT * FROM orders WHERE id = $1', [id]);
}
After (with repository) — the controller talks to an interface:
// ordersRepository.ts
interface OrderRepository {
getById(id: number): Promise<Order>;
}
// ordersController.ts (no database imports!)
function getOrder(id: number) {
return ordersRepository.getById(id);
}
What this demonstrates: The controller now works with any data source that implements the OrderRepository interface. You can swap from PostgreSQL to MongoDB, or create a mock for testing, without touching a single line of business logic.
Key Takeaway
Inject a repository interface where data meets logic, and you'll free your application from being locked to any specific storage technology. Martin Fowler's "Patterns of Enterprise Application Architecture" book covers this pattern in depth if you want to go deeper.
Top comments (0)