Martin Joo

« back published by @mmartin_joo on March 8, 2022

Domain-Driven Design with Laravel - Repositories

"The repository pattern abstracts the data store and enables you to replace your database without changing your business code." - Every Java and C# developer

This is the worst argument in favor of the repository pattern. This is probably one of the most divisive topics in the Laravel community. Some people even hate them.

In this article I try to explain why this pattern is so misunderstood (spoiler alert: it's because the Java and C# tutorials) and how you can use them effectively.

I'll also show you how can you write repositories that are:

  • One abstraction level above your models.
  • One abstraction level below your models.
  • Exactly at the same level as your models.

What Is Domain-Driven Design or DDD?

Domain-Driven Design is a software development approach that tries to bring the business language and the source code as close as possible.

I think that's the most simple yet correct definition of DDD. To be a little bit more specific it achieves this by making every important "stuff" a first-class citizen. If "Change invoice status" or "Increase order item quantity" are two sentences that your business people say a lot then you probably should have

  • ChangeInvoiceStatus
  • IncreaseOrderItemQuantity

classes somewhere in your application. That's one example of being a first-class citizen.

Now let's say you're working on some server management / monitoring application. Maybe a commercial one, or some internal tool for your own projects. What is the single most important "stuff" in this application? In my opinion, it's the server status. If it says "Healthy" everyone is happy, but if it says "Down" you know you're about to have a hard day. We know it's important but still, it's just a string in our database table. It's the 15th attribute on our Server model. And we're changing it from "Healthy" to "Deploying" in the 873rd line of some random Command or Job, or in a Model somewhere, or even worse in a Controller. Instead of having these string values and changing them, we can have classes like:

  • ServerStatus/Healthy
  • ServerStatus/Down
  • ServerStatus/Deploying

We can also have transitions like:

  • HealthyToDeploying

So this is what I mean by making things, first-class citizens. This is what Domain-Driven Design is all about (and a little bit more, but now this is the important thing).

What Is a Repository in the Domain-Driven Design World?

Repository is a simple class that contains database queries. This is your database layer, no more no less. Here's an example:

class OrderRepository
{
    /**
     * @param Collection<Product> $products
     */
    public function create(OrderData $orderData, Collection $products): Order
    {
        $order = Order::create([
            'vat_id' => $orderData->vatId,
            'state_class' => DraftOrderState::class,
        ]);

        $this->addItems($order, $orderData->orderItems, $products);
        return $order;
    }

    /**
     * @param Collection<OrderItemData> $orderItemDatas
     * @param Collection<Product> $products
     * @return Collection<OrderItem>
     */
    private function addItems(
        Order $order,
        Collection $orderItemDtos,
        Collection $products
    ): Collection {
        return $orderItemDtos
            ->map(fn (OrderItemData $item) => $this->createOrderItem(
                $order,
                $item,
                $products->firstWhere('id', $item->productId)
            ));
    }
}

As you can see there is nothing special about a Repository class really. Just a simple class with methods that interact with the database. This is how we can use it:

class CreateOrderAction
{
    public function __construct(
        private OrderRepository $orders,
        private ProductRepository $products
    ) {}

    public function execute(OrderData $orderData): Order
    {
        return DB::transaction(function () use ($orderData) {
            $products = $this->products->getByIds($orderData->orderItems->pluck('productId'));
            return $this->orders->create($orderData, $products);
        });
    }
}

They can be injected which is really useful. Here the ProductRespository is another repository class that defines some getters like the getByIds() method. This is not necessary, you can write directly:

public function execute(OrderData $orderData): Order
{
    return DB::transaction(function () use ($orderData) {
        $products = Product::whereIn($orderData->orderItems->pluck('productId'));
        return $this->orders->create($orderData, $products);
    });
}

In fact I do think that the second one is a better approach, but this example comes from a real application and this is the way it's implemented.

What’s the Problem With Repositories in Laravel?

Maybe you already know that, but the repository pattern is somewhat hated in the Laravel community. But I think they are hated for the wrong reasons.

This pattern originally comes from (I think) Java and / or C#. In my opinion these languages love to overcomplicate things compared to PHP and Laravel. I show you how I should implement these classes if I was following a Java tutorial:

// 1. Create an interface
interface IOrderRepository
{
    /**
     * @param Collection<Product> $products
     */
    public function create(OrderData $orderData, Collection $products): Order;
}

// 2. Implement the class
class OrderRepository implements IOrderRepository
{
    /**
     * @param Collection<Product> $products
     */
    public function create(OrderData $orderData, Collection $products): Order
    {
        // ...
    }
}

// 3. Bind it in the AppServiceProvider
class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app->bind(
            IOrderRepository::class, 
            OrderRepository::class
        );
    }
}

