Skip to Content
Course content

94: The Factory Pattern in Ruby

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was building a payment integration for an e-commerce site. At first, it looked fine—they had a StripeProcessor class that handled everything. But then the business decided to add PayPal and Square. I remember opening the CheckoutService and seeing a massive if/elsif/else block that spanned thirty lines just to decide which class to instantiate based on a string from the database. It was brittle, ugly, and every time we added a new payment method, we had to touch the core checkout logic, which is a recipe for introducing bugs in the most sensitive part of the app.

This is exactly where the Factory Pattern saves your sanity. In essence, a Factory is an object whose sole responsibility is to create other objects. Instead of your business logic worrying about how to instantiate a specific class or which specific class is needed, it just tells the Factory: "I need a processor for 'stripe'," and the Factory handles the messy details of returning the right instance.

Stop Scattering Your New Keywords

When you call .new on a class, you're hard-coding a dependency. If you have StripeProcessor.new sprinkled across five different services, and you later decide that StripeProcessor now requires an API key in its initializer, you have to hunt down every single one of those calls and update them. That's a tedious way to spend a Tuesday.

By wrapping this in a Factory, you centralize the instantiation. Your service layer doesn't know StripeProcessor exists; it only knows the PaymentProcessorFactory. This decoupling means you can change your class names, add initialization logic, or even switch to a different implementation entirely without touching your business logic.

class StripeProcessor
  def process(amount)
    puts "Processing $#{amount} via Stripe..."
  end
end

class PaypalProcessor
  def process(amount)
    puts "Processing $#{amount} via PayPal..."
  end
end

class PaymentProcessorFactory
  def self.for(type)
    case type
    when 'stripe' then StripeProcessor.new
    when 'paypal' then PaypalProcessor.new
    else
      raise "Unsupported payment method: #{type}"
    end
  end
end

Keeping it Ruby-ish with Hash Lookups

While a case statement works, it can get bloated. Since we're working in Ruby, we can leverage the fact that classes are first-class objects. We can store the classes themselves in a hash and call .new on the result. I personally prefer this approach because it's cleaner and makes it incredibly easy to register new types dynamically if you ever need to.

Check out how much leaner this becomes. We map the identifier to the class, fetch the class, and then instantiate it. It's a tiny change that makes the code feel much more professional and extensible.

class PaymentProcessorFactory
  PROCESSORS = {
    'stripe' => StripeProcessor,
    'paypal' => PaypalProcessor,
    'square' => SquareProcessor
  }.freeze

  def self.for(type)
    processor_class = PROCESSORS[type]
    
    raise "Unsupported payment method: #{type}" unless processor_class
    
    processor_class.new
  end
end

# Now the client code is blissfully ignorant:
processor = PaymentProcessorFactory.for('stripe')
processor.process(100)

The beauty here is that the CheckoutService now only cares that the object it receives responds to .process. It doesn't care if it's Stripe, PayPal, or some obscure local bank API. You've effectively isolated the "creation" logic from the "execution" logic.




📋 Practical Task

The Multi-Channel Notification Dispatcher

You are building a notification system for a social media app. Depending on the user's preferences, a notification needs to be sent via Email, SMS, or Push Notification.

Your task:

  1. Create three classes: EmailNotification, SmsNotification, and PushNotification. Each should have a method called send_message(message) that prints "Sending [Message] via [Channel]".
  2. Implement a NotificationFactory that uses a hash lookup (not a case statement) to return the correct notification object based on a symbol (e.g., :email, :sms, :push).
  3. Ensure the factory raises a custom error or a descriptive RuntimeError if an unsupported notification type is requested.
  4. Write a small script that iterates through an array of preferences [:email, :push, :carrier_pigeon, :sms] and attempts to send the message "Your post was liked!" using the factory. Your code should handle the error gracefully when it hits :carrier_pigeon.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.