Domain-Driven Design's core interest lays in the domain - the expression of purpose, behavior and goals. In a typical Ruby on Rails application, the domain elements are expressed using ActiveRecord objects. ActiveRecord ties the domain (logic and rules) with persistence (saving to and loading from a storage). In this article, I will show you how to free your domain from the ever-present ORM-driven development.
"A REPOSITORY lifts a huge burden from the client, which can now talk to a simple, intention-revealing interface, and ask for what it needs in terms of the model."
— Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software
Aggregate
In one of the previous articles in this series, I've written that "an aggregate is a group of objects (entities and associated value objects), which guards business rules". Such an aggregate is always in a valid state. It is created that way (which will be covered in a future article about Factories), modified only into another valid state and therefore persisted in such a state as well. No operation can make an aggregate "invalid", which is much different from the Ruby on Rails / ActiveRecord approach to entities.
Whenever an aggregate is loaded from a persistence store, it has to be recreated into a valid, consistent state. This means, that we should never arbitrarily load an entity from within an aggregate's boundary and modify (or even use) it. We need a mechanism, which persists and loads the entire aggregates.
Repository
"Every persistent Aggregate type will have a Repository."
— Vaughn Vernon, Implementing Domain-Driven Design
In Ruby on Rails, ActiveRecord objects contain their own persistence interface. They include complex API for saving, updating and fetching objects as well as collections of them. Such a solution binds the domain and the infrastructure (persistence) layers. Domain-driven design urges us to tackle the complexity at the domain layer so it's essential that it's well separated from the others.
Let's see how we can separate the layers using an additional object, which has the unique ability to persist or load an aggregate:
# A domain object represented as a PORO
class Car
attr_reader :registration_number, :color
def initialize(registration_number:, color:)
@registration_number = registration_number
@color = color
end
end
# Instantiate the domain object
car = Car.new(registration_number: 'DDD 2003', color: 'red')
# Use repository to persist the object
repository = CarRepository.new
repository.add(car)
# Then some other day....
# Use repository to load the object
repository = CarRepository.new
car = repository.of_registration_number('DDD 2003')
car.color
=> 'red'
A repository is an object which can
- save an aggregate
- load an aggregate
- load a collection of aggregates
From the perspective of the domain object, it is not important where or how the information is saved. It may be stored in a relational database, a document database, an event store or simply in memory. Your domain object is expressed as a Plain Old Ruby Object and it just doesn't care. You have the total freedom to express your business domain and not be bound by persistence restrictions.
How to create a repository in Ruby on Rails
We can reuse the Car
example and just make it a bit more realistic. Let's add an id
attribute, which cannot be modified within domain's logic.
class Car
attr_reader :registration_number, :color, :id
def initialize(registration_number:, color:)
@registration_number = registration_number
@color = color
end
def repaint_to(new_color)
@color = new_color
end
private
attr_writer :id
end
In order to save a Car
object into a database, we need to use some persistence mechanism. You can use anything, but the most handy thing would be the ActiveRecord itself! We can define a DbCar
class like this:
class DbCar < ApplicationRecord
def self.table_name
"cars"
end
end
Let's see how we can load or save a car.
How to load an object using a repository
The goal is to load a persisted object and use its data to initialize a domain object. Let us do it:
class CarRepository
def of_registration_number(registration_number)
db_car = DbCar.find_by(registration_number: registration_number)
return unless db_car
car = Car.new(
registration_number: db_car[:registration_number],
color: db_car[:color]
)
car.send('id=', db_car[:id])
car
end
end
What? send
??
Well, yes!
A repository knows everything about the aggregate it is supposed to load. It has a very close relationship with it. Repository can use methods forbidden for others. Anything, to fulfill its purpose.
Alternatively, you may prepare a factory method within your aggregate, but it’s up to you to decide whether (or to what extent) an aggregate should be aware of the fact that it’s being recreated by a repository.
The method’s code gets a bit more complex when there are associations to be loaded. But it's nothing that should stop us from using the pattern. Please remember that aggregates are not ActiveRecord-like monsters with tens of associations. They should be kept small and simple.
How to save an object using a repository
Saving is as straightforward as loading:
class CarRepository
def save(car)
db_car = car.id ? DbCar.find(car.id) : DbCar.new
db_car.assign_attributes(
registration_number: car.registration_number,
color: car.color
)
db_car.save!
rescue ActiveRecordSpecificErrors => e
# adjust behavior to your use case
end
end
You might be surprised that saving requires an additional DB read at the beginning. It does, but I feel it's a very low price for such a major architectural advantage. On the other hand, using ActiveRecord directly, often comes with a lot of coupled logic and unnecessary eager loading. It can have far greater impact on your database than a single read based on an indexed identifier.
Repositories can also be used to fetch collections
Provided that you don't use ActiveRecord's scopes for complex queries, there is a common strategy in the Ruby on Rails community for fetching collections. People have been using so-called Query Objects (sometimes written as Service Objects for the query purposes). When you switch to repositories, you get a fine place to put collection queries. Add them next to the previously discussed load/persist methods. Then, you are able to fetch collections of aggregates, expressed as domain objects.
Take a look at a Github repo with the discussed repository examples
Collection- vs Persistence-Oriented Repository
The kind of approach to repositories discussed so far is called "persistence-oriented". It requires performing an explicit persisting operation - like calling a save
method. In contrast, when using a "collection-oriented" repository, a loaded object is immediately persisted when it's modified.
Such a behavior is similar to loading objects from simple data structures like a hash or an array. Take a look at the following example:
> Car = Struct.new(:max_speed, :color, keyword_init: true)
> cars = [
Car.new(max_speed: 10, color: :red),
Car.new(max_speed: 20, color: :green)
]
=> [#<struct Car max_speed=10, color=:red>, #<struct Car max_speed=20, color=:green>]
> red = cars.first
=> #<struct Car max_speed=10, color=:red>
> red.max_speed = 100
=> 100
> cars
=> [#<struct Car max_speed=100, color=:red>, #<struct Car max_speed=20, color=:green>]
In this example, the car fetched from cars
collection is updated in the collection's storage as soon as it's modified. The same behavior (conceptually) can be achieved for relational database repositories. It has its pros and cons, which go far beyond the scope of this article. Especially, that I have never seen such a repository used in a Ruby on Rails project. They are much more common in the world of Java and Hibernate - please reach out there for further information.
What about CRUD models?
"Strictly speaking, only Aggregates have Repositories. If you are not using Aggregates in a given Bounded Context, the Repository pattern may be less useful."
— Vaughn Vernon, Implementing Domain-Driven Design*
When all you use are Entities (with no business rules guarded between them), then you can just as well use a simpler persistence approach. In such a case (in the Ruby on Rails realm) you may just use ActiveRecord and not create implementation overhead. However, you may still use repositories in another Bounded Context (I will show you how to split a RoR app into contexts in the next articles).
Evans, in the Blue Book has written “do not fight your framework” and it fully applies to this article. ActiveRecord is great at saving simple entities. When that’s your goal, repositories might be a heavy overkill. Especially when you are not fully familiar with the pattern.
Should repositories handle database transactions?
Database transactions should not be handled within repositories. They should be left to any client using them. More often than not, it would be the application layer of a bounded context. That layer would use a repository to fetch an aggregate, invoke aggregate’s behavior and then use the repository to persist it. Usage of transactions should be aligned with and optimized for a specific application use case.
Summary
When focusing on modeling of your domain, you cannot be bothered by adjusting the model to the obstacles brought by the infrastructure. It doesn't matter what your persistence storage is. Tackle the complexity of your application at its heart and leave infrastructural matters to the repositories. It may be hard at first to detach yourself from the cosy, well-known ActiveRecord. But I promise you - it is worth it! Let's use ORM-driven approach when there is no deep logic. But when it comes to Domain-Driven Design, a sophisticated tool like a repository may be just what you need.
Resources
- Vaughn Vernon, Implementing Domain-Driven Design - a must read for this topic
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software
- Martin Fowler, Repository
- Repositories in Hanami
- Repository code examples
Articles in this series
- Introduction to DDD in Ruby on Rails
- Value Object
- Entity
- How to design an entity
- Aggregate
- Repository
- Bounded Context
- Example Application
Do you want to know more? Register for our DDD webinar