Repository pattern with Ruby on Rails: Decoupling ActiveRecord and persistence layer

Intro

The Repository pattern is a way of working with a data source. In the book Patterns of Enterprise Application Architecture, Martin Fowler describes a repository as follows:

In other words it decouples our business domain logic from the persistence layer. Some of the benefits using it are the following:

  • Increases testability and loose coupling with our persistence layer.
  • Easier to test your application logic since we test logic and not infrastructure (persistence layer). We test our business logic in isolation from external dependencies.
  • Easier to persist a domain entity since each repository implementation knows how to save the entity itself and its underlying relationships.
  • We have an abstraction without caring about our persistence layer which yields to better separation of concerns since persisting domain entities is handled by each concrete repository.
  • Each domain entity is focused exclusively in the domain model itself and its specific domain logic.
  • Better object oriented design since each repository encapsulates a collection of objects that is responsible to perform actions on them.
  • Improves code maintainability and readability by separating business logic from data or service access logic.

A simplified class diagram can be found in the figure below.

Repository pattern diagram

In order to simplify things we will take a simpler approach by removing and interfaces. Both of them deserve an article itself. In the next article we will introduce mappings between our data layer and our domain entities layer.

Dependency Injection

Dependency injection is a strong and major thing in static typed languages like Java. It connects components together at runtime providing loose coupling between them and yields to better testable code since tests depends on interfaces and not implementation details.

A simple example can be seen below where we pass a to another object’s constructor signature.

class SomeServiceHandler
def initialize(repository)
@repository = repository
end
def call
....
end
end

But that’s a simple example where the client initiates an object and passes the repository instance to it. What happens in a different context and environment? The real problem comes when the same instance is used in different context by different clients. In order to avoid the dependency’s reference you need a way to reference it at runtime without referencing it’s actual class name. For example the following is a code snippet that I came across a couple of months ago:

class SomeService  def self.client
if Rails.env.test?
MockTestClient.new
else
RealClient.new
end
end
end

What we want to achieve in the end is to reference our dependency without having to change or type it in a different context or place. What most frameworks do is that they provide a configuration where a centralised place exists for all our objects. How they should be initialised it’s up to developers responsibility to provide it. Dependency injection is a strong technique used to increase efficiency and modularity. Each technology has each own dependency injection framework but all of them serve the same purpose: increase efficiency and modularity.

There are a couple of ways to implement it in Ruby. project has dry-container intended to be a middleware for dependency injection implementations. For more details please have a look on the related documentation. Other approaches include registries (which we will follow in this article) or different configurations per environment.

Being a big fan of explicit vs implicit we will start by declaring each dependency per environment. It’s a simple and easy to understand paradigm where you don’t have to keep your mind questioning why and how this works or putting effort on learning any kind of library or framework. It’s easy to implement and you avoid adding another dependency to your project which I also think that it is a big benefit itself.

Repository definition via a Registry

One simple method that one can follow is to keep a registry of all the repositories of the application and register each one of them depending on the context that we are into. In our example below we define two set of repositories. One collection includes all repository definitions that will be used in our test environment and another collection includes all our repositories that will be used in our non test environment. Test repositories will have an in memory persistence layer implementation which will provide us with fast running test. Persistence layer can be set to the actual database implementation in our acceptance or functional tests.

Let’s see an example in Rails where we define and register our repository implementations.

# file environment.rbNON_TEST_REPOS = {
products: MoviesRepository.new(gateway: Product),
recos: RecosRepository.new(gateway: RecosService.instance)
# ....
}
TEST_REPOS = {
products: InMemoryTestRepository.new(gateway: InMemoryPersistence.new),
recos: InMemoryTestRepository.new(gateway: RecosMockService.new)
# ...
}
if Rails.env.test?
RepositoryRegistry.register_many(TEST_REPOS)
else
RepositoryRegistry.register_many(NON_TEST_REPOS)
end

