Repository Pattern
The Repository Pattern is a software design pattern used to separate data access logic from business logic. It creates a dedicated layer responsible for retrieving, storing, updating, and deleting data, while the rest of the application works with clear methods instead of direct database queries.
This pattern is commonly used in object-oriented applications, backend systems, APIs, Laravel projects, Symfony applications, and enterprise software. It helps developers organize database operations, reduce duplication, improve testability, and keep business code clean.
Introduction
In many applications, data is stored in a database. Developers need to create users, find products, update orders, delete records, filter results, paginate lists, and perform complex queries. If all of this logic is written directly inside controllers or services, the project becomes harder to maintain.
The Repository Pattern solves this problem by placing data access operations inside repository classes. A repository acts as a middle layer between the application logic and the data source.
Instead of writing database queries everywhere, the application calls methods such as findById, findByEmail, getActiveUsers, save, update, or delete. The repository handles how the data is actually retrieved or stored.
What Is the Repository Pattern?
The Repository Pattern is a design pattern that provides an abstraction over data access. It hides the details of how data is stored and retrieved, and exposes simple methods that the rest of the application can use.
In simple terms, a repository is a class responsible for talking to the database or data source. The business logic does not need to know whether the data comes from MySQL, PostgreSQL, MongoDB, an API, a cache system, or a file.
For example, a UserRepository may provide methods such as findByEmail and create. The authentication service can use these methods without knowing the exact SQL query or ORM details.
Why the Repository Pattern Is Important
The Repository Pattern is important because it improves separation of concerns. Controllers should not contain database queries. Business services should not be full of raw SQL or ORM-specific details. Data access should be organized in a dedicated layer.
When database logic is scattered across the application, any change in the data structure or query rules becomes difficult. Developers may need to update many files, and it becomes easy to introduce bugs.
With repositories, data access is centralized. This makes the code easier to maintain, easier to test, and easier to refactor.
Problem Without Repository Pattern
Imagine a controller that handles user registration and directly uses database queries or ORM calls:
class RegisterController
{
public function register(Request $request)
{
$existingUser = User::where('email', $request->email)->first();
if ($existingUser) {
throw new RuntimeException('Email already exists.');
}
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => password_hash($request->password, PASSWORD_BCRYPT),
]);
return $user;
}
}This may work in a small application, but the controller now knows too much about data access. If the user lookup logic changes, or if the application needs to add conditions such as active status or tenant ID, similar queries in many files may need updates.
The Repository Pattern moves this responsibility into a UserRepository.
Basic Repository Pattern Example in PHP
A simple repository can be created with an interface and a concrete implementation.
interface UserRepositoryInterface
{
public function findById(int $id): ?User;
public function findByEmail(string $email): ?User;
public function save(User $user): User;
public function delete(User $user): bool;
}This interface defines what the repository can do. It does not define how the data is stored or retrieved.
Now we can create a concrete implementation:
class UserRepository implements UserRepositoryInterface
{
public function findById(int $id): ?User
{
return User::find($id);
}
public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
public function save(User $user): User
{
$user->save();
return $user;
}
public function delete(User $user): bool
{
return $user->delete();
}
}The implementation uses an ORM model, but the rest of the application can depend on the interface instead of depending directly on ORM queries.
Using the Repository in a Service
A service class can use the repository through dependency injection:
class UserRegistrationService
{
public function __construct(
private UserRepositoryInterface $users
) {
}
public function register(array $data): User
{
$existingUser = $this->users->findByEmail($data['email']);
if ($existingUser) {
throw new RuntimeException('Email already exists.');
}
$user = new User();
$user->name = $data['name'];
$user->email = $data['email'];
$user->password = password_hash($data['password'], PASSWORD_BCRYPT);
return $this->users->save($user);
}
}The service now focuses on business logic. It checks whether the email exists, creates the user object, and asks the repository to save it. The service does not need to know the exact database query.
Main Parts of the Repository Pattern
The Repository Pattern usually includes these main parts:
Repository interface: Defines the methods available for data access.
Concrete repository: Implements the interface using a database, ORM, API, or another data source.
Entity or model: Represents the data object used by the application.
Service or use case: Uses the repository to perform business operations.
This structure helps separate business behavior from persistence details.
Repository Pattern and Data Access Logic
Data access logic includes queries, filters, pagination, joins, sorting, persistence operations, and data retrieval rules. These operations often change as the application grows.
Repositories provide a dedicated place for this logic. For example, instead of repeating active user queries everywhere, the UserRepository can define a method called getActiveUsers.
public function getActiveUsers(): array
{
return User::where('active', true)
->orderBy('created_at', 'desc')
->get()
->all();
}Now the application can call getActiveUsers without repeating the query in many places.
Repository Pattern in Laravel
Laravel uses Eloquent ORM, which already provides many data access methods. Because of this, some developers debate whether Repository Pattern is always needed in Laravel.
In small Laravel projects, using Eloquent directly may be enough. However, in larger applications, repositories can still be useful when queries become complex, when data access needs to be tested separately, or when the application needs to depend on interfaces instead of Eloquent models directly.
Repository Pattern in Laravel is most useful when the project has a service layer, complex business rules, multiple data sources, or a clean architecture approach.
Laravel Repository Interface Example
In a Laravel project, a repository interface can be placed inside an app/Repositories or app/Contracts folder.
namespace App\Repositories;
use App\Models\User;
interface UserRepositoryInterface
{
public function findByEmail(string $email): ?User;
public function create(array $data): User;
public function getActiveUsers(int $limit = 20);
}The interface defines the contract used by services and controllers.
Laravel Repository Implementation Example
namespace App\Repositories;
use App\Models\User;
class EloquentUserRepository implements UserRepositoryInterface
{
public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
public function create(array $data): User
{
return User::create($data);
}
public function getActiveUsers(int $limit = 20)
{
return User::where('active', true)
->latest()
->paginate($limit);
}
}This implementation uses Eloquent. If the data source changes later, another implementation can be created without changing the service interface.
Binding Repository in Laravel Service Container
Laravel's service container can bind the interface to the concrete repository class:
use App\Repositories\UserRepositoryInterface;
use App\Repositories\EloquentUserRepository;
public function register(): void
{
$this->app->bind(
UserRepositoryInterface::class,
EloquentUserRepository::class
);
}After this binding, Laravel can automatically inject EloquentUserRepository whenever a class requires UserRepositoryInterface.
Using Repository in Laravel Service
namespace App\Services;
use App\Repositories\UserRepositoryInterface;
use App\Models\User;
class UserService
{
public function __construct(
private UserRepositoryInterface $users
) {
}
public function register(array $data): User
{
if ($this->users->findByEmail($data['email'])) {
throw new \RuntimeException('Email already exists.');
}
return $this->users->create($data);
}
}The UserService depends on the repository interface and stays independent from direct Eloquent queries.
Repository Pattern in Symfony
Symfony projects often use repositories with Doctrine ORM. Doctrine repositories are a common part of Symfony applications and are used to organize queries related to an entity.
For example, a UserRepository may contain methods such as findActiveUsers, findByEmail, or findUsersCreatedAfter. Symfony and Doctrine naturally support repository-style organization.
In larger Symfony applications, developers may also define repository interfaces and use dependency injection to decouple business services from Doctrine-specific details.
Repository Pattern with Raw SQL
The Repository Pattern is not limited to ORM tools. A repository can also use raw SQL, query builders, stored procedures, external APIs, or files.
For example, a product repository may use PDO directly:
class PdoProductRepository implements ProductRepositoryInterface
{
public function __construct(
private PDO $pdo
) {
}
public function findById(int $id): ?Product
{
$statement = $this->pdo->prepare('SELECT * FROM products WHERE id = :id');
$statement->execute(['id' => $id]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
return $data ? Product::fromArray($data) : null;
}
}The business logic still depends on ProductRepositoryInterface and does not care whether the implementation uses PDO, Eloquent, Doctrine, or an API.
Repository Pattern with External APIs
Repositories can also hide external API access. For example, a CustomerRepository may retrieve customer data from a CRM API instead of a local database.
class ApiCustomerRepository implements CustomerRepositoryInterface
{
public function __construct(
private CrmApiClient $client
) {
}
public function findByEmail(string $email): ?Customer
{
$response = $this->client->get('/customers', [
'email' => $email,
]);
return Customer::fromApiResponse($response);
}
}The rest of the application uses the same repository interface. The data source is hidden behind the repository.
Repository Pattern and Service Layer
The Repository Pattern and Service Layer Pattern are related but different.
A repository handles data access. It knows how to find, save, update, and delete data. A service handles business logic. It coordinates repositories, applies rules, validates workflows, and performs operations.
For example, UserRepository may find a user by email. UserRegistrationService may check if the email already exists, hash the password, create the user, send a verification email, and return the result.
Repositories should not become business services. Keeping this separation clear improves maintainability.
Repository Pattern vs DAO
DAO stands for Data Access Object. DAO and Repository are similar because both organize data access logic. However, they are often used with slightly different intentions.
A DAO is usually closer to the database and focuses on persistence operations. It may expose methods that match tables and queries closely.
A repository is often more domain-oriented. It represents a collection of domain objects and provides methods that make sense for the application, such as findActiveCustomers or getRecentOrders.
In practice, many projects use these terms in similar ways, but Repository Pattern is usually more connected to domain-driven design and business-oriented data access.
Repository Pattern vs Active Record
Active Record is a pattern where the model object contains both data and database operations. Laravel Eloquent is an example of Active Record.
With Active Record, developers can write User::where(...), User::create(...), or $user->save(). This is simple and productive.
Repository Pattern places data access logic in a separate repository class. This can add more structure and abstraction.
In Laravel, using repositories on top of Eloquent is a design choice. It can be useful for large projects, but it may be unnecessary for small CRUD applications.
Repository Pattern and Clean Architecture
Repository Pattern is important in clean architecture because it helps keep business logic independent from external data sources. The core application can define repository interfaces, while infrastructure code provides concrete implementations.
For example, the application layer may define OrderRepositoryInterface. The infrastructure layer may implement it using MySQL, PostgreSQL, MongoDB, or an external API.
This allows business logic to remain stable even if the database technology changes.
Repository Pattern and Testing
One of the biggest benefits of Repository Pattern is improved testability. If a service depends on a repository interface, tests can provide a fake repository instead of using a real database.
class FakeUserRepository implements UserRepositoryInterface
{
private array $users = [];
public function findByEmail(string $email): ?User
{
foreach ($this->users as $user) {
if ($user->email === $email) {
return $user;
}
}
return null;
}
public function create(array $data): User
{
$user = new User($data);
$this->users[] = $user;
return $user;
}
}This makes unit tests faster and more focused because they do not need to connect to a real database.
Common Repository Methods
Repositories often include common methods for data access. The exact methods depend on the project and the entity.
Common repository methods include:
findById
findByEmail
findAll
create
save
update
delete
paginate
getActive
findByStatus
However, repositories should not become generic dumping grounds. Methods should be meaningful for the application.
Generic Repository vs Specific Repository
Some developers create a generic repository with common CRUD methods for all models. This can reduce duplication, but it can also hide important domain-specific behavior.
A specific repository, such as UserRepository or OrderRepository, can contain meaningful methods related to that entity.
For example, OrderRepository may have findPendingOrders, findPaidOrders, or getOrdersForCustomer. These methods are clearer than using generic filters everywhere.
In many real projects, specific repositories are more expressive and easier to understand than one large generic repository.
Benefits of Repository Pattern
The Repository Pattern provides many benefits in object-oriented software development.
Main benefits include:
Separates data access logic from business logic.
Keeps controllers and services cleaner.
Centralizes queries and persistence operations.
Improves testability through interfaces and fake repositories.
Reduces duplication of database queries.
Makes it easier to change data sources.
Supports clean architecture and domain-driven design.
Improves maintainability in large applications.
These benefits are especially valuable in projects with complex data access requirements.
Drawbacks of Repository Pattern
The Repository Pattern can also have drawbacks. It adds extra classes and interfaces, which may be unnecessary for simple applications.
In frameworks such as Laravel, Eloquent already provides a powerful Active Record API. Adding repositories for every model may create duplicate methods that only wrap Eloquent without adding real value.
Another drawback is over-abstraction. If repositories are designed poorly, they can make simple queries more complicated than necessary.
Repository Pattern should be used when it improves the structure, not automatically in every project.
When to Use Repository Pattern
Use Repository Pattern when data access logic is complex, repeated, or important enough to be separated from business logic.
Repository Pattern is useful when:
Controllers or services contain too many database queries.
The same queries are repeated in multiple places.
The application needs clear separation between business logic and persistence.
You want to test services without using a real database.
The project uses clean architecture or domain-driven design.
The application may switch data sources in the future.
Queries are complex and should be organized in one place.
If these conditions exist, Repository Pattern can improve maintainability and testability.
When Not to Use Repository Pattern
Do not use Repository Pattern automatically for every simple CRUD feature. If the project is small and Eloquent or another ORM already provides clear data access, adding repositories may not be necessary.
Avoid Repository Pattern when:
The application is small and simple.
Repository methods only duplicate ORM methods without adding value.
The abstraction makes the code harder to understand.
The project does not need testing through fake repositories.
The data access logic is minimal and unlikely to change.
Good design should be practical. Repository Pattern is useful when it solves a real organization or testing problem.
Common Mistakes with Repository Pattern
One common mistake is putting business logic inside repositories. A repository should handle data access, not business workflows.
Another mistake is creating generic repositories that hide meaningful domain methods. A method like findActiveUsers is often clearer than a generic findByConditions method used everywhere.
A third mistake is creating repositories that only wrap simple ORM calls without adding value. This increases code size without improving design.
A fourth mistake is returning inconsistent data types. Repository methods should be predictable and documented clearly.
Best Practices for Repository Pattern
To use Repository Pattern effectively, developers should keep repositories focused and meaningful.
Useful best practices include:
Use repositories to organize real data access logic.
Keep business rules in services or domain classes, not repositories.
Define interfaces when testability or decoupling is needed.
Use meaningful method names based on application needs.
Avoid creating repositories that only duplicate ORM methods.
Keep query logic centralized and reusable.
Return consistent types from repository methods.
Use dependency injection to provide repositories to services.
These practices help repositories improve the design instead of adding unnecessary complexity.
Practical Checklist Before Using Repository Pattern
Before using Repository Pattern, developers can ask these questions:
Is data access logic repeated in multiple places?
Are controllers or services too full of queries?
Do I need to test business logic without a real database?
Do I need to hide ORM or database details?
Are the queries complex enough to deserve a dedicated class?
Will repository methods make the code more readable?
Am I adding abstraction for a real reason?
If the answer is yes to several of these questions, Repository Pattern may be a good design choice.
Conclusion
The Repository Pattern is a design pattern that separates data access logic from business logic. It provides a clean layer for retrieving, storing, updating, and deleting data while keeping controllers and services focused on application behavior.
Repository Pattern is useful in complex applications, APIs, Laravel projects, Symfony applications, clean architecture, and domain-driven design. It improves organization, reduces duplicated queries, supports dependency injection, and makes testing easier through fake implementations.
However, repositories should be used carefully. In small applications or simple CRUD systems, adding repositories may create unnecessary abstraction. When applied correctly, the Repository Pattern is a powerful tool for building clean, maintainable, and scalable object-oriented software.

