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

Is Go Language the Right Choice for Your Next Project?

14
.
11
.
2023
Maciej Zdunek
Backend
Business

SXSW Tradeshow 2020: Get Your FREE Tickets and Meet Us

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Conferences
Frontend
Backend
Business

How to build effective website: simplicity & McDonald's

14
.
11
.
2023
Lukasz Jackiewicz
Ruby on Rails
Frontend
Design

Thermal Printer Protocols for Image and Text

14
.
11
.
2023
Burak Aybar
Backend
Tutorial
Software

WebUSB - Print Image and Text in Thermal Printers

14
.
11
.
2023
Burak Aybar
Backend
Tutorial
Software

What happened in Visuality in 2019

14
.
11
.
2023
Maciej Zdunek
Visuality
HR

Three strategies that work in board games and in real life

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

HR Wave - No Bullshit HR Conference 2019

14
.
11
.
2023
Alicja Gruszczyk
HR
Conferences

Lightning Talks in your company

14
.
11
.
2023
Jarosław Kowalewski
Ruby on Rails
Visuality

Stress in Project Management

02
.
10
.
2024
Wiktor De Witte
HR
Project Management

How to find good developers and keep them happy - Part 1

02
.
10
.
2024
Michał Krochecki
HR
Visuality

PKP Intercity - Redesign and case study of polish national carrier

14
.
11
.
2023
Katarzyna Szewc
Design
Business
Frontend

Let’s prepare for GITEX Dubai together!

14
.
11
.
2023
Michał Piórkowski
Conferences
Business

Ruby Quirks

14
.
11
.
2023
Jan Matusz
Ruby on Rails
Ruby

Visuality recognized as one of the Best Ruby on Rails Devs

14
.
11
.
2023
Maciej Zdunek
Ruby on Rails
Visuality
Business

Is the culture of the organization important?

14
.
11
.
2023
Alicja Gruszczyk
Conferences
Visuality

Between the devil and the deep blue sea

04
.
12
.
2023
Mateusz Wodyk
Project Management
Backend
HR

Let’s prototype!

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

5 marketing hacks which will make your life easier

14
.
11
.
2023
Maciej Zdunek
Marketing
Design