RL ROLAND LOPEZ
// 3 min read

Design Patterns in Rails

Why design patterns in Rails

Rails leans on convention over configuration, so a lot of best practices come for free.

But hard problems still show up, and that is where design patterns earn their keep: a standard way to solve a recurring problem, so your code stays reusable and maintainable.

The trick is knowing when a pattern actually helps, and when it just adds ceremony.

Singleton

Singleton ensures a class has one instance with a global access point. In Rails it fits shared, read-mostly state, like a configuration object.

Don’t do this (class variables)

# app/models/global_config.rb
class GlobalConfig
  @@instance = new

  def self.instance
    @@instance
  end

  private_class_method :new
end

Class variables invite threading issues and leak state between tests.

Do this (Ruby’s Singleton)

# app/models/configuration.rb
require 'singleton'

class Configuration
  include Singleton

  attr_reader :api_key, :api_secret

  def initialize
    @api_key = ENV['API_KEY']
    @api_secret = ENV['API_SECRET']
  end
end

Ruby’s Singleton module gives you thread safety and clean tests. Same object every time:

Configuration.instance # => #<Configuration:0x00007ffca20fd998>
Configuration.instance # => #<Configuration:0x00007ffca20fd998>
Configuration.instance # => #<Configuration:0x00007ffca20fd998>

Factory

The Factory pattern wraps object creation. It shines when one trigger has to build several object types that share a shape.

Example: user types

Say your app has several user types, each with its own behavior and permissions.

# app/models/user_factory.rb
class UserFactory
  def self.create(user_type, attributes)
    case user_type
    when :admin
      AdminUser.new(attributes)
    when :guest
      GuestUser.new(attributes)
    else
      StandardUser.new(attributes)
    end
  end
end

The factory hides the instantiation logic and keeps callers clean.

You can also write it with metaprogramming:

# app/models/user_factory.rb
class UserFactory
  def self.create(user_type, attributes)
    if user_type
      klass = "#{user_type.humanize}User".constantize
      klass.new(attributes)
    else
      StandardUser.new(attributes)
    end
  end
end

PS: this pairs with STI. Your status column gets validated by an enum, with one model per enum value, which makes the version above developer-friendly. Metaprogramming can still be a trap.

Don’t overdo it

Patterns are tools, not trophies. Drop one where it is not needed and you just make the app harder to read. Always ask whether the pattern earns its place, or whether a plain constructor would do.

As a rule of thumb, these deserve a second thought before you reach for them:

  • Has and belongs to many tables
  • Multi-database connections
  • Wrapping AWS services
  • Metaprogramming
  • dry.rb
  • STI
Roland Lopez
Written by
Roland Lopez

Technical founder & AI crack-head

Built by Agent Skynet See the agency