Asynchronous method calls using Rails’s ActiveJob

16 février 2026

ActiveJob is great – but every time you need to perform some kind of delayed execution, you need to create a new job class for it. Something like:

class DestroyAccountJob < ActiveJob
  def perform(account)
    account.destroy!
  end
end

# In the code, this will be used like:
DestroyAccountJob.perform_later(account)

And ideally, the job #perform method should fit on a single line: no complex code in the job itself, to make it easier to test.

But many other programming languages allow performing any method call in the background, without needing to declare a specific class.

Could we cut the boilerplate also in Ruby? Turns out we can.

Introducing AsyncExecutor

This is a small toy snippet, to call any method in a background job. No need to define custom job classes!

The simplest usage looks like this:

include AsyncExecutor

# Call `self.destroy!` in a background job
perform_later(:destroy!)

It will create and enqueue a AsyncExecutorJob, that will perform the given method.

And we also get more advanced usages, using a Proxy object:

balance = AccountBalance.create!(account: account)
start_date = 3.months.ago
end_date = Time.now

# 1. With a method name
balance.perform_later(:compute_balance, start_date:, end_date:)

# 2. With a proxy object
balance.perform_later.compute_balance(start_date:, end_date:)

# 3. With a block
balance.perform_later do
  compute_balance(start_date:, end_date:)
end

The code

Only two files:

# app/lib/active_job/async_executor.rb

# Perform any action later (by enqueuing an ad-hoc job), without needing to define a new ActiveJob subclass.
module ActiveJob::AsyncExecutor
  # Internal class. Converts each method called on `target` into an asynchronous job.
  class Proxy
    # @param target [StandardObject]
    def initialize(target)
      @target = target
    end

    def method_missing(method, *args, &block)
      AsyncExecutorJob.perform_later(@target, method, *args)
    end
  end

  # Call the given method later, in an ad-hoc job.
  #
  # @overload perform_later(method)
  #   Invoke the given method on `self` later.
  #   @param method [String, Symbol, nil] the name of the method to call
  #   @return [Proxy]
  #   @example
  #     perform_later(:compute_balance, start_date:, end_date:)
  #
  # @overload perform_later()
  #   Return a proxy to `self`, that will execute methods called on it later.
  #   @return [Proxy]
  #   @example
  #     balance.perform_later.compute_balance(start_date:, end_date:)
  #
  # @overload perform_later{}
  #   Evaluate the block in the receiver's context, and execute methods called on it later.
  #   @yield [] a block evaluated in the receiver's context (optional)
  #   @return [Proxy]
  #   @example
  #     balance.perform_later { compute_balance(start_date:, end_date:) }
  def perform_later(method = nil, *args, &block)
    proxy = Proxy.new(self)
    if block_given?
      proxy.instance_eval(&block)
    elsif method
      proxy.public_send(method, *args)
    else
      proxy
    end
  end
end
# app/jobs/async_executor_job.rb

# See ActiveJob::AsyncExecutor
class AsyncExecutorJob < ApplicationJob
  def perform(target, method, *args)
    target.public_send(method, *args)
  end
end

How does it work?

The AsyncExecutor::Proxy object convert method calls into a job invocation.

The receiver and its arguments are serialized using ActiveJob usual mechanisms – which means for instance that ActiveRecord objects get serialized into a neat and compact globalID.

Should I use this in production?

A nice property of class-based jobs is that is makes queuing and debugging easier: you can enqueue some jobs in specific queues, and get a neat dashboard with a description of all your jobs.

Using AsyncExecutor#perform_later means that all invocations will be mixed in the same queue, and harder to debug.

But for small systems, I’m curious of how it can reduce the boilerplate. It is small, monkey-patch-free, and works with any ActiveJob backend. Fell free to experiment with it.

Prior art

Kudos

Discussion, liens, et tweets

J’écris des sites web, des logiciels, des applications mobiles. Vous me trouverez essentiellement sur ce blog, mais aussi sur Mastodon, parmi les Codeurs en Liberté, ou en haut d’une colline du nord-est de Paris.