How to Use Ruby on Rails Repositories and Active Record Model

Mateusz Karbowiak03/25/2024

repository pattern

Remember the story of tech leading a project in fours acts? It was a motivational (hopefully!) story about delivering a project and my experience with being a tech lead.

It was focused around repositories helping us move all ActiveRecord calls into single files (per model or context).

Although I provided some insights on what we did code-wise, I feel like it lacked a FULL technical explanation how it helped exactly. And it bore hard on me. After all, I’m a developer and I couldn’t let that knowledge-sharing opportunity slip away. Besides, looking back, I’ve noticed a couple of shortcomings in the presented solution. So, let me update the story with some tech meat and guide you through the process of creating a repository pattern using ActiveRecord.

Intro to Ruby on Rails Repositories


Don’t worry just yet, the solution I provided you with wasn’t bad, it simply wasn’t perfect—but in the end, what is?

Cta image

For the sake of clarity, we wanted to achieve database calls separate from business logic. A nice, descriptive, and easily testable interface for querying and persistent data. Thin models and controllers. Single responsibilities. Separation of concerns. All the good stuff. But we were short of time and I was new to the topic—I have just learned about the repositories

Let’s start by laying out what repositories are. I could paste several definitions here, but as I’m a visual-oriented person, like most humans, here’s an awesome graph that illustrates the idea:

Graph showing the Repository Pattern with Active Record

Source: https://msdn.microsoft.com/en-us/library/ff649690.aspx

Moving on. Remember those?


class Contact < ActiveRecord::Base
  after_destroy :do_something
  
  belongs_to :user
  belongs_to :category
  has_many :emails, dependent: :destroy
  
  validates :name, presence: true
  validate :some_custom_validator
  
  scope(:eligible_for_email, lambda do |user_id| 
    joins(:category)
    .select("contacts.*, category.title AS category_title")
    .where(emailed: false, unsubscribed: false, user_id: user_id)
  end)
  
  def status
    emails.any? ? "Emailed" : "Never emailed"
  end
  
  private
  
  def do_something
    # Harmless callback
  end
  
  def some_custom_validator
    # Can I even save?
  end 
end

Are you able to count the number of things this class is responsible for? Me neither. And that’s without mentioning all the ActiveRecord methods it inherits. Plus, the example is thinned, mind you. I saw and wrote Models 3-4 times as long and bloated. When business logic gets more and more complicated, things get increasingly unreadable and unmaintainable. Even though ActiveRecord has its place, we wanted something more flexible, with more explicit control over it.

But since the application was already written using AR, we didn’t want to go all in but rather take iterative steps—find all database calls, extract them, test them, remove AR.

RoR Repository Model with ActiveRecord

Is that even possible? Well, that’s the thing. This is not a perfect solution but: it works, gets the job done, meets our needs, leaves doors open for improvement. This is definitely not a definition of done—we meant to push the re-work further but life happened.

We took EVERY call to the database from the app and moved it to the repository. Here’s the example repository file:


class ContactRepository
  class << self
    def find_by(*attrs)
      Contact.find_by(*attrs)
    end
    
    def destroy(contact)
      contact.destroy
    end
    
    def eligible_for_email(user_id)
      Contact
      .joins(:category)
      .select("contacts.*, category.title AS category_title")
      .where(emailed: false, unsubscribed: false, user_id: user_id)
    end
  end
end

Here’s how we use this in the business logic code:



class SendEmail
  def initialize(current_user_id, repo = ContactRepository)
    @contacts_to_email = repo.eligible_for_email(current_user_id)
  end
  
  def call
    # Do something with @contacts_to_email
  end
end

module Api
  module V1
    class ContactsController < ApiController
      def destroy
        contact = ContactRepository.find_by(id: params[:id])
        ContactRepository.destroy(contact)
        head 204
      end
    end
  end
end

You get the idea. We created a repo class for every model that required any database (ActiveRecord) call (querying or persisting).

The Good Part

  • Database calls separate from business logic? Check. 

Database-specific code is only in the repos themselves and we use them EVERYWHERE.

  • Nice, descriptive, and easily testable interface? Check.

You look at the repo and you instantly know what it does. You read the methods, you know what you’ll get. Additionally, we can mock up repositories in all of the unit tests. Things got fast! No more unit-sorta-integration tests.

  • Thin models and controllers? Single responsibilities? Separation of concerns? Check, check, and check.

Models are no longer responsible for communication with the database. Controllers listen to and respond to clients. Repositories query and persist data. Everything has its place. We achieved what we wanted to.

The Not-So-Good Part

The weak part that popped up during the in-house review of the previous story was that this is not a “by the book” repository pattern. Only the repositories should be responsible and have the ability to communicate with the database. Here, it’s still an AR Model thing; our repos are just an another abstraction layer on top of that.

So it might be tempting to just go “Ah, well, what’s the difference” and use Model methods instead. So, going with ActiveRecord is actually using the wrong tool for the job which necessitates putting trust in the developers’ discipline to use our repos.

The Best Part

However, it can always get better. The first steps for improvement that come to mind include:

1. Divide repos into queries and commands.

They can grow, but separation like this makes them even more descriptive and clean. It’s not always needed of course, everything should be suited to your needs. 

2. Build your own entities.

Relying on AR models is risky, as I already mentioned above. In “by the book repository patterns,” repos should be the only place where database calls are performed. This is where entities can come in handy—they just describe how the object should look, no additional magicky things inside. Take a look at dry-rb gems or… simple struct.

3. Get rid of ActiveRecord completely.

For someone who got into Web development with all of Rails’ nice conventions, this is heavy. But the more I code, the more control I want. It’s like riding a bicycle with training wheels—easy to learn, accessible entry threshold, convenient, but if you want to start doing fancy tricks, well…

I decided to shift away from ActiveRecord (both the tool and the pattern) and use something more flexible due to working on more complicated projects with more business logic. Maybe Sequel instead? Or even the whole ROM? Still, I’m not saying ActiveRecord is bad or anything—just pick the right tools for the job!

We’ve come to an end of the repository pattern story. I’ll try to make my repos and my app a little better following these steps and come back to you with the results. As this is just one of the phases, I’m far from done yet. The solution outlined above would work, but I still wouldn’t risk leaving it like that in a project—too many loose ends. So stay tuned for the next part!

Cta image
Mateusz Karbowiak avatar
Mateusz Karbowiak