| name | factory-method-pattern |
| description | Creates objects through factory methods with Factory Method Pattern. Use for polymorphic object creation, notification systems, report generators, authentication providers, or when you need framework extensibility. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
Factory Method Pattern in Rails
Overview
The Factory Method Pattern defines an interface for creating objects, but lets subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
Key Insight: Replace direct object creation with factory methods that can be overridden to produce different product types polymorphically.
Core Components
Client → Creator (base) → Product (interface)
↓ ↓
Concrete Creators → Concrete Products
- Product Interface - Declares operations all products must implement
- Concrete Products - Different implementations of product interface
- Creator (Base) - Declares factory method returning products
- Concrete Creators - Override factory method to return specific products
When to Use Factory Method
✅ Use Factory Method when you need:
- Polymorphic object creation - Don't know exact types beforehand
- Framework extensibility - Let users extend with new product types
- Decouple creation from usage - Client doesn't depend on concrete classes
- Open/Closed Principle - Add new products without modifying existing code
❌ Don't use Factory Method for:
- Simple object creation (use constructor)
- Creating families of related objects (use Abstract Factory)
- Complex step-by-step construction (use Builder)
- Just centralizing creation logic (use Simple Factory)
Difference from Similar Patterns
| Aspect | Factory Method | Abstract Factory | Builder | Simple Factory |
|---|
| Purpose | Polymorphic creation | Family creation | Step-by-step | Centralized creation |
| Inheritance | Uses subclasses | Composition | Composition | Static method |
| Products | Single product type | Multiple products | Complex product | Single product |
| Extensibility | High (subclass) | High (new factory) | High (new steps) | Low (modify method) |
Common Rails Use Cases
1. Notification System
class Notification
def deliver
raise NotImplementedError
end
def recipient
raise NotImplementedError
end
end
class EmailNotification < Notification
def initialize(user:, message:, subject:)
@user, @message, @subject = user, message, subject
end
def deliver
NotificationMailer.send_notification(
to: @user.email,
subject: @subject,
body: @message
).deliver_later
end
def recipient
@user.email
end
end
class SmsNotification < Notification
def initialize(user:, message:)
@user, @message = user, message
end
def deliver
TwilioClient.send_sms(to: @user.phone, body: @message)
end
def recipient
@user.phone
end
end
class NotificationFactory
FACTORIES = {
email: 'EmailNotificationFactory',
sms: 'SmsNotificationFactory',
push: 'PushNotificationFactory'
}.freeze
def create_notification(user:, message:, **options)
raise NotImplementedError
end
def send_notification(user:, message:, **options)
notification = create_notification(user: user, message: message, **options)
notification.deliver
end
def self.for(type)
factory_class_name = FACTORIES[type]
raise ArgumentError, "Unknown notification type: #{type}" unless factory_class_name
Object.const_get(factory_class_name).new
end
end
class EmailNotificationFactory < NotificationFactory
def create_notification(user:, message:, subject: "Notification", **options)
EmailNotification.new(user: user, message: message, subject: subject)
end
end
class SmsNotificationFactory < NotificationFactory
def create_notification(user:, message:, **options)
SmsNotification.new(user: user, message: message)
end
end
factory = NotificationFactory.for(user.notification_preference)
factory.send_notification(user: user, message: "Hello!")
2. Report Generators (Composition Pattern)
Note: This example uses composition over inheritance. The Report class receives a formatter strategy through dependency injection, making it more flexible and following the Open/Closed Principle.
class ReportFormatter
def format(data)
raise NotImplementedError
end
def content_type
raise NotImplementedError
end
end
class PdfFormatter < ReportFormatter
def format(data)
Prawn::Document.new do |pdf|
pdf.text "Sales Report"
data.each { |row| pdf.text row.to_s }
end.render
end
def content_type
'application/pdf'
end
end
class ExcelFormatter < ReportFormatter
def format(data)
package = Axlsx::Package.new
workbook = package.workbook
workbook.add_worksheet(name: "Sales") do |sheet|
data.each { |row| sheet.add_row row }
end
package.to_stream.read
end
def content_type
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
end
end
class CsvFormatter < ReportFormatter
def format(data)
CSV.generate do |csv|
data.each { |row| csv << row }
end
end
def content_type
'text/csv'
end
end
class Report
attr_reader :data, :formatter
def initialize(data:, formatter:)
@data = data
@formatter = formatter
end
def generate
formatter.format(data)
end
def content_type
formatter.content_type
end
end
class ReportFactory
FORMATTERS = {
pdf: PdfFormatter,
excel: ExcelFormatter,
csv: CsvFormatter
}.freeze
def create_report(data:)
raise NotImplementedError
end
def self.for(format)
formatter_class = FORMATTERS[format]
raise ArgumentError, "Unknown format: #{format}" unless formatter_class
new(formatter_class)
end
def initialize(formatter_class)
@formatter_class = formatter_class
end
def create_report(data:)
Report.new(data: data, formatter: @formatter_class.new)
end
end
factory = ReportFactory.for(params[:format])
report = factory.create_report(data: @sales_data)
send_data report.generate, type: report.content_type
3. Authentication Providers
class AuthProvider
def authenticate(credentials)
raise NotImplementedError
end
end
class PasswordAuthProvider < AuthProvider
def authenticate(credentials)
user = User.find_by(email: credentials[:email])
user&.authenticate(credentials[:password]) ? user : nil
end
end
class OauthAuthProvider < AuthProvider
def initialize(provider)
@provider = provider
end
def authenticate(credentials)
User.find_or_create_by_oauth(@provider, credentials[:auth_hash])
end
end
class TokenAuthProvider < AuthProvider
def authenticate(credentials)
decoded = JWT.decode(credentials[:token], Rails.application.secret_key_base)
User.find(decoded[0]['user_id'])
rescue JWT::DecodeError
nil
end
end
class AuthProviderFactory
FACTORIES = {
password: 'PasswordAuthProviderFactory',
oauth: 'OauthAuthProviderFactory',
token: 'TokenAuthProviderFactory'
}.freeze
def create_provider
raise NotImplementedError
end
def self.for(type, **options)
factory_class_name = FACTORIES[type]
raise ArgumentError, "Unknown auth type: #{type}" unless factory_class_name
factory_class = Object.const_get(factory_class_name)
if type == :oauth
factory_class.new(options[:provider])
else
factory_class.new
end
end
end
factory = AuthProviderFactory.for(auth_type, provider: params[:provider])
provider = factory.create_provider
if user = provider.authenticate(credentials)
sign_in(user)
redirect_to dashboard_path
else
render :new, alert: "Authentication failed"
end
4. Payment Processors
class PaymentProcessorFactory
FACTORIES = {
stripe: 'StripeProcessorFactory',
paypal: 'PaypalProcessorFactory',
braintree: 'BraintreeProcessorFactory'
}.freeze
def create_processor
raise NotImplementedError
end
def self.for(gateway)
factory_class_name = FACTORIES[gateway]
raise ArgumentError, "Unknown gateway: #{gateway}" unless factory_class_name
Object.const_get(factory_class_name).new
end
end
factory = PaymentProcessorFactory.for(user.preferred_gateway)
processor = factory.create_processor
result = processor.process(amount: order.total, details: payment_details)
Implementation Guidelines
1. Define Clear Product Interface
class Product
def operation
raise NotImplementedError, "#{self.class} must implement #operation"
end
end
2. Use Factory Method in Template Methods
class Creator
def create_product
raise NotImplementedError
end
def execute
product = create_product
product.operation
log_result(product)
end
private
def log_result(product)
end
end
3. Use Registry Pattern with Metaprogramming (Open/Closed Principle)
Avoid case statements - Use a registry hash with metaprogramming to make factories extensible without modification:
class NotificationFactory
FACTORIES = {
email: 'EmailNotificationFactory',
sms: 'SmsNotificationFactory',
push: 'PushNotificationFactory'
}.freeze
def create_notification(user:, message:, **options)
raise NotImplementedError
end
def self.for(type)
factory_class_name = FACTORIES[type]
raise ArgumentError, "Unknown type: #{type}" unless factory_class_name
Object.const_get(factory_class_name).new
end
end
Benefits of this approach:
- ✅ Open/Closed Principle: Add new types by registering in FACTORIES hash
- ✅ No case statements: Scales better as you add more types
- ✅ Easy to test: Can stub FACTORIES for testing
- ✅ Configuration-driven: Can load FACTORIES from config/initializers
4. Prefer Composition Over Inheritance
Inheritance is for specialization and should be narrow/shallow. For many factory scenarios, composition with dependency injection is more flexible:
class Report
def generate; raise NotImplementedError; end
end
class PdfReport < Report
def generate;
end
class ExcelReport < Report
def generate;
end
class ReportFormatter
def format(data); raise NotImplementedError; end
end
class PdfFormatter < ReportFormatter
def format(data);
end
class Report
def initialize(data:, formatter:)
@data = data
@formatter = formatter
end
def generate
@formatter.format(@data)
end
end
class ReportFactory
def self.for(format)
formatter = FORMATTERS[format].new
new(formatter)
end
def initialize(formatter)
@formatter = formatter
end
def create_report(data:)
Report.new(data: data, formatter: @formatter)
end
end
When to use composition:
- Behavior varies but core structure is the same
- You want runtime flexibility (swap formatters)
- Multiple dimensions of variation (format + template + theme)
When inheritance is OK:
- True specialization (Car < Vehicle, Admin < User)
- Shared implementation with small variations
- Deep domain modeling where "is-a" relationship is clear
5. Keep Factories Simple
class EmailNotificationFactory < NotificationFactory
def create_notification(user:, message:, **options)
EmailNotification.new(user: user, message: message)
end
end
class BadEmailNotificationFactory < NotificationFactory
def create_notification(user:, message:, **options)
message = add_branding(message) if user.premium?
EmailNotification.new(user: user, message: message)
end
end
Testing Factory Method
RSpec.shared_examples 'a notification' do
it 'responds to send' do
expect(subject).to respond_to(:send)
end
it 'responds to recipient' do
expect(subject).to respond_to(:recipient)
end
end
RSpec.describe EmailNotification do
it_behaves_like 'a notification'
describe '#deliver' do
it 'enqueues email' do
expect { subject.deliver }.to have_enqueued_mail
end
end
end
RSpec.describe EmailNotificationFactory do
describe '#create_notification' do
it 'creates EmailNotification' do
notification = subject.create_notification(
user: user,
message: "Test"
)
expect(notification).to be_a(EmailNotification)
end
end
end
RSpec.describe NotificationFactory do
describe '.for' do
it 'returns EmailNotificationFactory for :email' do
factory = described_class.for(:email)
expect(factory).to be_a(EmailNotificationFactory)
end
it 'raises error for unknown type' do
expect {
described_class.for(:unknown)
}.to raise_error(ArgumentError)
end
end
end
Anti-Patterns to Avoid
❌ Don't Use Case Statements (Violates Open/Closed)
class NotificationFactory
def self.for(type)
case type
when :email then EmailNotificationFactory.new
when :sms then SmsNotificationFactory.new
end
end
end
class NotificationFactory
FACTORIES = {
email: 'EmailNotificationFactory',
sms: 'SmsNotificationFactory'
}.freeze
def self.for(type)
factory_class_name = FACTORIES[type]
raise ArgumentError, "Unknown type: #{type}" unless factory_class_name
Object.const_get(factory_class_name).new
end
end
❌ Don't Put Business Logic in Factories
class BadFactory < NotificationFactory
def create_notification(user:, message:, **options)
message = premium_template(message) if user.premium?
schedule = user.timezone_adjusted_time
EmailNotification.new(user: user, message: message, schedule: schedule)
end
end
class GoodFactory < NotificationFactory
def create_notification(user:, message:, **options)
EmailNotification.new(user: user, message: message)
end
end
class NotificationService
def send(user:, message:)
message = premium_template(message) if user.premium?
factory = NotificationFactory.for(user.preference)
notification = factory.create_notification(user: user, message: message)
notification.deliver
end
end
❌ Don't Overuse Inheritance When Composition Is Better
class Report
def generate; raise NotImplementedError; end
end
class PdfReport < Report; end
class ExcelReport < Report; end
class CsvReport < Report; end
class HtmlReport < Report; end
class Report
def initialize(data:, formatter:)
@data = data
@formatter = formatter
end
def generate
@formatter.format(@data)
end
end
Remember: Inheritance is for specialization and should be narrow/shallow. Use composition when you're varying behavior rather than specializing types.
❌ Don't Skip Product Interface
class EmailNotification
def send_email; end
end
class SmsNotification
def send_sms; end
end
class Notification
def deliver
raise NotImplementedError
end
end
class EmailNotification < Notification
def deliver
end
end
❌ Don't Create God Factories
class GodFactory
def create_user; end
def create_post; end
def create_notification; end
def create_payment; end
end
class UserFactory; end
class NotificationFactory; end
class PaymentFactory; end
Decision Tree
When to use Factory Method vs alternatives:
Creating a single object with known type?
→ YES: Use constructor
→ NO: Keep reading
Need step-by-step construction with many options?
→ YES: Use Builder Pattern
→ NO: Keep reading
Creating families of related objects?
→ YES: Use Abstract Factory
→ NO: Keep reading
Just centralizing simple creation logic?
→ YES: Use Simple Factory (static method)
→ NO: Keep reading
Need polymorphic creation with subclass extensibility?
→ YES: Use Factory Method Pattern ✅
Benefits
✅ Decoupling - Client doesn't depend on concrete classes
✅ Extensibility - Add new products without modifying existing code
✅ Polymorphism - Create objects without knowing exact types
✅ Single Responsibility - Creation logic separated from usage
✅ Open/Closed - Open for extension, closed for modification
Drawbacks
❌ More classes - Requires factory hierarchy
❌ Indirection - Extra layer between client and product
❌ Overkill - Too complex for simple creation needs
Real-World Rails Examples
File Parsers
class ParserFactory
FACTORIES = {
csv: 'CsvParserFactory',
json: 'JsonParserFactory',
xml: 'XmlParserFactory'
}.freeze
def self.for(format)
factory_class_name = FACTORIES[format]
raise ArgumentError, "Unknown format: #{format}" unless factory_class_name
Object.const_get(factory_class_name).new
end
end
factory = ParserFactory.for(file.format)
parser = factory.create_parser
data = parser.parse(file.content)
Shipping Calculators
class ShippingCalculatorFactory
FACTORIES = {
ups: 'UpsCalculatorFactory',
fedex: 'FedexCalculatorFactory',
usps: 'UspsCalculatorFactory'
}.freeze
def self.for(carrier)
factory_class_name = FACTORIES[carrier]
raise ArgumentError, "Unknown carrier: #{carrier}" unless factory_class_name
Object.const_get(factory_class_name).new
end
end
factory = ShippingCalculatorFactory.for(order.shipping_carrier)
calculator = factory.create_calculator
cost = calculator.calculate(order.weight, order.destination)
Search Engines
class SearchEngineFactory
FACTORIES = {
elasticsearch: 'ElasticsearchFactory',
postgres: 'PostgresSearchFactory',
algolia: 'AlgoliaFactory'
}.freeze
def self.for(engine)
factory_class_name = FACTORIES[engine]
raise ArgumentError, "Unknown engine: #{engine}" unless factory_class_name
Object.const_get(factory_class_name).new
end
end
factory = SearchEngineFactory.for(Rails.configuration.search_engine)
search = factory.create_search
results = search.query(params[:q])
Summary
Use Factory Method when:
- You don't know exact types beforehand
- You want framework extensibility
- You need polymorphic object creation
- Client shouldn't depend on concrete classes
Avoid Factory Method when:
- Simple object creation (use constructor)
- Creating families (use Abstract Factory)
- Complex construction (use Builder)
- Just centralizing (use Simple Factory)
Most common Rails use cases:
- Notification systems (Email, SMS, Push, Slack)
- Report generators (PDF, Excel, CSV, HTML)
- Authentication providers (Password, OAuth, Token, SSO)
- Payment processors (Stripe, PayPal, Braintree)
- File parsers (CSV, JSON, XML, YAML)
- Search engines (Elasticsearch, PostgreSQL, Algolia)
- Shipping calculators (UPS, FedEx, USPS, DHL)
- Logging adapters (File, Database, External service)
Key Pattern:
class Product
def operation; raise NotImplementedError; end
end
class ConcreteProductA < Product
def operation; end
end
class Factory
def create_product; raise NotImplementedError; end
def self.for(type); end
end
class ConcreteFactoryA < Factory
def create_product
ConcreteProductA.new
end
end
factory = Factory.for(:a)
product = factory.create_product
product.operation