DEV Community

Cover image for Design Patterns in Ruby #5: Abstract Factory Pattern
Shrouk Abozeid
Shrouk Abozeid

Posted on

Design Patterns in Ruby #5: Abstract Factory Pattern

In the previous article, we explored the Factory Method pattern and saw how it delegates object creation to subclasses.

But what if your application doesn't need to create just one object?

What if it needs an entire family of related objects that are designed to work together?

That's exactly the problem the Abstract Factory pattern solves.


The Problem

Imagine you're building an e-commerce application that supports multiple payment providers.

Your application currently integrates with:

  • Stripe
  • PayPal

For each payment provider, you need several related objects:

  • A Payment processor
  • A Refund processor
  • A Customer service

For Stripe, those objects might be:

StripePayment
StripeRefund
StripeCustomer
Enter fullscreen mode Exit fullscreen mode

For PayPal:

PayPalPayment
PayPalRefund
PayPalCustomer
Enter fullscreen mode Exit fullscreen mode

If you instantiate each object manually, your code quickly becomes filled with conditionals:

if provider == :stripe
  payment = StripePayment.new
  refund  = StripeRefund.new
  customer = StripeCustomer.new
else
  payment = PayPalPayment.new
  refund  = PayPalRefund.new
  customer = PayPalCustomer.new
end
Enter fullscreen mode Exit fullscreen mode

The problem isn't creating a single object anymore.

It's ensuring that all related objects come from the same payment provider.

The client shouldn't accidentally create a StripePayment with a PayPalRefund.


Abstract Factory

The Gang of Four defines Abstract Factory as:

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Instead of asking for individual objects, the client asks a factory to provide an entire family.

StripeFactory
 ├── Payment
 ├── Refund
 └── Customer

PayPalFactory
 ├── Payment
 ├── Refund
 └── Customer
Enter fullscreen mode Exit fullscreen mode

The client never knows which concrete classes it's using.

It only communicates with the factory interface.


Structure

The pattern consists of five participants.

Abstract Factory

Defines the interface for creating every product in the family.

create_payment
create_refund
create_customer
Enter fullscreen mode Exit fullscreen mode

Concrete Factory

Implements the Abstract Factory.

Each concrete factory creates one complete product family.

Examples:

  • StripeFactory
  • PayPalFactory

Abstract Product

Defines the common interface shared by each product.

Examples:

  • Payment
  • Refund
  • Customer

Concrete Products

The concrete implementations belonging to a specific family.

StripePayment
StripeRefund
StripeCustomer

PayPalPayment
PayPalRefund
PayPalCustomer
Enter fullscreen mode Exit fullscreen mode

Client

The client depends only on the Abstract Factory.

At runtime, it receives whichever concrete factory is needed.

This completely decouples the client from concrete implementations.


Ruby Example

Products

class StripePayment
  def process(amount)
    puts "Charging $#{amount} with Stripe"
  end
end

class StripeRefund
  def process(transaction_id)
    puts "Refunding #{transaction_id} through Stripe"
  end
end

class StripeCustomer
  def create(name)
    puts "Creating Stripe customer: #{name}"
  end
end

class PayPalPayment
  def process(amount)
    puts "Charging $#{amount} with PayPal"
  end
end

class PayPalRefund
  def process(transaction_id)
    puts "Refunding #{transaction_id} through PayPal"
  end
end

class PayPalCustomer
  def create(name)
    puts "Creating PayPal customer: #{name}"
  end
end
Enter fullscreen mode Exit fullscreen mode

Abstract Factory

class PaymentProviderFactory
  def create_payment
    raise NotImplementedError
  end

  def create_refund
    raise NotImplementedError
  end

  def create_customer
    raise NotImplementedError
  end
end
Enter fullscreen mode Exit fullscreen mode

Concrete Factories

class StripeFactory < PaymentProviderFactory
  def create_payment
    StripePayment.new
  end

  def create_refund
    StripeRefund.new
  end

  def create_customer
    StripeCustomer.new
  end
end

class PayPalFactory < PaymentProviderFactory
  def create_payment
    PayPalPayment.new
  end

  def create_refund
    PayPalRefund.new
  end

  def create_customer
    PayPalCustomer.new
  end
end
Enter fullscreen mode Exit fullscreen mode

Client

class CheckoutService
  def initialize(factory)
    @factory = factory
  end

  def checkout(customer_name, amount)
    customer = @factory.create_customer
    payment = @factory.create_payment

    customer.create(customer_name)
    payment.process(amount)
  end

  def refund(transaction_id)
    refund = @factory.create_refund
    refund.process(transaction_id)
  end
end
Enter fullscreen mode Exit fullscreen mode

Using the client:

factory = StripeFactory.new
checkout = CheckoutService.new(factory)

checkout.checkout("Alice", 100)
checkout.refund("txn_123")
Enter fullscreen mode Exit fullscreen mode

Switching to PayPal requires changing only one line:

factory = PayPalFactory.new
checkout = CheckoutService.new(factory)
Enter fullscreen mode Exit fullscreen mode

The rest of the application remains unchanged.

Notice that CheckoutService never references StripePayment, PayPalPayment, or any other concrete implementation.

It only communicates with the Abstract Factory.


Factory Method vs Abstract Factory

Although both patterns deal with object creation, they solve different problems.

Factory Method Abstract Factory
Creates one product Creates a family of related products
Uses inheritance Uses object composition
Subclasses decide what to create A factory object decides which family to create
Best when object creation varies Best when entire product families vary

Another way to think about it:

Factory Method asks:

Which object should I create?

Abstract Factory asks:

Which family of objects should I create?


Adding New Products vs New Families

Suppose our payment providers now support webhooks.

We need another product:

create_webhook
Enter fullscreen mode Exit fullscreen mode

With Abstract Factory, the abstract factory interface must change.

class PaymentProviderFactory
  def create_payment; end
  def create_refund; end
  def create_customer; end
  def create_webhook; end
end
Enter fullscreen mode Exit fullscreen mode

Every concrete factory (StripeFactory, PayPalFactory, etc.) must also implement create_webhook.

On the other hand, if you add a completely new payment provider—say Adyen—you simply introduce a new concrete factory:

AdyenFactory
 ├── AdyenPayment
 ├── AdyenRefund
 ├── AdyenCustomer
Enter fullscreen mode Exit fullscreen mode

The existing factories remain unchanged.

That's why people often say:

  • Factory Method is open to new creators.
  • Abstract Factory is open to new product families.

Where You'll See It in Rails

Although Rails doesn't explicitly label it as the Abstract Factory pattern, the idea appears in several places.

Active Storage

Depending on your configuration, Rails creates a family of storage-related objects for:

  • Local Disk
  • Amazon S3
  • Google Cloud Storage
  • Azure Blob Storage

Your application interacts with a common interface while Rails supplies the correct implementation behind the scenes.


When Should You Use It?

Use Abstract Factory when:

  • Your application works with multiple product families.
  • Objects within a family should always be used together.
  • You want to switch implementations through configuration.
  • You want to isolate your application from concrete classes.

Avoid it when you only need to create a single object. In that case, Factory Method or even a Simple Factory is usually a better fit.


Key Takeaways

The Factory Method pattern creates one object and relies on inheritance to determine which concrete product to instantiate.

The Abstract Factory pattern creates families of related objects and relies on composition to swap entire implementations at runtime.

A simple rule to remember is:

Factory Method answers: Which object should I create?

Abstract Factory answers: Which family of objects should I create?

for a full example in ruby check this https://github.com/ShroukAbozeid/design-pattern/tree/main/factory/abstract_factory

Top comments (0)