In the world of software development, design patterns and anti-patterns play a critical role in writing maintainable, clean, and scalable code. As a Ruby developer, mastering these concepts will help you design robust applications and avoid common pitfalls.
What are Design Patterns?
Design patterns are time-tested, reusable solutions to common problems in software design. They aren’t code snippets but conceptual templates you can adapt based on your needs. Think of them as architectural blueprints.
🧱 Common Ruby Design Patterns
1. Singleton Pattern
the_logger = Logger.new
class Logger
@@instance = Logger.new
def self.instance
@@instance
end
private_class_method :new
end
Use when only one instance of a class should exist (e.g., logger, configuration manager).
🧠 What is the Singleton Pattern?
The Singleton Pattern ensures that only one instance of a class exists during the lifetime of an application. This is useful for managing shared resources like:
- a logger (so logs are not duplicated or misdirected),
- a configuration manager (so all parts of the app read/write the same config),
- or a database connection pool.
🔍 Why private_class_method :new?
This line prevents other parts of the code from calling Logger.new, like this:
Logger.new # ❌ Will raise a NoMethodError
So, you’re restricting object creation from outside the class.
Q) Then how do you get an object of Logger?
By using a class method like Logger.instance that returns the only instance of the class.
✅ Full Singleton Example in Ruby
class Logger
# Create and store the single instance
@@instance = Logger.new
# Provide a public way to access that instance
def self.instance
@@instance
end
# Prevent external instantiation
private_class_method :new
# Example method
def log(message)
puts "[LOG] #{message}"
end
end
# Usage
logger1 = Logger.instance
logger2 = Logger.instance
logger1.log("Singleton works!")
puts logger1.object_id == logger2.object_id # true
🧾 Explanation:
@@instance = Logger.new: Creates the only instance when the class is loaded.
Logger.instance: The only way to access that object.
private_class_method :new: Prevents creation of new objects using Logger.new.
logger1 == logger2: ✅ True, because they point to the same object.
💬 Think of a Real-World Example
Imagine a central control tower at an airport:
- There should only be one control tower instance managing flights.
- If each plane connected to a new tower, it would be chaos!
The Singleton pattern in Ruby ensures there’s just one control tower (object) shared globally.
2. Observer Pattern
class Order
include Observable
def place_order
changed
notify_observers(self)
end
end
class EmailNotifier
def update(order)
puts "Email sent for order #{order.id}"
end
end
order = Order.new
order.add_observer(EmailNotifier.new)
order.place_order
Use when one change in an object should trigger actions in other objects.
🔁 Observer Pattern in Ruby – Explained in Detail
The Observer Pattern is a behavioral design pattern that lets one object (the subject) notify other objects (the observers) when its state changes.
Ruby has built-in support for this through the Observable module in the standard library (require 'observer').
How it Works
- The Subject (e.g.,
Order) includes the Observable module.
- The subject calls:
changed → marks the object as changed.
notify_observers(data) → notifies all subscribed observers.
- Observers (e.g.,
EmailNotifier) implement an update method.
- Observers are registered using
add_observer(observer_object).
🧪 Complete Working Example
require 'observer'
class Order
include Observable
attr_reader :id
def initialize(id)
@id = id
end
def place_order
puts "Placing order #{@id}..."
changed # Mark this object as changed
notify_observers(self) # Notify all observers
end
end
class EmailNotifier
def update(order)
puts "📧 Email sent for Order ##{order.id}"
end
end
class SMSNotifier
def update(order)
puts "📱 SMS sent for Order ##{order.id}"
end
end
# Create subject and observers
order = Order.new(101)
order.add_observer(EmailNotifier.new)
order.add_observer(SMSNotifier.new)
order.place_order
# Output:
# Placing order 101...
# 📧 Email sent for Order #101
# 📱 SMS sent for Order #101
🧠 When to Use the Observer Pattern
- You have one object whose changes should automatically update other dependent objects.
- Examples:
- UI updates in response to data changes
- Logging, email, or analytics triggers after a user action
- Notification systems in event-driven apps
Updated Observer Pattern
require 'observer'
class Order
include Observable
attr_reader :id
def initialize(id)
@id = id
end
def place_order
changed
notify_observers(self)
end
end
class EmailNotifier
def update(order)
puts "📧 Email sent for Order ##{order.id}"
end
end
order = Order.new(42)
order.add_observer(EmailNotifier.new)
order.place_order
What’s Happening?
Order includes the Observable module to gain observer capabilities.
add_observer registers an observer object.
- When
place_order is called:
changed marks the state as changed.
notify_observers(self) triggers the observer’s update method.
EmailNotifier reacts to the change — in this case, it simulates sending an email.
🧩 Use this pattern when one change should trigger multiple actions — like sending notifications, logging, or syncing data across objects.
Check: https://docs.ruby-lang.org/en/2.2.0/Observable.html
3. Decorator Pattern
class SimpleCoffee
def cost
2
end
end
class MilkDecorator
def initialize(coffee)
@coffee = coffee
end
def cost
@coffee.cost + 0.5
end
end
coffee = MilkDecorator.new(SimpleCoffee.new)
puts coffee.cost # => 2.5
Add responsibilities to objects dynamically without modifying their code.
🔄 4. Strategy Pattern
The Strategy Pattern allows choosing an algorithm’s behaviour at runtime. This pattern is useful when you have multiple interchangeable ways to perform a task.
✅ Example: Text Formatter Strategies
Let’s say you have a system that outputs text in different formats — plain, HTML, or Markdown.
❌ Before Using Strategy Pattern — Hardcoded Conditional Formatting
class TextFormatter
def initialize(format)
@format = format
end
def format(text)
case @format
when :plain
text
when :html
"<p>#{text}</p>"
when :markdown
"**#{text}**"
else
raise "Unknown format: #{@format}"
end
end
end
# Usage
formatter1 = TextFormatter.new(:html)
puts formatter1.format("Hello, World") # => <p>Hello, World</p>
formatter2 = TextFormatter.new(:markdown)
puts formatter2.format("Hello, World") # => **Hello, World**
⚠️ Problems With This Approach
- All formatting logic is stuffed into one method.
- Adding a new format (like XML) means modifying this method (violates Open/Closed Principle).
- Not easy to test or extend individual formatting behaviors.
- Harder to maintain and violates SRP (Single Responsibility Principle).
This sets the stage perfectly to apply the Strategy Pattern, where each format becomes its own class with a clear responsibility.
Instead of writing if/else logic everywhere, use strategy objects:
# Strategy Interface
class TextFormatter
def self.for(format)
case format
when :plain
PlainFormatter.new
when :html
HtmlFormatter.new
when :markdown
MarkdownFormatter.new
else
raise "Unknown format"
end
end
end
# Concrete Strategies
class PlainFormatter
def format(text)
text
end
end
class HtmlFormatter
def format(text)
"<p>#{text}</p>"
end
end
class MarkdownFormatter
def format(text)
"**#{text}**"
end
end
# Usage
def render_text(text, format)
formatter = TextFormatter.for(format)
formatter.format(text)
end
puts render_text("Hello, world", :html) # => <p>Hello, world</p>
puts render_text("Hello, world", :markdown) # => **Hello, world**
📌 Why It’s Better
- Adds new formats easily without changing existing code.
- Keeps formatting logic isolated in dedicated classes.
- Follows the Open/Closed Principle.
💳 Strategy Pattern in Rails: Payment Gateway Integration
Here’s a Rails-specific Strategy Pattern example. This example uses a service to handle different payment gateways (e.g., Stripe, PayPal, Razorpay), which are chosen dynamically based on configuration or user input.
Problem:
You want to process payments, but the actual logic differs depending on which gateway (Stripe, PayPal, Razorpay) is being used. Avoid if/else or case all over your controller or service.
✅ Solution Using Strategy Pattern
# app/services/payment_processor.rb
class PaymentProcessor
def self.for(gateway)
case gateway.to_sym
when :stripe
StripeGateway.new
when :paypal
PaypalGateway.new
when :razorpay
RazorpayGateway.new
else
raise "Unsupported payment gateway"
end
end
end
# app/services/stripe_gateway.rb
class StripeGateway
def charge(amount, user)
# Stripe API integration here
puts "Charging #{user.name} ₹#{amount} via Stripe"
end
end
# app/services/paypal_gateway.rb
class PaypalGateway
def charge(amount, user)
# PayPal API integration here
puts "Charging #{user.name} ₹#{amount} via PayPal"
end
end
# app/services/razorpay_gateway.rb
class RazorpayGateway
def charge(amount, user)
# Razorpay API integration here
puts "Charging #{user.name} ₹#{amount} via Razorpay"
end
end
# app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def create
user = User.find(params[:user_id])
gateway = params[:gateway] # e.g., 'stripe', 'paypal', etc.
amount = params[:amount].to_i
processor = PaymentProcessor.for(gateway)
processor.charge(amount, user)
render json: { message: \"Payment processed via #{gateway.capitalize}\" }
end
end
✅ Benefits in Rails Context
- Keeps your controller slim and readable.
- Each gateway integration is encapsulated in its own class.
- Easy to test, extend, and maintain (open/closed principle).
- Avoids future code smell like “Shotgun Surgery”.
⚠️ What are Anti-Patterns?
Anti-patterns are poor programming practices or ineffective solutions that seem helpful at first but cause long-term issues like unmaintainable code or hidden bugs.
Common Ruby Anti-Patterns
🧨 1. God Object
A class that knows too much or does too much.
class UserManager
def create_user
# logic
end
def send_email
# unrelated responsibility
end
def generate_report
# another unrelated responsibility
end
end
Fix: Follow the Single Responsibility Principle. Split into smaller, focused classes.
🧨 Shotgun Surgery
Tiny changes require touching many different places in code.
# Example of Shotgun Surgery - Business rule spread across many files
# In order.rb
class Order
def eligible_for_discount?
user.vip? && total > 1000
end
end
# In invoice.rb
class Invoice
def apply_discount(order)
if order.user.vip? && order.total > 1000
# apply discount logic
end
end
end
# In email_service.rb
class EmailService
def send_discount_email(order)
if order.user.vip? && order.total > 1000
# send congratulatory email
end
end
end
Here, the logic user.vip? && total > 1000 is repeated in multiple places. If the discount eligibility rules change (e.g., change the threshold to 2000 or add a new condition), you’ll have to update every occurrence.
✅ Fix: Centralize the Logic
class DiscountPolicy
def self.eligible?(order)
order.user.vip? && order.total > 1000
end
end
Now all files can use DiscountPolicy.eligible?(order), ensuring consistency and easier maintenance.
3. Spaghetti Code
Unstructured and difficult-to-follow code.
def calculate_discount(user, items)
if user.vip?
# deeply nested logic
total = items.sum { |i| i.price }
total * 0.2
else
if user.new_customer?
total = items.sum { |i| i.price }
total * 0.1
else
total = items.sum { |i| i.price }
total * 0.05
end
end
end
This code is hard to read, hard to extend, and violates SRP (Single Responsibility Principle).
Fix: Break into smaller methods, use polymorphism or strategy patterns.
✅ Refactored with Strategy Pattern
# Strategy Interface
class DiscountStrategy
def self.for(user)
if user.vip?
VipDiscount.new
elsif user.new_customer?
NewCustomerDiscount.new
else
RegularDiscount.new
end
end
end
# Concrete Strategies
class VipDiscount
def apply(items)
total = items.sum(&:price)
total * 0.2
end
end
class NewCustomerDiscount
def apply(items)
total = items.sum(&:price)
total * 0.1
end
end
class RegularDiscount
def apply(items)
total = items.sum(&:price)
total * 0.05
end
end
# Usage
def calculate_discount(user, items)
strategy = DiscountStrategy.for(user)
strategy.apply(items)
end
🎯 Benefits
- No nested conditionals
- Easy to add new discount types (open/closed principle)
- Each class has one responsibility
- Testable and readable
🔍 How to Detect Anti-Patterns in Ruby Code
- Code Smells:
- Long methods
- Large classes
- Repetition (DRY violations)
- Deep nesting
- Code Review Checklist:
- Is each class doing only one thing?
- Can this method be broken down?
- Are we repeating business logic?
- Is the code testable?
- Use Static Analysis Tools:
- Rubocop for style and complexity checks
- Reek for code smells
- Flog for measuring code complexity
🛠 How to Refactor Anti-Patterns
- Use Service Objects: Extract complex logic into standalone classes.
- Apply Design Patterns: Choose the right pattern that matches your need (Strategy, Adapter, etc).
- Keep Methods Small: Limit to a single task; ideally under 10 lines.
- Write Tests First: Test-driven development (TDD) helps spot untestable designs.
🎯 Conclusion
Understanding design patterns and identifying anti-patterns is crucial for writing better Ruby code. While patterns guide you toward elegant solutions, anti-patterns warn you about common mistakes. With good design principles, automated tools, and thoughtful reviews, your Ruby codebase can remain healthy, scalable and developer-friendly.
Happy Designing Ruby! ✨