The letter A styled as Alchemists logo. lchemists
Published May 1, 2025 Updated April 1, 2026
Cover
Options Pattern

The Options Pattern provides a solution for making your applications configurable. The name of this pattern stems from .NET but we’ll learn how to implement this in Ruby.

Quick Start

To make any object configurable, you only need Structs to define the attributes you need and a class to inject the configuration into. Example:

Configuration = Struct.new :token, :uri

class Client
  def initialize(settings = Configuration.new)
    @settings = settings

    yield settings if block_given?
  end
end

Client.new do |settings|
  settings.uri = "https://api.demo.io"
  settings.token = "secret"
end

The above will produce an instance of a Client as follows:

#<Client:0x00000001287f53e0
#  @settings=#<struct Configuration token="secret",
#                                   uri="https://api.demo.io">
#>

The beauty of this pattern is that you use a Struct, as a whole value object, to define all attributes of your configuration and then inject that configuration — using Dependency Injection (DI) — into an object that needs the configuration. Yielding (i.e. yield settings) if a block is given is what makes your object configurable because, unlike Data objects, you can mutate a Struct.

These are the basic principles, now let’s learn how to expand upon this pattern further.

Advanced

With the basics in mind, we can improve upon this pattern by looking at additional objects and complimentary patterns for which you can architect more robust solutions. We’ll start by taking another look at the Configuration object.

Configuration

In the Quick Start example above, we used a Struct to define the attributes of our configuration. You could also enhance this object with defaults:

Configuration = Struct.new :token, :uri do
  def initialize(**attributes)
    super
    self[:uri] ||= "https://api.demo.io"
  end
end

The problem with the above is there isn’t a great way to provide a default value for the token attribute which ends up being nil because the value should be an API token unique to each instance. One way to address this is to use environment variables for greater customization and flexibility:

Configuration = Struct.new :token, :uri, :environment do
  def initialize(environment: ENV, **attributes)
    super

    self[:token] ||= environment["API_TOKEN"]
    self[:uri] ||= environment.fetch("API_URI", "https://api.demo.io")
  end
end

With the above, we can see that if no token or uri value is supplied, we’ll fall back to what is supplied by the environment. In the case of token this’ll still be nil if nothing exists but with the uri, we’ll fall back to "https://api.demo.io". Definitely a step in the right direction but there are problems with this approach:

  • The constructor now has a lot more setup behavior which isn’t necessarily a bad thing but definitely gets worse if more attributes are added.

  • Dependency Injection (DI) is used for ENV which allows for customization but, in this case, is too clever because there is a Connascence of Position problem should the environment attribute change position which could cause it not be set in time to provide fall backs for the token and uri attributes. This can lead to surprising behavior and/or subtle bugs which are worth avoiding.

There’s a better way to handle this which is to use a dedicated loader as explained below.

Loader

Having a dedicated loader allows you to split the behavior of loading your configuration from construction of your configuration and opens up the possibility of having different loaders for the same configuration. Example:

Configuration = Struct.new :token, :uri

class Loader
  def initialize model: Configuration, environment: ENV
    @model = model
    @environment = environment
  end

  def call
    model[
      token: environment["API_TOKEN"],
      uri: environment.fetch("API_URI", "https://api.demo.io")
    ]
  end

  private

  attr_reader :model, :environment
end

Notice how the loader is injected, upon initialization, with the model and the environment giving you full flexibility to inject different objects with the same behavior if desired. This is the power of Dependency Injection (DI). Then, all you have to do is initialize and call the loader in order to build a configuration with safe defaults.

You can take this a step further by adding data transformations, validations, and more to the entire process. This is exactly what Etcher was created for so check out the gem documentation to learn more on how nice this can be for loading of advanced configurations.

Dependency Injection

With your configuration and loader in hand, you can now use a dependency container, like Containable, and automatic injection, like Infusible, to load and memoize your configuration for use across multiple objects if desired. Example:

module Container
  extend Containable

  register(:settings) { Loader.new.call }
end

Dependencies = Infusible[Container]

The above loads your settings with safe defaults once and only once but you can always force this to happen each time by using register(:settings, as: :fresh) { Loader.new.call }. See the Containable documentation for further details, though.

Example

By using everything discussed thus far, we can apply this pattern in full force by building a simple, but fault tolerant, GitHub API client. The implementation below is a Bundler Inline script so you can immediately experiment with the implementation:

A GitHub Client Implementation
#! /usr/bin/env ruby
# frozen_string_literal: true

# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "containable"
  gem "debug"
  gem "dry-monads"
  gem "http"
  gem "infusible"
end

Configuration = Struct.new :token, :uri do
  def initialize(**attributes)
    super

    self[:token] ||= "secret"
    self[:uri] ||= "https://demo.api.io"
  end
end

class Loader
  def initialize model: Configuration, environment: ENV
    @model = model
    @environment = environment
  end

  def call
    model[
      token: environment["API_TOKEN"],
      uri: environment.fetch("API_URI", "https://api.demo.io")
    ]
  end

  private

  attr_reader :model, :environment
end

module Container
  extend Containable

  register(:settings) { Loader.new.call }
  register :http, HTTP
end

Dependencies = Infusible[Container]

class Client
  include Dry::Monads[:result]
  include Dependencies[:settings, :http]

  def initialize(**)
    super(**)
    yield settings if block_given?
  end

  def get(path, **params) = call(__method__, path, params:)

  def post(path, **json) = call(__method__, path, json:)

  private

  attr_reader :settings, :http

  def call verb, path, **options
    http.auth("Bearer #{settings.token}")
        .public_send(verb, "#{settings.uri}/#{path}", options)
        .then { |response| response.status.success? ? Success(response) : Failure(response) }
  end
end

client = Client.new do |settings|
  settings.uri = "https://api.github.com"
  settings.token = "<your API token>"
end

include Dry::Monads[:result]

case client.get "users"
  in Success(response) then puts response.parse
  in Failure(message) then puts message
  else puts "Unknown type."
end

You’ll notice that the above implementation uses all objects discussed:

  • Configuration

  • Loader

  • Container

  • Dependencies

  • Client

At a minimum, you only need the Configuration and Client (or whatever object you wish to inject the Configuration into) but the other objects show how you can make all of this reusable.

Advantages

The advantages of this pattern are:

  • Uses whole value objects (i.e. Structs) to define all attributes of your configuration which is great for equality and quick access to reading and writing individual attributes.

  • Uses the Single Responsibility Principle (SRP) which is the S in SOLID design to keep housing configuration data separate from the actual loading and customization of the data.

  • Uses Dependency Injection (DI) which is the D in SOLID design that makes testing a breeze by swapping out different configurations as long as they have the same basic behavior.

  • Attributes can be modified after any defaults are loaded if desired.

  • Provides a flexible way in which to configure individual or multiple objects that might need to share the same configuration.

Disadvantages

There are few disadvantages to using this pattern. You could argue that mutation is involved because we can mutate the Struct via a block upon initialization but, remember, mutation works to our advantage in this case because the configuration instance is isolated to the object it’s injected into.

Guidelines

Avoid using classes to implement your configuration. Example:

class Configuration
  attr_accessor :token, :uri
end

The above is an antipattern because the class has no state, behavior, and/or functionality other than housing attributes. That’s not what classes are meant for and is why Structs exist to encapsulate raw data like this.

Another mistake people make is storing configuration logic at the class level instead of instance level. Example:

class Client
  def self.configuration = @configuration ||= Configuration.new

  def self.configure = yield configuration
end

There are multiple issues with the above:

  • This isn’t what a class is for. A Module or a container (i.e. Containable) would be better.

  • The configuration is now global and shared across all objects.

  • Introduces a Connascence of Name issue due Configuration being hard coded into .configuration class method which creates a tight coupling to the Configuration class. To modify any of this, you’d have to alter the source code of the class method itself.

  • Will error if no block is given to the .configure method.

All of this is avoidable by using object composition to inject your dependencies instead as denoted by use of the Containable and Infusible gems mentioned above.

Examples

Should you need more examples to see this pattern in practice, here are a few gems that leverage this pattern.

  • APIs

  • CLIs

    • Gemsmith: For building new gems and, if you use the --cli flag when building CLI specific gems, this pattern will be applied automatically for you.

    • Pennyworth: Enhances your Alfred workflows with Ruby firepower.

    • Milestoner: Allows you to automatically version and generate release notes for your softare releases.

Conclusion

This pattern is simple in implementation and use while providing a lot of flexibility in making your applications simple to configure with nice fallback support for environment variables if desired. Even better, you can use more advanced configuration loading techniques as provided by the Etcher gem.

If this pattern isn’t already part of your toolbox then hopefully the next time you need to make your objects configurable, you can apply this pattern to build a robust solution with grace. Enjoy!