| name | strategy-pattern |
| description | Implements interchangeable algorithms with Strategy Pattern. Use when implementing payment gateways, notification channels, export formats, authentication methods, or any scenario requiring runtime algorithm selection. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
Strategy Pattern in Rails
Overview
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Key Insight: Replace conditional logic with polymorphic strategy objects that can be swapped at runtime.
Core Components
Client → Context → Strategy (Interface)
↓
ConcreteStrategyA
ConcreteStrategyB
ConcreteStrategyC
- Strategy (Interface) - Declares method all strategies must implement
- Concrete Strategies - Implement different algorithm variations
- Context - Maintains reference to strategy, delegates work to it
- Client - Selects and configures the appropriate strategy
When to Use Strategy Pattern
✅ Use Strategy Pattern when you need:
- Multiple ways to do the same thing - Payment gateways (Stripe, PayPal, Credit Card)
- Runtime algorithm selection - Choose behavior based on user preference
- Eliminate large conditionals - Replace case/if statements with strategy objects
- Independent, interchangeable algorithms - Notification channels, export formats
❌ Don't use Strategy Pattern for:
- Simple cases with 1-2 variants (overkill)
- Algorithm needs to modify object state (use State pattern)
- One-time operations (use Command pattern)
- Need algorithm skeleton with hooks (use Template Method)
Difference from Similar Patterns
| Aspect | Strategy | State | Template Method | Command |
|---|
| Purpose | Interchangeable algorithms | Behavior changes with state | Algorithm skeleton | Encapsulate request |
| Selection | Runtime (client chooses) | Automatic (state driven) | Compile-time (inheritance) | Execute later/queue |
| Context awareness | No | Yes | No | No |
| Typical use | Payments, exports | Order states, workflow | Import/export base class | Undo/redo |
Common Rails Use Cases
1. Payment Processing
class PaymentProcessor
def charge(amount, method, details)
case method
when 'stripe'
Stripe::Charge.create(amount: amount * 100, source: details[:token])
when 'paypal'
when 'credit_card'
end
end
end
class PaymentProcessor
def initialize(strategy:)
@strategy = strategy
end
def charge(amount:, details:)
@strategy.charge(amount: amount, payment_details: details)
end
end
class StripeStrategy < PaymentStrategy
def charge(amount:, payment_details:)
end
end
class PaypalStrategy < PaymentStrategy
def charge(amount:, payment_details:)
end
end
strategy = params[:method] == 'stripe' ? StripeStrategy.new : PaypalStrategy.new
processor = PaymentProcessor.new(strategy: strategy)
processor.charge(amount: 100, details: payment_details)
2. Notification Channels
class NotificationStrategy
def deliver(recipient:, message:)
raise NotImplementedError
end
end
class EmailStrategy < NotificationStrategy
def deliver(recipient:, message:)
NotificationMailer.send_notification(
to: recipient.email,
subject: message[:subject],
body: message[:body]
).deliver_later
end
end
class SmsStrategy < NotificationStrategy
def deliver(recipient:, message:)
TwilioClient.send_sms(
to: recipient.phone,
body: message[:body]
)
end
end
class PushStrategy < NotificationStrategy
def deliver(recipient:, message:)
FCM.send_notification(
device_token: recipient.device_token,
title: message[:subject],
body: message[:body]
)
end
end
class NotificationSender
def initialize(strategy:)
@strategy = strategy
end
def deliver(recipient:, message:)
@strategy.deliver(recipient: recipient, message: message)
end
end
class NotificationStrategyRegistry
STRATEGIES = {
email: 'EmailStrategy',
sms: 'SmsStrategy',
push: 'PushStrategy'
}.freeze
def self.for(type)
strategy_class_name = STRATEGIES[type.to_sym]
raise ArgumentError, "Unknown strategy: #{type}" unless strategy_class_name
Object.const_get(strategy_class_name).new
end
end
strategy = NotificationStrategyRegistry.for(user.notification_preference)
sender = NotificationSender.new(strategy: strategy)
sender.deliver(recipient: user, message: notification_message)
3. Export Formats
class ExportStrategy
def export(data:)
raise NotImplementedError
end
def content_type
raise NotImplementedError
end
end
class CsvStrategy < ExportStrategy
def export(data:)
CSV.generate(headers: true) do |csv|
csv << data.first.keys
data.each { |row| csv << row.values }
end
end
def content_type
'text/csv'
end
end
class JsonStrategy < ExportStrategy
def export(data:)
data.to_json
end
def content_type
'application/json'
end
end
class PdfStrategy < ExportStrategy
def export(data:)
Prawn::Document.new do |pdf|
end.render
end
def content_type
'application/pdf'
end
end
class DataExporter
def initialize(strategy:)
@strategy = strategy
end
def export(data:)
{
content: @strategy.export(data: data),
content_type: @strategy.content_type
}
end
end
class ExportStrategyRegistry
STRATEGIES = {
csv: 'CsvStrategy',
json: 'JsonStrategy',
pdf: 'PdfStrategy'
}.freeze
def self.for(format)
strategy_class_name = STRATEGIES[format.to_sym]
raise ArgumentError, "Unknown format: #{format}" unless strategy_class_name
Object.const_get(strategy_class_name).new
end
end
strategy = ExportStrategyRegistry.for(params[:format])
exporter = DataExporter.new(strategy: strategy)
result = exporter.export(data: @records)
send_data result[:content], type: result[:content_type]
4. Authentication Methods
class PasswordStrategy < AuthStrategy
def authenticate(credentials:)
user = User.find_by(email: credentials[:email])
user&.authenticate(credentials[:password]) ? { success: true, user: user } : { success: false }
end
end
class OauthStrategy < AuthStrategy
def authenticate(credentials:)
end
end
class TokenStrategy < AuthStrategy
def authenticate(credentials:)
end
end
class SsoStrategy < AuthStrategy
def authenticate(credentials:)
end
end
Implementation Guidelines
1. Define Clear Strategy Interface
module Payments
class PaymentStrategy
def charge(amount:, payment_details:)
raise NotImplementedError
end
def refund(transaction_id:, amount:)
raise NotImplementedError
end
end
end
2. Use Strategy Registry (Open/Closed Principle)
Avoid case statements - Use a registry with metaprogramming to select strategies:
strategy = case user.preference
when 'email' then EmailStrategy.new
when 'sms' then SmsStrategy.new
end
class NotificationStrategyRegistry
STRATEGIES = {
email: 'EmailStrategy',
sms: 'SmsStrategy',
push: 'PushStrategy'
}.freeze
def self.for(type)
strategy_class_name = STRATEGIES[type.to_sym]
raise ArgumentError, "Unknown strategy: #{type}" unless strategy_class_name
Object.const_get(strategy_class_name).new
end
end
strategy = NotificationStrategyRegistry.for(user.preference)
sender = NotificationSender.new(strategy: strategy)
Benefits:
- ✅ Open/Closed: Add strategies via configuration, not code modification
- ✅ Testable: Easy to stub STRATEGIES for testing
- ✅ Configuration-driven: Can load from config/initializers
- ✅ Type-safe: Centralized validation of strategy types
3. Keep Strategies Stateless
class BadStrategy
attr_accessor :last_result
def execute
@last_result = do_something
end
end
class GoodStrategy
def execute
do_something
end
end
4. Make Strategies Independent
class BadEmailStrategy
def deliver(recipient:, message:)
if email_fails
SmsStrategy.new.deliver(recipient, message)
end
end
end
class NotificationSender
def initialize(primary:, fallback: nil)
@primary = primary
@fallback = fallback
end
def deliver(recipient:, message:)
result = @primary.deliver(recipient: recipient, message: message)
@fallback.deliver(recipient: recipient, message: message) if !result[:success] && @fallback
end
end
5. Don't Put Business Logic in Strategies
class BadPaymentStrategy
def charge(amount:, details:)
fee = amount * 0.029 + 0.30
total = amount + fee
validate_business_rules!(total)
process_charge(total, details)
end
end
class PaymentProcessor
def charge(amount:, details:)
fee = calculate_fee(amount)
total = amount + fee
validate_amount!(total)
@strategy.charge(amount: total, details: details)
end
end
Testing Strategies
RSpec.shared_examples 'a payment strategy' do
it 'responds to charge' do
expect(subject).to respond_to(:charge)
end
it 'responds to refund' do
expect(subject).to respond_to(:refund)
end
it 'returns success hash on charge' do
result = subject.charge(amount: 100, payment_details: valid_details)
expect(result).to have_key(:success)
expect(result).to have_key(:transaction_id)
end
end
RSpec.describe StripeStrategy do
it_behaves_like 'a payment strategy'
describe '#charge' do
it 'successfully charges card' do
result = subject.charge(amount: 100, payment_details: valid_details)
expect(result[:success]).to be true
end
end
end
RSpec.describe PaymentProcessor do
let(:strategy) { instance_double(PaymentStrategy) }
subject { described_class.new(strategy: strategy) }
it 'delegates to strategy' do
expect(strategy).to receive(:charge).with(amount: 100, payment_details: anything)
subject.charge(amount: 100, details: {})
end
end
Decision Tree
When to use Strategy vs other patterns:
Need multiple algorithms that are interchangeable?
→ YES: Use Strategy Pattern
→ NO: Keep reading
Need undo/redo functionality?
→ YES: Use Command Pattern
→ NO: Keep reading
Algorithm needs to know object's internal state?
→ YES: Use State Pattern
→ NO: Keep reading
Need algorithm skeleton with customizable steps?
→ YES: Use Template Method Pattern
→ NO: Keep reading
Simple conditional with 2 options?
→ YES: Use simple if/else or ternary
→ NO: Consider Strategy
Benefits
✅ Runtime flexibility - Swap algorithms at runtime
✅ Eliminates conditionals - No large case/if statements
✅ Open/Closed Principle - Add strategies without modifying context
✅ Single Responsibility - Each strategy handles one algorithm
✅ Easy to test - Test strategies independently
Drawbacks
❌ Increased complexity - More classes to maintain
❌ Client must know strategies - Client needs to choose appropriate strategy
❌ Overkill for simple cases - 2-3 variants might not justify pattern
Real-World Rails Examples
Shipping Calculators
class UpsStrategy < ShippingStrategy
def calculate(package:, destination:)
end
end
class FedexStrategy < ShippingStrategy
def calculate(package:, destination:)
end
end
class UspsStrategy < ShippingStrategy
def calculate(package:, destination:)
end
end
calculator = ShippingCalculator.new(strategy: UpsStrategy.new)
cost = calculator.calculate(package: package, destination: address)
Tax Calculators
class UsTaxStrategy < TaxStrategy
def calculate(amount:, state:)
end
end
class EuTaxStrategy < TaxStrategy
def calculate(amount:, country:)
end
end
class InternationalTaxStrategy < TaxStrategy
def calculate(amount:, country:)
end
end
Search Engines
class ElasticsearchStrategy < SearchStrategy
def search(query:, filters:)
end
end
class PostgresFullTextStrategy < SearchStrategy
def search(query:, filters:)
end
end
class AlgoliaStrategy < SearchStrategy
def search(query:, filters:)
end
end
Summary
Use Strategy Pattern when:
- You have 3+ interchangeable algorithms
- You need to swap behavior at runtime
- You want to eliminate large conditional statements
- Algorithms are independent and don't need object state
Avoid Strategy Pattern when:
- You have only 1-2 variants (overkill)
- Algorithm needs object state (use State instead)
- You need undo/redo (use Command instead)
- You need algorithm skeleton (use Template Method instead)
Most common Rails use cases:
- Payment gateways (Stripe, PayPal, etc.)
- Notification channels (Email, SMS, Push)
- Export formats (CSV, JSON, PDF)
- Authentication methods (Password, OAuth, Token, SSO)
- Shipping calculators (UPS, FedEx, USPS)
- Tax calculators (US, EU, International)
- Search engines (Elasticsearch, PostgreSQL, Algolia)