29
.
04
.
2024
12
.
12
.
2022
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Repository - DDD in Ruby on Rails

Paweł Strzałkowski
Chief Technology Officer

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

Articles in this series

Do you want to know more? Register for our DDD webinar

Paweł Strzałkowski
Chief Technology Officer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

Vector Search in Ruby - Paweł Strzałkowski

Vector Search in Ruby

17
.
03
.
2024
Paweł Strzałkowski
ChatGPT
Embeddings
Postgresql
Ruby
Ruby on Rails
LLM Embeddings in Ruby - Paweł Strzałkowski

LLM Embeddings in Ruby

17
.
03
.
2024
Paweł Strzałkowski
Ruby
LLM
Embeddings
ChatGPT
Ollama
Handling Errors in Concurrent Ruby, Michał Łęcicki

Handling Errors in Concurrent Ruby

14
.
11
.
2023
Michał Łęcicki
Ruby
Ruby on Rails
Tutorial
Recap of Friendly.rb 2024 conference

Insights and Inspiration from Friendly.rb: A Ruby Conference Recap

02
.
10
.
2024
Kaja Witek
Conferences
Ruby on Rails

Covering indexes - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Ruby on Rails
Postgresql
Backend
Ula Sołogub - SQL Injection in Ruby on Rails

The Deadly Sins in RoR security - SQL Injection

14
.
11
.
2023
Urszula Sołogub
Backend
Ruby on Rails
Software
Michal - Highlights from Ruby Unconf 2024

Highlights from Ruby Unconf 2024

14
.
11
.
2023
Michał Łęcicki
Conferences
Visuality
Cezary Kłos - Optimizing Cloud Infrastructure by $40 000 Annually

Optimizing Cloud Infrastructure by $40 000 Annually

14
.
11
.
2023
Cezary Kłos
Backend
Ruby on Rails

Smooth Concurrent Updates with Hotwire Stimulus

14
.
11
.
2023
Michał Łęcicki
Hotwire
Ruby on Rails
Software
Tutorial

Freelancers vs Software house

02
.
10
.
2024
Michał Krochecki
Visuality
Business

Table partitioning in Rails, part 2 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
Ruby on Rails

N+1 in Ruby on Rails

14
.
11
.
2023
Katarzyna Melon-Markowska
Ruby on Rails
Ruby
Backend

Turbo Streams and current user

29
.
11
.
2023
Mateusz Bilski
Hotwire
Ruby on Rails
Backend
Frontend

Showing progress of background jobs with Turbo

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Ruby
Hotwire
Frontend
Backend

Table partitioning in Rails, part 1 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend
Ruby on Rails

Table partitioning types - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend

Indexing partitioned table - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
SQL Views in Ruby on Rails

SQL views in Ruby on Rails

14
.
11
.
2023
Jan Grela
Backend
Ruby
Ruby on Rails
Postgresql
Design your bathroom in React

Design your bathroom in React

14
.
11
.
2023
Bartosz Bazański
Frontend
React
Lazy Attributes in Ruby - Krzysztof Wawer

Lazy attributes in Ruby

14
.
11
.
2023
Krzysztof Wawer
Ruby
Software

Exporting CSV files using COPY - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Ruby
Ruby on Rails
Michał Łęcicki - From Celluloid to Concurrent Ruby

From Celluloid to Concurrent Ruby: Practical Examples Of Multithreading Calls

14
.
11
.
2023
Michał Łęcicki
Backend
Ruby
Ruby on Rails
Software

Super Slide Me - Game Written in React

14
.
11
.
2023
Antoni Smoliński
Frontend
React