Ignoring the implementation details about the registry for now we register our dependencies per environment. At the above example test environment will have our in memory persistence repositories so it will be database agnostic and all non test environments will use the implementation with the database persistence.

Below we describe a possible implementation for the RepositoryRegistry:

# file repository_registry.rbclass = Class.new(StandardError)

def self.register(, )
repositories[] = end
def self.register_many(collection = {})
.each { |, |
register(, )
}
end
def self.for()
repositories.fetch() do
raise
(RepositoryNotFound, "Repository #{} not registered")
end
end

def self
.repositories
@repositories ||= {}
end

private_class_method
:repositories
private def initialize(*)
raise "Should not be initialiazed"
end
end

Pretty simple. A registry to hold all our registered repositories that can be accessed by calling the for class method. Before seeing the implementation for each concrete repository let’s see an example of a service that will use the registry to store some products.

class ProductsServiceHandler
attr_reader :
products_repo
def initialize(products_repo = RepositoryRegistry.for(:products))
@products_repo = products_repo
end
def add_product(params)
products_repo.add(name: params.name,
description: params.description,
price: params.price)
end
def rate_product(params)
product = products_repo.find(params.product_id)
product.rate(params.user_id, params.rating)
repository.save(product)
end
# def add_to_inventory ...
end

In the above example we see that ProductsServiceHanlder does not hold any reference to ActiveRecord Product model neither has any knowledge on how the product will be persisted. It only holds a reference to the ProductsRepository that will be responsible to add and save a product. The above can be extended with helper module to inject our repositories in a different way. For example:

class MyService
include RepoContainer.new(:products)
def call(params)
products_repo.add(params)
end
end

Since I would like to keep things simple I will avoid the definition of the container.

Below we describe a possible solution on how to implement each concrete repository and how to isolate ActiveRecord persistence layer behind each repository. First we will introduce ApplicationRepository class that will be the parent for each concrete repository implementation. The parent class will define some CRUD methods that can be shared across all repositories like add, save, delete etc. This is not necessarily but it will DRY things up and will be useful as we will see later on.

class ApplicationRepository  def initialize(:)
@gateway = end
def add!(= {})
gateway.create!()
end
def find()
gateway.find()
end
# ... private attr_reader :gateway
end

It’s pretty simple since it delegates most of the work to the gateway. The gateway would be the database context and in our case recall the we created each repository by defining an active record class as the gateway. This is pretty powerful since now in each concrete repository implementation we can take advantage of ActiveRecord API to build super thin repositories:

# recall in environment.rb. The gateway injected in the creation of the repository is an ActiveRecord Product class. MoviesRepository.new(gateway: Product)
class ProductsRepository < ApplicationRepository
end

There may be occasions where a subclass may not have any extra methods defined if the methods in the parent class are sufficient. In the example above the ProductsRepository does not define any new methods since it inherits them from its parent class. In that case we can avoid creating a new empty class but instead keeping the application repository which acts as a generic delegator to our storage. For example

RepositoryRegistry.register(
products: ApplicationRepository.new(gateway: Product)
)

NOTE: The above implementation is not complete since we did not define the mapping between our active record models and our domain entities. The next article will continue from where we left and we will introduce our domain entities which will be responsible to model and contain our domain business logic. Each active record instance will be decoupled from our domain layer and will only serve as data access objects which will provide the interface to our persistence mechanism.

Summary

So far we have decoupled active record and persistence knowledge from our application business use case. Persistence layer acts as implementation details in our application. Using the repository pattern it is easier to maintain centralized data access logic and we achieved our initial intention, to create an abstraction layer between the data access layer and the business logic layer. Last but not least we saw that dependency injection gives our code a higher loose coupling and by this more flexibility for changes as well as more testable code.

References

[1] https://martinfowler.com/eaaCatalog/repository.html

[2] https://deviq.com/repository-pattern/

[3] https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store