Why use design patterns in Rails?
Ruby on Rails, as a framework, encourages convention over configuration, making it easy to follow best practices.
However, despite Rails’ simplifications, complex problems needing elegant solutions will always exist. This is where design patterns come into play.
Design patterns in Rails serve as a standardized approach to common problems, promoting code reusability and maintainability.
Understanding when and how to apply these patterns can significantly optimize your application architecture.
Singleton pattern: a Rails example
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. In Rails, Singleton can be useful for managing a shared resource throughout the application, like a configuration settings object.
Bad practice: misusing global variables
# app/models/global_config.rb
class GlobalConfig
@@instance = new
def self.instance
@@instance
end
private_class_method :new
end
This approach, using class variables, risks threading issues and complicates testing due to state persistence between tests.
Preferred Singleton implementation
# 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
Using Ruby’s Singleton module, we sidestep potential pitfalls by ensuring thread safety and making unit tests easier to manage.
Same object ID every time:
Configuration.instance # => #<Configuration:0x00007ffca20fd998>
Configuration.instance # => #<Configuration:0x00007ffca20fd998>
Configuration.instance # => #<Configuration:0x00007ffca20fd998>
Factory pattern: managing object creation in Rails
The Factory pattern provides a way to encapsulate the instantiation of objects. This pattern is ideal when dealing with a system that must manage, maintain, or manipulate collections of objects that share common characteristics.
Example: different user types
Suppose your application handles different types of users, each with unique behaviors 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
This Factory abstracts the instantiation logic and promotes a cleaner codebase by encapsulating creation details.
The code above could also be refactored using 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 pattern is used with STI. In that case your
statuscolumn would be validated by an enum and you would have one model per enum value, making the code above developer-friendly. Metaprogramming can be a trap sometimes.
Overusing design patterns
While design patterns provide robust solutions, they come with the risk of overuse. Implementing a pattern where it is not needed can overcomplicate the application, making it harder to understand and maintain. Always evaluate if a pattern adds value to your solution or if a simpler approach could be enough.
Conclusion
As a rule of thumb, here are some overkill solutions that require second thought:
- Has and belongs to many tables
- Multi-database connections
- Include AWS services
- Metaprogramming
- Use dry.rb
- STI
Technical co-founder specialized in SaaS, DevOps, AI agents, and data platforms. Building and scaling with Ruby on Rails, n8n, and fast feedback loops.
Ruby on Rails and the Art of Object-Oriented Design
Rails' conventions guide you toward design patterns, but real maintainability comes from applying SRP, delegation, and SQL-first thinking from day one.
Redesign Your Models for Performance in Rails
Most performance issues come from over-normalized models. Learn how scopes, denormalization, and JSON columns simplify queries and remove painful joins.
Refactoring Fat Models in Rails: Delegate, Extract, and Compose
Reduce complexity in Rails models with method delegation, extraction into service classes, and composed_of - keeping ActiveRecord lean and SRP-aligned.