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:

  • 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.
Repository pattern diagram

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.

class SomeServiceHandler
def initialize(repository)
@repository = repository
end
def call
....
end
end
class SomeService  def self.client
if Rails.env.test?
MockTestClient.new
else
RealClient.new
end
end
end

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.

# 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
# file repository_registry.rbclass RepositoryRegistry
RepositoryNotFound = Class.new(StandardError)

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

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

private_class_method
:repositories
private def initialize(*)
raise "Should not be initialiazed"
end
end
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
class MyService
include RepoContainer.new(:products)
def call(params)
products_repo.add(params)
end
end
class ApplicationRepository  def initialize(gateway:)
@gateway = gateway
end
def add!(params = {})
gateway.create!(params)
end
def find(id)
gateway.find(id)
end
# ... private attr_reader :gateway
end
# 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
RepositoryRegistry.register(
products: ApplicationRepository.new(gateway: Product)
)

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

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