0% прочитано

When a Repository Actually Makes Sense in Laravel DDD

A Repository is not needed for every Eloquent model. In DDD, it makes sense as an aggregate persistence boundary or when the application layer should not know persistence details.

17 жовтня 2026 р. 11 хв читанняLaravel

When I first looked seriously at the Repository pattern in Laravel, the formula seemed simple: if there is an Eloquent model, there should be a repository next to it.

Over time I moved away from that. If a repository only repeats find(), save(), and delete(), it often adds another layer without adding a real boundary.

In DDD, a Repository became useful to me in a more specific role: as an aggregate persistence boundary.

This connects directly to the article about aggregates and consistency boundaries: if an aggregate protects an invariant, a repository can load and persist that unit as a whole.

A Repository makes sense not because a table exists, but because the application layer needs a domain-oriented persistence operation instead of knowledge about Eloquent, joins, or a concrete schema.

A Repository per Model Quickly Becomes Duplication

The repository only repeats the Eloquent API
final class ProductRepository{    public function find(int $id): ?Product    {        return Product::query()->find($id);    }     public function save(Product $product): void    {        $product->save();    }     public function delete(Product $product): void    {        $product->delete();    }

Such a class may still be useful in a particular codebase, but its existence alone does not create a DDD benefit.

It does not hide complex persistence, represent an aggregate, or give the application layer a new language. It merely forwards calls.

A Repository Becomes Useful at the Aggregate Persistence Boundary

The contract describes aggregate operations rather than table operations
interface ProductRepository{    public function get(        ProductId $id,    ): Product;     public function getForUpdate(        ProductId $id,    ): Product;     public function save(        Product $product,    ): void;}

Here I care about Product as an aggregate rather than the products table.

Its persistence may include several tables, related records, or mapping. The Application Service should not know those details.

The Application Service Coordinates, the Repository Persists

In the article about Application Service and Domain Service I wrote that an Application Service coordinates a use case rather than owning every rule.

The same applies to persistence. If the service knows which relations to eager load, which tables to join, and which records to update separately, the boundary is already leaking.

The Application Service sees the aggregate and use-case intent
final class ActivateProduct{    public function __construct(        private ProductRepository $products,    ) {    }     public function handle(        ProductId $productId,    ): void {        DB::transaction(function () use (            $productId,        ) {            $product =                $this->products->getForUpdate(                    $productId,

A Repository Should Not Be a Universal Query Builder

If Repository copies the Builder, the boundary becomes unclear
$repository    ->whereStatus('active')    ->whereCountry('UA')    ->withRegistrations()    ->orderByName()    ->paginate(50);

For lists, filters, sorting, and API reads, I often do not use a Repository at all.

In the article about Read Models in DDD I wrote that reads do not need to reconstruct the domain model. Query Builder or a dedicated query object is often simpler and more honest here.

A Repository Can Hide Complex Mapping

The application layer sees the aggregate, not the storage schema
Application Service        |        vProductRepository        |        +-- products        +-- product_settings        +-- product_rules        +-- mapping        |        vProduct aggregate

This is especially useful when the persistence model and domain model do not match one-to-one.

The repository can assemble an aggregate from several records and persist it correctly without forcing application code to know that schema.

A Repository Interface Is Not a Mandatory Ritual Either

I also stopped automatically creating an interface plus implementation for every repository.

An interface is useful when it genuinely separates application or domain code from an infrastructure implementation. If it separates nothing meaningful, the extra interface may be ceremony.

The Repository Pattern Can Also Be Architectural Decoration

In the article about why DDD does not start with folders I wrote that correctly named folders do not create DDD by themselves.

The same is true for repositories: correctly named classes do not help if they only duplicate the ORM and express no aggregate persistence boundary.

When I Do Not Need a Repository

I usually do not introduce a separate Repository when:

  • it is simple CRUD without meaningful domain rules;
  • the Eloquent model is already sufficient for the use case;
  • the Repository would only repeat the ORM API;
  • the operation is a pure read for a list or API;
  • there is no real persistence boundary to hide.

When a Repository Is Justified for Me

Strong Repository signals for me are:

  • the Application Service works with an aggregate rather than a table;
  • loading the aggregate requires several persistence records;
  • storage details should not leak into the application layer;
  • a critical use case needs an explicit method such as getForUpdate();
  • the domain model and Eloquent persistence model start diverging;
  • the aggregate must be persisted as one consistent unit.

In AVN Center, I Stopped Treating Repository as a Mandatory Template

While working on AVN Center, I started seeing Repository not as a "proper DDD folder" but as a concrete boundary.

If a use case needs to obtain an aggregate, execute domain behavior, and persist the result without knowing persistence details, a Repository works very well.

If the task is simply returning a filtered table, I do not force it through the same abstraction. I intentionally do not expose internal AVN Center repositories or aggregates here.

My Short Check Before Creating a Repository

Before creating a Repository, I ask:

  • Am I persisting an aggregate or simply a row?
  • Does the application layer know too much about Eloquent or SQL?
  • Will the Repository hide real complexity rather than add delegation?
  • Is this a write-side problem rather than an ordinary read?
  • Does the contract express domain intent?
  • Will this boundary make the code easier to understand?

A Repository Is About the Right Boundary, Not Every Model

For me, a Repository in Laravel DDD stopped being a mandatory layer between controllers and Eloquent.

Its value appears where the application layer works with an aggregate and should not know how that aggregate is assembled or persisted.

For simple CRUD and read-side queries, direct Eloquent or Query Builder is often more honest and simpler.

I do not add a Repository because I see a model. I add one when I see a persistence boundary worth making explicit.

A repository for every Eloquent model adds a pattern. A repository for an aggregate adds a boundary. That is the important difference for me.