// 4. Use it, but now we are injecting the interface
class CreateOrderAction
{
    public function __construct(
        private **IOrderRepository** $orders
    ) {}

    // ...
}

As you can see there are a lot of noise here:

  • We need an interface for every repository
  • We need an actual implementation
  • And we also need to bind every interface

Why they do it like this?

The main reason is always this: "This way the pattern abstracts the data store and enables you to replace your database without changing your business code."

This means that if I want to switch to Mongodb I can write my new repository classes but I don't need to change my controllers, my actions and so on because I'm only using the interface in these classes. So in theory switching to Mongodb means:

  • I write the new MongoOrderRepository
  • This class implements the IOrderRepository interface
  • I change the bindings in the AppServiceProvider

And I'm good to go. This is the reason behind the interfaces. Now, here's my opinion on it: this is number one bullshit.

In my career I faced with a lot of, let's say interesting feature requests, but two of them never came up:

  • Nobody ever asked me to change the programming language in an existing project
  • Nobody ever asked me to change the database under a running project

By the way we use Eloquent which works with MySQL, Postgres and SQLite (the important ones). So if I want to switch to SQLite (for example in CI tests) or Postgres I can do it without any extra work. (It's a bit of an oversimplification but I think you got the point).

To sum it up:

  • I don't use interfaces
  • I don't want to change my database

However, there are some real problems with repository classes, for example:

  • At the end of the day you just move a query from A to B. Or to be more specific you move a query from your model to your repository.
  • Because of this, you might end up with huge repository classes.
  • They don't feel like Laravel. It's highly subjective of course, but still it can be a problem for some people.
  • I think in Laravel you have a better option. You can write custom query builders.

Why Should I Use the Repository Pattern in Laravel?

As you can see there are some problems with repositories. However they have some benefits as well:

  • You can create an abstraction level above your models
  • You can create an abstraction level below your models

What are these abstraction levels?

One Abstraction Level Above Your Models

Let's say you work on some big enterprise application that contains some HR and business related logic. You have an issue tracker module where employees can submit issues or requests similar to Gitlab or Trello. Let's make some assumptions:

  • This is a monolith application
  • Overall you have ~300 database tables (and models)
  • The issue tracker module is simple
  • It contains only 5 tables (and models)

If you write your queries in your controllers you face obvious problems: they are not reusable (sometimes it's not a problem) and they are all over the place.

If you write your queries in your models they are reusable but they are all over the place. In my experience in a simple module like this it's just annoying that I need navigate between 3-5 files just to see what's happening behind an API endpoint.

If you write your queries in repositories you can create an IssueTrackerRepository that contains all the queries associated with these 5 tables in a few hundred lines of code. For example:

class IssueTrackerRepository
{
    public function createIssue(IssueData $data): Issue
    {
        // ...
    }

    /**
     * @return Collection<Issue>
     */
    public function getIssues(string $status): Collection
    {
        // ...
    }

    /**
     * @return Collection<IssueCategory>
     */
    public function getCategories(): Collection
    {
        // ...
    }

    /**
     * @return Collection<IssueAttribute>
     */
    public function getAttributes(): Collection
    {
        // ...
    }
}

I think this approach has benefits when the domain (in this example the issue tracker) is quite simple:

  • It's easy to understand
  • Everything in one place, but you don't have monster classes
  • Easy to understand by new developers

But it doesn't scale very well. What if you have an Invoice module that is extremely complicated?

One Abstraction Level Below Your Models

Now let's talk about complicated modules. You have an Invoice model that is 5000 lines long. How repositories can help in this situation?

Invoices have statuses. In some application it means 10 or even more different statuses. Usually each status requires different business logic and different queries.

If you write your queries in your controllers you face even bigger problems than before with the issue tracker. You spread 5000 line of queries in your controllers, you will duplicate yourself, it will cause bugs. You know how it goes.

If you write your queries in your models they are reusable but they you have a 5000 lines long Invoice. And you probably will duplicate yourself and it will cause bugs. You know how it goes.

If you write your queries in repositories you can create multiple repositories like:

  • DraftInvoiceRepository
  • PendingInvoiceRepository
  • PaidInvoiceRepository

So you can structure your classes around statuses. By the way this is just one solution. If you have a lot of state you probably want to use States and Transitions. You can check out this article.

Now let's say for a moment that statuses not a problem. You have 3 different invoice type. In this case you can create repositories for these types:

  • CreditInvoiceRepository
  • DebitInvoiceRepository
  • MixedInvoiceRepository

Each of these classes contain queries for only credit invoices or draft invoices or paid invoices and so on.

You have these separate classes but you still can (and probably should) have a base class: the InvoiceRepository. This class contains queries for every type of invoices.

What’s the Difference Between a Repository and a Service Class?

In a project where I have services and repositories I usually follow these rules:

  • Repository contains database queries. Nothing else, it's a strict rule.
  • Service contains everything "above" that. For example:
    • Sending emails
    • Dispatching jobs
    • Communicating with 3rd party APIs

As simple as that. So repository is the database layer and service is the layer on top of that.

Here's a simple example:

class OrderService
{
    public function __construct(
        private readonly OrderRepository $order,
        private readonly StripeService $stripe
    ) {}

    public function create(OrderData $data): Order
    {
        try {
            $order = $this->orders->create($data);
            $this->stripe->processPayment($order);
            OrderProcessedNotification::dispatch($data->user);
            
            // ...
            
            return $order;  
        } catch (Exception) {
            // ...
        }
    }
}

class OrderRepository
{
    public function create(OrderData $data): Order
    {
        return Order::create([
            // ...
        ]);
    }
}

You can see that the Repository method is one step of the whole process that contained in the Service.

Because Repositories are in my opinions a bit misunderstood and hated in the Laravel community another approach is to skip the Repository class and write the query in the Service. That looks like:

class OrderService
{
    public function __construct(
        private readonly StripeService $stripe
    ) {}

    public function create(OrderData $data): Order
    {
        try {
            $order = Order::create([
                // ...
            ]);

            $this->stripe->processPayment($order);
            OrderProcessedNotification::dispatch($data->user);
            
            // ...
            
            return $order;  
        } catch (Exception) {
            // ...
        }
    }
}

In this hypothetical example of course the second approach looks more clean. But the real world is never that simple.

So what approach should you use? As always there is no magic answer, but a good rule of thumb:

  • Using repository in a simple project is overkill. You should go with only services. Or if the project is really dead-simple just make your queries in your models or controllers. There's nothing wrong with that.
  • Using repository in a real-world project that is developed since 2012 and has 500 tables is a very good approach in my opinion.

Should I Use Cache in a Repository Class?

On last question that often comes up: should I use cache in the repository? I think the answer is a clear yes. If you search the web you can find some over-engineered Java or C# example where they create:

  • IOrderRepository
  • OrderRepositry
  • CachedOrderRepository

The CachedOrderRepository decorates the base class and implements cache behavior. Don't do this in Laravel. You can use cache like this:

class OrderRepository
{
    public function getOrders()
    {
        return Cache::remember('orders', 60 * 60, function () {
            // ...
        });
    }
}

You only need 1 class and 1 method.

Conclusion

To sum it up I think that the Repository "pattern" can be useful in some project. Especially in legacy ones. At the end of the day it's just a very simple class that contains your queries, but it gives the following benefits:

  • It gives you a nice, layered architecture.
  • You have leaner models but still reusable queries.
  • You can create a repository one level above your models. One repository has many models.
  • You can create a repository one level below your models. One model has many repositories.
  • Or you can create a repository at exactly the same level as your models. One model belongs to one repository.
  • You don't need to overcomplicate. Don't follow the Java / C# tutorials. We are the Laravel community, we love simplicity.

But there are some disadvantages as well:

  • At the end of the day you just move a query from A to B and you can still end up with huge repository classes.
  • They don't feel like Laravel.
  • I think in Laravel you have a better option. You can write custom query builders.
Domain-Driven Design with Laravel