The Service / Repository Pattern

January 21, 2023

In this post, I want to discuss the Service / Repository Pattern - what it is, why it’s used, and why (IMHO) it can be over-used.

What is the Service / Repository Pattern?

Service Repository Pattern Diagram

The idea of this pattern is that your code (top level code) should be completely ignorant of the business and data layers of your application. That is, if you’re retrieving a sales order, your code should ideally look something like this:

var salesOrder = salesOrderService.GetSalesOrder(12);

What does GetSalesOrder do? You don’t need to know - you have a service for that purpose. However, let’s follow down the rabbit hole and see what we see.

The service method may look like this:

GetSalesOrder(orderNumber) {
    salesOrder = repository.GetSalesOrder(orderNumber);

    if (salesOrder.IsAlert()) {
        notificationService.SendAlert(salesOrder.OrderNumber);
    }

    return salesOrder;
}

Okay, so the service provides some functionality for us: it gets the sales order from the repository, and then, conditionally, sends an alert. Let’s see what the repository method looks like:

GetSalesOrder(orderNumber) {
    var order = DB.RunSql(
        "SELECT * FROM SALES_ORDER WHERE ORDER_NUMBER = @OrderNumber", 
        orderNumber);

    var customer = DB.RunSql(
        "SELECT * FROM CUSTOMER WHERE CUSTOMER_ID = @CustomerId",
        order.CustomerId);

    return new { order, customer };
}

The repository here is looking in two separate DB tables and returning the result. This is an example of a good use of the service / repository pattern: each part of the stack fulfils it’s own responsibility.

Dependency Injection

What does this pattern give you? Well, the archetypal example when this question is asked is that you “may wish to change your RDBMS”. In fact, this is incredibly unlikely to happen; what’s far more likely, is that you’ll want to re-use your repository layer elsewhere in your program - perhaps from a different service.

The pattern relies on the concept of dependency injection. There’s little point in separating the three layers if you simply instantiate a repository inside the service.

Discussion around uses of the pattern

As with many things, it’s easy to misunderstand, or mis-use this pattern. I’m not classing these all as “mis-use”, but I want to discuss four controversial aspects of this pattern: double repository, pass-through service, responsibility bleed, and external access.

Double Repository

This is the situation where you have a repository, and then you implement another one on top. Most commonly, you find this with Entity Framework - which is, effectively, a repository of it’s own. Often, you’ll find EF used within a repository class of it’s own; whilst this is generally not useful, there are times where it can be. For example, imagine that you decide to switch from using EF to Dapper - that would mean taking the DB context that your service layer is using and replacing it with a repository.

Pass-through Service

Imagine the following service method:

GetSalesOrder(orderNumber) {
    salesOrder = repository.GetSalesOrder(orderNumber);

    return salesOrder;
}

The question here is: what does this do for you? In this case, it does absolutely nothing; it’s a bit like using a property instead of a public variable: it provides a wrapper that you can use if you need to. There’s definitely an argument for using it for consistency; if your application code is referencing services and repositories together, then it makes the code less clear.

Obviously, the ideal situation is if the service is giving you some business logic; although it’s worth watching out for responsibility bleed.

Responsibility Bleed

Responsibility bleed is the situation where one layer starts to take responsibility from the next layer. This is by far the worst transgression when using this pattern; the reason being that you add the complexity (the cost) of managing the separate layers, but you tightly couple them, such that the repository relies on the service. Let’s see a couple of examples:

GetSalesOrder(orderNumber) {
    salesOrder = repository.GetSalesOrder(orderNumber);
    if (salesOrder.OrderNumber.StartsWith("a")) {
        salesOrder.Notes = repository.GetNotes(orderNumber);
    }

    if (salesOrder.IsAlert()) {
        notificationService.SendAlert(salesOrder.OrderNumber);
    }

    return salesOrder;
}

This seems innocuous (I’ll show a more glaring example next), but what this is saying is that, based on the data that is retrieved, we should, or should not, retrieve additional data. Clearly, which data is retrieved, and how it’s retrieved is the responsibility of the repository, not the service. If we imagine this to be a facet of the data (as it appears to be) then anywhere that retrieves an order will need this logic: which means that you can’t call the repository without the service.

I imagine there are some that argue that the service and repository can be tightly coupled, and that’s fine. My only question would be, if that is the case, why maintain two levels of abstraction?

Let’s now look at a more obvious example of this:

CreateSalesOrder(salesOrder, salesOrderLines) {

    repository.StartTransaction();

    repository.CreateSalesOrder(salesOrder);
    repository.CreateSalesOrderLines(salesOrderLines);

    repository.CommitTransaction();    
}

Clearly, the transaction belongs to the repository, and has no business being anywhere near the service. As in the previous example, the service and repository are now inextricably linked.

External Access

This was something that took me a while to get my head around, but let’s imagine that we’re calling an external API. Surely that call should be treated the same as a database call (they both call out to things that are not in the system). So why would we not create a repository that sits in front of the API call, and deals with it in the same manner as though it were fetching or writing data to our database?

There are a number of advantages to this, but my favourite is that the rest of the system remains totally agnostic of the source of the data - so you can swap it out at any time.

Disclaimer

Any resemblance to actual code of any programming language, real or imagined, in this post is purely co-incidental.

All the opinions in this post are my own, as are the names that I’ve given for these discussion points.



Profile picture

A blog about one man's journey through code… and some pictures of the Peak District
Twitter

© Paul Michaels 2024