Repository Pattern — Data Access Abstraction (2026)
In this tutorial, you'll learn how the repository pattern decouples business logic from data storage, why it makes testing easier, and how real applications swap database implementations without changing business code.
Imagine you run a library. Readers come to you with a book title, and you retrieve it from wherever books are stored — maybe shelf A today, shelf B tomorrow after a reorganization. The reader doesn't need to know where you keep the books. They just hand you a request and get a book back. The repository pattern does the same for data: your business code asks for a user, and the repository handles the "where" and "how" of fetching it.
Core Concept
The repository pattern introduces an abstraction layer between business logic and data access. Instead of calling a database directly, business code talks to a repository interface. The actual implementation — SQL database, in-memory list, REST API, file system — is hidden behind that interface.
// The interface — business code depends on this
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
// Business layer only knows the interface
class UserService {
constructor(private repo: UserRepository) {}
async updateEmail(id: string, email: string) {
const user = await this.repo.findById(id);
if (!user) throw new Error("User not found");
user.email = email;
await this.repo.save(user);
}
}
How It Works
The repository sits between the business layer and the data source. Business code calls repository methods like findById or save. The repository translates these into the appropriate data operations — SQL queries, HTTP calls, or in-memory lookups.
Business Code → Repository Interface → Repository Implementation → Data Source
Here's a complete example with two repository implementations:
// In-memory implementation — great for tests
class InMemoryUserRepository implements UserRepository {
private users: Map<string, User> = new Map();
async findById(id: string): Promise<User | null> {
return this.users.get(id) ?? null;
}
async save(user: User): Promise<void> {
this.users.set(user.id, user);
}
async delete(id: string): Promise<void> {
this.users.delete(id);
}
}
// PostgreSQL implementation — used in production
class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
const result = await pool.query(
"SELECT * FROM users WHERE id = $1", [id]
);
return result.rows[0] ?? null;
}
async save(user: User): Promise<void> {
await pool.query(
`INSERT INTO users (id, name, email)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE
SET name = $2, email = $3`,
[user.id, user.name, user.email]
);
}
async delete(id: string): Promise<void> {
await pool.query("DELETE FROM users WHERE id = $1", [id]);
}
}
Expected output: Both implementations satisfy the same interface. The UserService works identically with either one. No business code changes when switching from in-memory to Postgres.
Real-World Examples
Unit Testing with Mock Repositories
In Durga Antivirus Pro, the signature scanning engine uses the repository pattern. During development, test suites use an InMemorySignatureRepository that loads test signatures from a JSON file. Production uses a PostgresSignatureRepository. The scanning engine never knows the difference — and tests run in milliseconds without a database.
Multi-Provider Support
DodaZIP supports cloud storage backends (S3, Google Cloud, local file system) through a repository abstraction. Adding a new storage provider means writing one new class that implements the FileRepository interface. Zero changes to the compression or decompression logic.
Pros & Cons
| Pros | Cons |
|---|---|
| Business logic is database-agnostic | Adds extra interfaces and classes |
| Unit tests use fast in-memory implementations | ORMs already provide some abstraction |
| Swap databases without touching business code | Can feel like over-engineering for simple CRUD |
| Clear boundary between data access and logic | Leaky abstractions if the repository exposes DB-specific types |
When to Use
The repository pattern shines when:
- You need to test business logic without a database
- Your application might switch databases (prototype with SQLite, deploy with Postgres)
- Multiple data sources exist — the repository hides whether data comes from a DB, cache, or external API
- You want clean separation between domain concerns and infrastructure
Skip it for simple applications where an ORM's built-in abstraction is sufficient, or when you're building a throwaway prototype.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro