How Can You Do it? Well, It Prepends...

Jonathan Greenberg
By Jonathan Greenberg
September 04, 2024

I have known about Ruby’s Module#prepend method since it was introduced in Ruby 2.0 over a decade ago but never quite found a need for it. Module#include is prepend’s older and more famous sibling similarly useful for decoration and composition. I suspect that other developers, like me, naturally just lean on include when abstracting shared methods to a module.

In fact, it is no surprise that I rarely find myself turning to this powerful feature since it goes largely neglected even in ActiveSupport::Concern. Although you can clearly use a prepended block when using this most popular wrapper for including module methods in a class, it isn’t even mentioned in the introductory documentation examples. I suppose the functionality is so similar to include, why bother confusing yourself and other developers that have enough trouble figuring out when and how to include vs extend a Module

However, it really isn’t too difficult to understand the critical difference between include and prepend so long as you understand how Ruby integrates methods into its lookup chain. Whenever you call a method on an object it not only will look for instance methods in the class itself but will also check for any methods that may be defined in classes from its inheritance chain but also first checks for any modules that have been included (or prepended!). As explained by ChatGPT the main difference between include and prepend is that a prepended module will actually get inserted before the class that it is prepended to. It is a bit of a mind-bender to think about giving some other method in a module precedence over the class itself but Ruby likes to be flexible and give you every possible trick for your trade just in case.

I recently finally found myself in a situation where I wanted just such a trick. I was working on a project that was upgrading its Rails version to 6.0 and wanted to make use of ActiveRecord’s Multiple Databases support which is a great feature worthy of its own blog. The project had been using the active-record-slave gem but now wanted to use the built-in support for primary/replica databases provided by Rails.

The project ideally wanted many of their ActiveJob workers to point to the replica by default, especially when they handled read intensive jobs. Though ActiveRecord does not provide an automatic way to switch between reading and writing out of the box, it does at least throw an error when a write is called against a replica. So, it seemed like best practice to point to the replica by default and switch to writing for creates, updates and destroys.

This app was using Sidekiq as their background queue so I considered using a Middleware to add this default replica read functionality. However, in the end, there were a few jobs that just involved writing to the primary and it seemed messy to have to switch back to the default primary database in such cases. I wanted something more flexible to wrap perform methods in a read block and that is when prepend came to mind. This is the solution I came up with in app/jobs/concerns/replica_reading.rb:

# frozen_string_literal: true

module ReplicaReading
  def perform(*args)
    read_from_replica { super }
  end
end

In ApplicationJob there is a call to

delegate :read_from_replica, :write_to_primary, to: ApplicationRecord

And in ApplicationRecord:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  
  # ...
  
  def self.read_from_replica(&block)
    return block.call if ActiveRecord::Base.connected_to?(role: :reading)

    Rails.logger.info('Switching to reading role')
    result = ActiveRecord::Base.connected_to(role: :reading, &block)
    Rails.logger.info('Returning to writing role')
    result
  end

  def self.write_to_primary(&block)
    return block.call if ActiveRecord::Base.connected_to?(role: :writing)

    Rails.logger.info('Switching to writing role')
    result = ActiveRecord::Base.connected_to(role: :writing, &block)
    Rails.logger.info('Returning to reading role')
    result
  end

  connects_to database: { writing: :primary, reading: :primary_replica }
end

Now any job simply needs to prepend ReplicaReading and when ActiveJob calls perform on a job it will first call our read_from_replica wrapping method and the call to super will actually call the perform method defined in the job itself. Note how it is important to slurp in all *args in the method signature so that they are passed along to the call to super. Like I said, a bit mind bending, but effective.

Of course, it does mean that reading from the replica database is not quite so automatic because you need to remember the explicit prepend call but in the end it was probably best to not hide that concern entirely. We still get the benefit on not having the actual perform method in the job cluttered with an additional wrapping block while an incoming developer is hopefully reminded that the job will read from replica by default.

I suspect that there are many creative and helpful ways to make use of Ruby’s prepend. Seeing a practical use of it helps make how it works more tangible and hopefully will inspire other clever ways to solve problems in your applications.

If you’re looking for a team to help you discover the right thing to build and help you build it, get in touch.