The Registry Pattern

Using a Registry to look up collaborators in Rails, like external APIs.

Avdi Grimm published a great RubyTapas episode last year on the Registry pattern. To summarize: the Registry pattern (sometimes called a Service Locator) is a way for classes to look up their collaborators, rather than require they instantiate their collaborators explicitly. This simple bit of indirection can go a long way to making code easier to write and test.

Avdi gave a simple Ruby example, but it made me think: how might I do something similar in a Rails app?

An example

Let’s say we’re building a Rails app that needs to collaborate with a REST API that is provided by the IT department at our organization. Our IT department is a bit paranoid, and wants to be notified when users perform password resets. No problem. We’ve already wrapped their API in a simple Ruby class that works like this:

auditor = SecurityAuditor.new(:api_key => ENV["SECURITY_LOGGER_API_KEY"])
auditor.log_password_reset(username, ip_address)

Imagine that this SecurityAuditor also exposes other methods that need to be hooked in to various parts of our controllers or models. How do we make this object available throughout our app?

Global variable. A “brute force” approach would be to create the auditor in config/initializers and make it available as a global variable. Indeed, I’ve seen this done in Rails occasionally with things like $redis or $memcache for global access to those external services. This definitely works, but globals are usually frowned upon. Let’s explore some other options.

Inline. Instead, I could instantiate the SecurityAuditor within each model or controller whenever I need it. This works too, but now I’m repeating code everywhere. If the SecurityAuditor performs its own caching or allocates resources like persistent HTTP connections, this could be really inefficient as well. Not good.

Application controller. A third option that I’ve seen in Rails apps is to create and memoize the SecurityAuditor in the ApplicationController. That means the auditor is getting created at most once per request, which might be acceptable. Controllers now have easy access to the auditor, but for models, I would have to pass the auditor as an argument to every model call that needs it. Messy. And if my models are being invoked indirectly (e.g. via Devise), that might be difficult.

The registry approach

Here’s the approach that I’ve come to use in my recent projects, inspired by Avdi’s Registry episode.

First, I create a Registry module (or MyAppNameRegistry) that provides module functions for accessing service objects, like the SecurityAuditor in my example. The delegate declaration is a bit of syntactic sugar that I’ll explain later.

module Registry
  delegate :security_auditor, :to => self

  def self.security_auditor
    @security_auditor ||= SecurityAuditor.new(
      :api_key => ENV.fetch("SECURITY_AUDITOR_API_KEY")
      )
  end
end

Then, I use it like this:

Registry.security_auditor.log_password_reset(username, ip_address)

The delegate macro allows me to use the Registry as a mixin, which makes accessing the auditor more concise. This can be used to avoid repetition when the auditor is used multiple times within a single class.

class User < ActiveRecord::Base
  include Registry

  def reset_password
    # Delegates to Registry.security_auditor
    security_auditor.log_password_reset(...)
  end
end

The Registry itself is essentially a global singleton, so it shares the benefit of simplicity with the global variable approach. But it gives us more safety (reassigning the Registry constant causes a warning), and more flexibility: the security_auditor method can do whatever it wants to obtain an instance of the SecurityAuditor. Here are some possibilities:

Where the registry pattern falls down

The Registry pattern is not always needed, and in some cases it can be the wrong solution. If you are writing code that will eventually be distributed outside of your app, you can’t assume that the global Registry will always be present. In this case, adding attribute writers or initializer arguments to pass in collaborators (i.e. dependency injection) is a more verbose but foolproof approach.