Opinion: Stop Using the Generic Repository Pattern

Updated March 27th, 2023

Opinion: Stop Using the Generic Repository Pattern

 

The repository pattern is a popular design pattern used in software development for separating business logic from data access. It is widely used in modern software development for creating a layer of abstraction between the application's domain logic (business rules) and the data access layer. The repository pattern exists to abstract the mechanics and details of I/O systems (databases, external systems, 3rd party APIs) and provides several benefits, including better testability, improved maintainability, and increased flexibility. 

The benefits of a repository pattern and its similar abstractions don’t stop there (deferring decisions is another great use case), but for the sake of this article we will focus on using them as a boundary between our business logic and data access so we can discuss a sadly overly common misuse of the pattern. There are two different approaches to implementing the repository pattern: the generic repository pattern and the dedicated repository pattern. In this post, we will explore the differences between the two and look at why the former could very well be an anti-pattern that you aren’t aware of, at best a leaky abstraction.


Image for the generic repository pattern section.

The Generic Repository Pattern

 

The generic repository pattern is a one-size-fits-all approach that tries to provide a generic implementation of the repository interface that can be used for all types of entities. This pattern is simple to implement and can save development time by providing a common interface for all repositories. However, it has some serious drawbacks. Let’s take a look at a typical example of the generic repository pattern.

 

Repository:

```public interface IRepository<T>```

```{```

   ``` void Add(T entity);```

   ``` void Update(T entity);```

    ```void Delete(T entity);```

  ```  IEnumerable<T> GetAll();```

   ``` T GetById(int id);```

   ```IEnumerable<T> GetByQuery(Func<T, bool> query);```

```}```

 

Usage (example in a traditional n-tier service layer):

 

```public class ProductService```

```{```

   ``` private readonly IRepository<Product> _repository;```

 

   ``` public ProductService(IRepository<Product> repository)```

```    {```

      ```  _repository = repository;```

   ``` }```

 

   ``` public IEnumerable<Product> GetAllProducts()```

  ```  {```

      ```  return _repository.GetAll();```

 ```  }```

 

  ```  public Product GetProductById(int id)```

  ```  {```

      ```  return _repository.GetById(id);```

 ```   }```

 

   ``` public void AddProduct(Product product)```

   ``` {```

      ```  _repository.Add(product);```

  ```  }```

 

  ```  public void UpdateProduct(Product product)```

  ```  {```

      ```  _repository.Update(product);```

   ``` }```

 

   ``` public void DeleteProduct(Product product)```

   ``` {```

       ``` _repository.Delete(product);```

```    }```

 

   ``` public IEnumerable<Product> GetProductsByCategory(string category)```

   ``` {```

        ```return _repository.GetByQuery(p => p.Category == category);```

 ```   }```

```}```

At face value this actually makes a lot of sense, we’re eliminating a lot of commonplace database operations for every table we could possibly have but as we start to see this play out the issues with the pattern become a little more evident:

 

  1. There is not enough control over input parameters. The Update method accepts an entire Product Entity, and we will likely never be updating an entire Product, rather we would update specific properties on a product. Depending on the implementation side of the repository pattern this could be a non-issue, or it could be a critical problem allowing properties as important as the Primary Key or OwnerId to be changed. In general, it's poor practice to accept more data than is needed to perform your operation and one size certainly does not fit all when it comes to database operations.
  2. There is too much onus on the application (or service) layer to construct valid and performant queries. Specifically with the usage of the GetByQuery() method (a huge cop-out for responsibility separation), we put the authoring of whatever potential query into the hands of a layer that should be completely agnostic of I/O mechanics. That was the whole point of the repository pattern, after all. 
  3. The generic repository pattern strongly encourages a database-first approach to software authoring. Your domain layer is (or should be) a collection of business rules agnostic of any hard dependencies (I/O included)*, these rules should be ignorant of whether they are attached to an SQL database, a cloud API, or a punch card system. They don’t care if they're being interfaces via a web UI, a console application, or an API! This code should define solutions to business (domain) problems, not serve as forwarding proxies for your database design.
  4. We cannot perform operations in the aggregate. Having a simple interface for each of your Entities is nice and all, but what happens when you need to perform operations across multiple Entities?
  5. Similar to #2 but easily the most important flaw in the generic repository pattern, it is a leaky abstraction! It needs to be restated that the entire value of the repository pattern is separation, abstraction of the business logic, and the data access, but if we take a look at what’s passed between the boundaries of our method signatures… it’s I/O entities! <T> can only ever be declared on the implementation side of the repository boundary, causing the domain layer to depend on the repository. This means that we’ve broken SRP(Single Responsibility Principle), if we introduce a change to our storage mechanism we could very well inflect change on our domain layer. Instead, a much better system is to define the boundary on the domain side with DTOs and dictate this boundary to your repository.

 

*To step down off the pedestal for a second, this does not apply to every application. This applies to applications that need to follow strict and domain-heavy architectures. Sometimes you just need a website to talk to a database, and that’s fine. There’s no silver bullet in software architecture.

 

Code image for Dedicated Repository Pattern section.

The Dedicated Repository Pattern

 

The dedicated repository pattern is an alternative approach to implementing the repository pattern. Unlike the generic repository pattern, the dedicated repository pattern provides a specific repository implementation for each entity in the application. This approach is more explicit and provides a clearer separation of concerns between the data access layer and the domain logic layer. Let’s take a look at an example.

 

Consider your domain layer has the following Use Case:

 

```public class Employee```

```{ ```

   ``` public record Retire(Guid Id);```

```}```

 

Maybe this Use Case needs to

  1. Confirm that the Employee exists
  2. Ensure that the Employee is over the required age of retirement
  3. Retire the Employee

 

Our resulting dedicated repository pattern could look something like this

 

```public interface IEmployeeRepository ```

```{```

  ```  bool DoesEmployeeExist(Guid employeeId);```

   ``` int RetirementAge();```

   ``` void RetireEmployee(Guid employeeId);```

```}```

With this approach, we have dictated the inputs and outputs (the boundary contract) on the domain layer side and leave the implementation of whatever queries are needed completely in the hands of the repository implementation. The repository implementation could be an ORM, an API, Stored Procedure, who cares! We can isolate those details in the scope where they are important and not leak them out into the world. We’ve also achieved an incredible level of isolation from any incoming or outbound sources, making our code trivial to test. It’s also easy to see from this implementation how trivial it would be to have queries in the aggregate. The methods exist for the business rules so they can overlap whatever entities the rules dictate. 


Image for Conclusion section.

Conclusion

 

The repository pattern is an effective way to separate business logic from data access, providing benefits like better testability, maintainability, and flexibility. However, the generic repository pattern, which tries to provide a one-size-fits-all solution, has some serious drawbacks, including a lack of control over input parameters, an onus on the application layer to construct queries, and a leaky abstraction. In contrast, the dedicated repository pattern, which provides a specific repository implementation for your domain, offers a clearer separation of concerns between the data access layer and the domain logic layer.

By partnering with GoodJava, you can take your information to the next level. For articles like this, read our other wonderful blogs or visit our Contact Us Page and Get Started.

 

We use cookies to ensure that you get the best experience on our website, although the cookies we use do not contain personally identifiable information. By continuing on this website or by clicking “I Accept Cookies”, you agree to the storing of cookies on your device. Learn More

I Accept Cookies