CrackedRuby CrackedRuby

Overview

The Facade Pattern offers a unified, higher-level interface that makes a subsystem easier to use. The pattern addresses the problem of complex systems with many interdependent classes, intricate initialization sequences, or convoluted APIs that burden client code with unnecessary details.

The pattern originates from the Gang of Four design patterns catalog, classified as a structural pattern. The name derives from architectural facades that present a simple exterior while hiding complex internal structure. In software, a facade object wraps multiple subsystem components, exposing only the operations clients need while concealing implementation complexity.

The pattern proves valuable when systems grow organically, accumulating layers of functionality that create tangled dependencies. Without a facade, client code must understand numerous classes, their relationships, and correct invocation sequences. This coupling makes the codebase fragile—changes to subsystem internals ripple through all clients.

Consider a home theater system with components like a DVD player, amplifier, projector, screen, and lights. Direct interaction requires:

dvd_player.on
dvd_player.play(movie)
amplifier.on
amplifier.set_dvd(dvd_player)
amplifier.set_volume(5)
projector.on
projector.wide_screen_mode
screen.down
lights.dim(10)

A facade reduces this to:

home_theater.watch_movie(movie)

The facade encapsulates subsystem knowledge, presenting a single method that coordinates all necessary components. Clients remain unaware of individual components or their interaction protocols.

The pattern differs from complete encapsulation. Clients can still access subsystem classes directly if needed, providing flexibility for advanced use cases while maintaining simplicity for common scenarios.

Key Principles

The Facade Pattern operates on several fundamental principles that define its structure and behavior.

Simplified Interface: The facade provides a reduced set of operations tailored to client needs. Rather than exposing every subsystem method, it offers targeted functionality that handles common use cases. This principle acknowledges that most clients use only a fraction of available features, so the facade surfaces the frequently-needed subset.

Subsystem Decoupling: Clients depend on the facade interface rather than concrete subsystem classes. This indirection creates a buffer between client code and implementation details. When subsystems change—different classes, modified methods, altered initialization—only the facade requires updates. Client code remains stable.

Delegation and Coordination: The facade contains no business logic of its own. It translates high-level operations into appropriate subsystem calls, handling sequencing, parameter mapping, and error coordination. The facade serves as an intelligent intermediary that understands how subsystem components collaborate.

Optional Access: The pattern maintains subsystem accessibility. Clients can bypass the facade when they require fine-grained control or advanced features not exposed through the simplified interface. This flexibility prevents the facade from becoming a bottleneck while still providing convenience for common cases.

Single Responsibility: Each facade addresses a specific domain or workflow. Rather than creating one massive facade for an entire system, multiple focused facades handle distinct areas. A payment facade manages transactions, an authentication facade handles security, and a reporting facade coordinates data generation. This partitioning maintains manageable scope and clear purpose.

The pattern creates a layered architecture:

Client Layer
Facade Layer (simplified interface)
Subsystem Layer (complex implementation)

Clients interact primarily with the facade layer, which translates requests into subsystem operations. The subsystem layer remains hidden behind the facade abstraction, allowing internal evolution without external disruption.

The facade instantiates or receives references to subsystem objects during initialization. It stores these references and uses them to fulfill client requests:

class OrderFacade
  def initialize
    @inventory = InventorySystem.new
    @payment = PaymentProcessor.new
    @shipping = ShippingService.new
    @notification = NotificationService.new
  end
  
  def place_order(items, customer, payment_details)
    @inventory.reserve(items)
    transaction = @payment.process(payment_details)
    shipment = @shipping.schedule(items, customer.address)
    @notification.send_confirmation(customer, transaction, shipment)
  end
end

The OrderFacade coordinates four separate subsystems through a single method. Clients call place_order without managing inventory, payment, shipping, or notification complexities directly.

Ruby Implementation

Ruby's dynamic nature and flexible object model support clean facade implementations. The language features—blocks, keyword arguments, method delegation—enable expressive facade code.

A basic facade structure in Ruby:

class DatabaseFacade
  def initialize(config)
    @connection = DatabaseConnection.new(config)
    @query_builder = QueryBuilder.new
    @result_parser = ResultParser.new
    @cache = CacheLayer.new
  end
  
  def find_user(id)
    cache_key = "user:#{id}"
    
    @cache.fetch(cache_key) do
      query = @query_builder.select(:users).where(id: id).build
      raw_result = @connection.execute(query)
      @result_parser.parse_single(raw_result)
    end
  end
  
  def search_users(criteria)
    query = @query_builder.select(:users)
                          .where(criteria)
                          .order(:created_at)
                          .build
    raw_results = @connection.execute(query)
    @result_parser.parse_collection(raw_results)
  end
end

The facade hides database connection management, query construction, result parsing, and caching. Clients interact with simple methods that return Ruby objects.

Ruby modules provide an alternative implementation using composition:

module EmailFacade
  extend self
  
  def send_welcome(user)
    template = TemplateEngine.render('welcome', user: user)
    formatted = HtmlFormatter.format(template)
    EmailService.deliver(
      to: user.email,
      subject: 'Welcome!',
      body: formatted
    )
    AnalyticsTracker.track('email_sent', user.id, 'welcome')
  end
  
  def send_reset_password(user)
    token = TokenGenerator.generate_secure_token
    PasswordReset.create(user: user, token: token)
    template = TemplateEngine.render('reset_password', 
                                     user: user, 
                                     token: token)
    formatted = HtmlFormatter.format(template)
    EmailService.deliver(
      to: user.email,
      subject: 'Password Reset',
      body: formatted
    )
    AnalyticsTracker.track('email_sent', user.id, 'reset_password')
  end
end

The module approach works well for stateless facades that don't require instance variables. The extend self idiom enables calling methods directly on the module.

Ruby's method_missing can create dynamic facades:

class ApiFacade
  def initialize(client)
    @client = client
    @serializer = JsonSerializer.new
    @deserializer = JsonDeserializer.new
    @error_handler = ApiErrorHandler.new
  end
  
  def method_missing(method, *args)
    endpoint = method.to_s
    payload = @serializer.serialize(args.first || {})
    
    begin
      response = @client.post(endpoint, payload)
      @deserializer.deserialize(response.body)
    rescue ApiError => e
      @error_handler.handle(e)
    end
  end
  
  def respond_to_missing?(method, include_private = false)
    true
  end
end

# Usage
api = ApiFacade.new(HttpClient.new('https://api.example.com'))
api.create_user(name: 'Alice', email: 'alice@example.com')
api.update_profile(id: 123, bio: 'Developer')

This dynamic approach automatically handles any method call as an API request, applying consistent serialization, error handling, and deserialization.

Ruby's SimpleDelegator offers built-in delegation support:

require 'delegate'

class LoggingFacade < SimpleDelegator
  def initialize(service, logger)
    super(service)
    @logger = logger
  end
  
  def method_missing(method, *args, &block)
    @logger.info("Calling #{method} with #{args.inspect}")
    result = super
    @logger.info("#{method} returned #{result.inspect}")
    result
  rescue => e
    @logger.error("#{method} failed: #{e.message}")
    raise
  end
end

payment_service = PaymentProcessor.new
logged_service = LoggingFacade.new(payment_service, Logger.new(STDOUT))
logged_service.process_payment(amount: 100)
# Logs: Calling process_payment with [{:amount=>100}]
# Logs: process_payment returned #<Transaction:...>

The SimpleDelegator forwards all methods to the wrapped object while intercepting calls for logging.

Facades often use dependency injection for flexibility:

class ReportFacade
  def initialize(data_source:, formatter:, exporter:)
    @data_source = data_source
    @formatter = formatter
    @exporter = exporter
  end
  
  def generate_monthly_report(month)
    raw_data = @data_source.fetch_data(month)
    formatted = @formatter.format(raw_data)
    @exporter.export(formatted, "report_#{month}.pdf")
  end
end

# Different implementations can be injected
facade = ReportFacade.new(
  data_source: DatabaseSource.new,
  formatter: PdfFormatter.new,
  exporter: FileExporter.new
)

# Or test doubles
test_facade = ReportFacade.new(
  data_source: MockDataSource.new,
  formatter: MockFormatter.new,
  exporter: MockExporter.new
)

This approach makes facades testable and configurable without modifying the facade class.

Practical Examples

The Facade Pattern applies to numerous real-world scenarios where complexity reduction improves code quality.

File Processing System: Applications that handle file uploads often require virus scanning, format validation, storage, thumbnail generation, and metadata extraction:

class FileUploadFacade
  def initialize
    @validator = FileValidator.new
    @scanner = VirusScanner.new
    @storage = S3Storage.new
    @thumbnail = ThumbnailGenerator.new
    @metadata = MetadataExtractor.new
  end
  
  def upload(file, user)
    # Validation
    raise InvalidFileError unless @validator.valid_format?(file)
    raise FileSizeError if @validator.too_large?(file)
    
    # Security
    raise VirusDetectedError if @scanner.infected?(file)
    
    # Storage
    storage_key = @storage.save(file, folder: "uploads/#{user.id}")
    
    # Processing
    if @validator.image?(file)
      thumbnail_key = @thumbnail.generate(file, size: '200x200')
    end
    
    metadata = @metadata.extract(file)
    
    # Create record
    Upload.create(
      user: user,
      storage_key: storage_key,
      thumbnail_key: thumbnail_key,
      metadata: metadata,
      uploaded_at: Time.now
    )
  end
end

# Client code remains simple
facade = FileUploadFacade.new
facade.upload(uploaded_file, current_user)

Without the facade, controllers or services would need to coordinate all these steps, leading to duplication and inconsistency across different upload scenarios.

Third-Party API Integration: Applications that consume external APIs benefit from facades that handle authentication, rate limiting, error handling, and response parsing:

class WeatherApiFacade
  API_ENDPOINT = 'https://api.weather.com/v3'
  
  def initialize(api_key)
    @api_key = api_key
    @http_client = HttpClient.new(base_url: API_ENDPOINT)
    @rate_limiter = RateLimiter.new(max_requests: 100, period: 3600)
    @cache = RedisCache.new(ttl: 1800)
    @parser = WeatherDataParser.new
  end
  
  def current_weather(location)
    cache_key = "weather:current:#{location}"
    
    @cache.fetch(cache_key) do
      @rate_limiter.wait_if_needed
      
      response = @http_client.get('/current', {
        location: location,
        apiKey: @api_key,
        units: 'metric'
      })
      
      handle_api_errors(response)
      @parser.parse_current(response.body)
    end
  end
  
  def forecast(location, days: 5)
    cache_key = "weather:forecast:#{location}:#{days}"
    
    @cache.fetch(cache_key) do
      @rate_limiter.wait_if_needed
      
      response = @http_client.get('/forecast', {
        location: location,
        days: days,
        apiKey: @api_key,
        units: 'metric'
      })
      
      handle_api_errors(response)
      @parser.parse_forecast(response.body)
    end
  end
  
  private
  
  def handle_api_errors(response)
    case response.status
    when 401
      raise AuthenticationError, 'Invalid API key'
    when 429
      raise RateLimitError, 'Rate limit exceeded'
    when 500..599
      raise ServiceError, 'Weather service unavailable'
    end
  end
end

# Usage
weather = WeatherApiFacade.new(ENV['WEATHER_API_KEY'])
current = weather.current_weather('New York')
forecast = weather.forecast('London', days: 7)

The facade isolates clients from API details—authentication headers, error codes, response formats, rate limiting. Changes to the API or caching strategy require updates only within the facade.

Database Transaction Management: Complex transactions involving multiple tables, validations, and side effects benefit from facade coordination:

class OrderProcessingFacade
  def initialize
    @db = DatabaseConnection.new
    @inventory = InventoryManager.new
    @payment = PaymentGateway.new
    @shipping = ShippingCalculator.new
    @email = EmailNotifier.new
  end
  
  def create_order(cart, customer, payment_info)
    @db.transaction do
      # Check inventory
      cart.items.each do |item|
        available = @inventory.check_availability(item.product_id, item.quantity)
        raise InsufficientInventoryError unless available
      end
      
      # Calculate totals
      subtotal = cart.calculate_subtotal
      shipping_cost = @shipping.calculate(cart, customer.address)
      total = subtotal + shipping_cost
      
      # Process payment
      transaction = @payment.charge(payment_info, total)
      raise PaymentFailedError unless transaction.success?
      
      # Create order record
      order = Order.create!(
        customer: customer,
        subtotal: subtotal,
        shipping_cost: shipping_cost,
        total: total,
        payment_transaction_id: transaction.id,
        status: 'pending'
      )
      
      # Create order items
      cart.items.each do |item|
        OrderItem.create!(
          order: order,
          product_id: item.product_id,
          quantity: item.quantity,
          price: item.price
        )
      end
      
      # Update inventory
      cart.items.each do |item|
        @inventory.decrement(item.product_id, item.quantity)
      end
      
      # Send confirmation
      @email.send_order_confirmation(customer, order)
      
      order
    end
  rescue => e
    @payment.refund(transaction.id) if transaction
    raise
  end
end

The facade ensures all steps complete successfully or none do, maintaining data consistency. Error handling and rollback logic remain centralized rather than scattered across controllers.

Design Considerations

Selecting when to apply the Facade Pattern requires understanding its benefits and limitations within specific architectural contexts.

When to Use Facades: The pattern fits scenarios where subsystem complexity burdens clients. Multiple classes with intricate relationships, lengthy initialization sequences, or APIs with numerous configuration options signal facade opportunities. Systems with high coupling between clients and implementation details benefit from the decoupling a facade provides.

Applications that consume external services or libraries should consider facades at integration boundaries. The facade translates between external APIs and internal models, isolating the application from third-party changes. When a library releases breaking changes, updates concentrate in the facade rather than spreading throughout the codebase.

Teams working on large systems with distinct modules or microservices use facades to define clear boundaries. Each module exposes a facade that represents its public API, hiding internal complexity. This approach supports independent module evolution—internal refactoring doesn't affect clients using the facade.

When to Avoid Facades: Simple subsystems with few classes and clear APIs rarely justify facades. The pattern adds abstraction layers that complicate rather than simplify when the underlying system already provides good ergonomics. Over-application creates unnecessary indirection that obscures direct class relationships.

Systems where clients need fine-grained control or access to advanced features may find facades restrictive. If most use cases require bypassing the facade to access subsystem classes directly, the abstraction provides little value. The pattern works best when a simplified interface covers the majority of client needs.

Facades that become dumping grounds for unrelated functionality violate single responsibility principles. A facade that handles authentication, logging, caching, validation, and business logic transforms into a god object that's difficult to test and maintain. Multiple focused facades prove more maintainable than one massive facade.

Trade-offs: Facades introduce indirection that can impact performance. Each facade method call delegates to subsystem methods, adding invocation overhead. For performance-critical paths, direct subsystem access may prove necessary. Profiling helps identify whether facade overhead matters for specific use cases.

The pattern creates an additional maintenance burden. Changes to subsystem interfaces require corresponding facade updates. When subsystems evolve—new parameters, different return types, additional methods—the facade must adapt. This maintenance tax justifies itself only when facade benefits outweigh the costs.

Facades can hide necessary complexity that clients should understand. Security configurations, transaction boundaries, or resource management decisions shouldn't be completely opaque. The facade should make common cases simple while still exposing important details through clear documentation or optional parameters.

Comparison with Related Patterns: The Adapter Pattern and Facade Pattern both wrap existing code, but serve different purposes. Adapters convert one interface to another, often to make incompatible interfaces work together. Facades simplify complex subsystems without interface conversion. An adapter implements a specific target interface, while a facade defines its own simplified interface.

The Mediator Pattern centralizes communication between objects, similar to facade coordination. However, mediators reduce coupling between peer objects that communicate bidirectionally, while facades reduce coupling between clients and subsystems through unidirectional calls. Mediators manage object interaction protocols; facades manage subsystem complexity.

The Proxy Pattern controls access to objects, adding functionality like lazy loading or access control. Facades simplify complex subsystems. A proxy maintains the same interface as the wrapped object; a facade provides a different, simpler interface. Proxies focus on access control; facades focus on usability.

Common Patterns

The Facade Pattern manifests in several variations that address specific architectural needs.

Layered Facades: Complex systems benefit from multiple facade layers, each simplifying the layer below:

# Low-level facade
class DatabaseFacade
  def execute_query(sql, params)
    connection = ConnectionPool.acquire
    statement = connection.prepare(sql)
    statement.execute(params)
  ensure
    ConnectionPool.release(connection)
  end
end

# Mid-level facade
class RepositoryFacade
  def initialize
    @db = DatabaseFacade.new
  end
  
  def find_user(id)
    result = @db.execute_query(
      'SELECT * FROM users WHERE id = ?',
      [id]
    )
    User.from_hash(result.first)
  end
end

# High-level facade
class UserServiceFacade
  def initialize
    @repository = RepositoryFacade.new
    @validator = UserValidator.new
    @logger = AuditLogger.new
  end
  
  def get_user(id)
    @validator.validate_id(id)
    user = @repository.find_user(id)
    @logger.log_access(id)
    user
  end
end

Each layer provides appropriate abstraction levels. Clients choose the layer matching their needs—direct database access, repository operations, or full service logic.

Transparent Facades: Some facades maintain the same interface as underlying objects while adding cross-cutting concerns:

class CachingFacade
  def initialize(service)
    @service = service
    @cache = MemoryCache.new
  end
  
  def method_missing(method, *args)
    cache_key = "#{method}:#{args.hash}"
    
    @cache.fetch(cache_key) do
      @service.public_send(method, *args)
    end
  end
  
  def respond_to_missing?(method, include_private = false)
    @service.respond_to?(method, include_private)
  end
end

# Usage remains unchanged
original_service = DataService.new
cached_service = CachingFacade.new(original_service)
cached_service.fetch_data(params) # Cached transparently

This variation adds functionality without changing how clients interact with the service.

Configuration Facades: Applications with complex configuration benefit from facades that hide setup details:

class ApplicationFacade
  def self.configure
    config = yield Configuration.new if block_given?
    
    setup_database(config.database)
    setup_logging(config.logging)
    setup_cache(config.cache)
    setup_middleware(config.middleware)
    
    new(config)
  end
  
  private_class_method def self.setup_database(db_config)
    DatabaseConnection.establish(
      adapter: db_config.adapter,
      host: db_config.host,
      pool: db_config.pool_size
    )
  end
  
  private_class_method def self.setup_logging(log_config)
    Logger.configure do |logger|
      logger.level = log_config.level
      logger.output = log_config.output
      logger.formatter = log_config.formatter
    end
  end
  
  # Additional setup methods...
end

# Configuration
app = ApplicationFacade.configure do |config|
  config.database.adapter = 'postgresql'
  config.logging.level = :info
  config.cache.enabled = true
end

The facade coordinates initialization across multiple subsystems, presenting a unified configuration interface.

Batch Operation Facades: Systems that perform multiple related operations benefit from facades that batch requests:

class NotificationFacade
  def initialize
    @email_service = EmailService.new
    @sms_service = SmsService.new
    @push_service = PushNotificationService.new
  end
  
  def notify_users(users, message)
    email_recipients = users.select(&:email_enabled)
    sms_recipients = users.select(&:sms_enabled)
    push_recipients = users.select(&:push_enabled)
    
    results = {}
    
    unless email_recipients.empty?
      results[:email] = @email_service.send_batch(
        email_recipients.map(&:email),
        message
      )
    end
    
    unless sms_recipients.empty?
      results[:sms] = @sms_service.send_batch(
        sms_recipients.map(&:phone),
        message
      )
    end
    
    unless push_recipients.empty?
      results[:push] = @push_service.send_batch(
        push_recipients.map(&:device_token),
        message
      )
    end
    
    results
  end
end

The facade handles batching logic, selecting appropriate services based on user preferences and coordinating parallel operations.

Common Pitfalls

Developers encounter recurring issues when implementing facades.

God Object Facades: Facades that accumulate excessive responsibility become maintenance nightmares:

# Anti-pattern: Overloaded facade
class ApplicationFacade
  def initialize
    # Too many dependencies
    @db = Database.new
    @cache = Cache.new
    @logger = Logger.new
    @mailer = Mailer.new
    @validator = Validator.new
    @authenticator = Authenticator.new
    @authorizer = Authorizer.new
    @payment = PaymentProcessor.new
    @analytics = Analytics.new
  end
  
  # Unrelated methods
  def send_email(user, subject, body); end
  def validate_input(data); end
  def log_event(event); end
  def process_payment(amount); end
  def authenticate_user(credentials); end
  # ... dozens more methods
end

This facade violates single responsibility by handling authentication, validation, payments, and notifications. The solution divides functionality into domain-specific facades:

# Better: Focused facades
class AuthenticationFacade
  def initialize
    @authenticator = Authenticator.new
    @authorizer = Authorizer.new
  end
  
  def authenticate(credentials)
    @authenticator.verify(credentials)
  end
  
  def authorize(user, resource)
    @authorizer.check_permission(user, resource)
  end
end

class PaymentFacade
  def initialize
    @processor = PaymentProcessor.new
    @validator = PaymentValidator.new
  end
  
  def process(amount, method)
    @validator.validate(amount, method)
    @processor.charge(amount, method)
  end
end

Each facade addresses a cohesive set of related operations.

Leaky Abstractions: Facades that expose subsystem details undermine encapsulation:

# Anti-pattern: Leaky facade
class DataFacade
  def find_user(id)
    # Returns subsystem object directly
    @database.connection.execute("SELECT * FROM users WHERE id = #{id}").first
  end
end

# Client code becomes coupled to database implementation
user_row = facade.find_user(123)
name = user_row['name'] # Depends on database column names

Clients must understand database result formats, coupling them to the subsystem. The facade should return domain objects:

# Better: Proper abstraction
class DataFacade
  def find_user(id)
    row = @database.execute_query('SELECT * FROM users WHERE id = ?', [id]).first
    return nil unless row
    
    User.new(
      id: row['id'],
      name: row['name'],
      email: row['email']
    )
  end
end

# Client code works with domain objects
user = facade.find_user(123)
name = user.name # Domain interface, not database columns

Insufficient Error Handling: Facades that propagate subsystem exceptions directly force clients to understand implementation details:

# Anti-pattern: Raw exception propagation
class StorageFacade
  def save_file(file, key)
    @s3_client.put_object(bucket: @bucket, key: key, body: file)
    # Raises AWS::S3::Errors::NoSuchBucket, AWS::S3::Errors::AccessDenied, etc.
  end
end

# Client must handle specific S3 exceptions
begin
  facade.save_file(data, 'document.pdf')
rescue AWS::S3::Errors::NoSuchBucket => e
  # Coupled to S3 implementation
end

Facades should translate subsystem exceptions into domain exceptions:

# Better: Domain exceptions
class StorageFacade
  class StorageError < StandardError; end
  class BucketNotFoundError < StorageError; end
  class AccessDeniedError < StorageError; end
  class StorageFullError < StorageError; end
  
  def save_file(file, key)
    @s3_client.put_object(bucket: @bucket, key: key, body: file)
  rescue AWS::S3::Errors::NoSuchBucket => e
    raise BucketNotFoundError, "Storage bucket not found"
  rescue AWS::S3::Errors::AccessDenied => e
    raise AccessDeniedError, "Permission denied for storage operation"
  rescue AWS::S3::Errors::EntityTooLarge => e
    raise StorageFullError, "File exceeds storage limits"
  end
end

# Client handles domain exceptions
begin
  facade.save_file(data, 'document.pdf')
rescue StorageFacade::StorageError => e
  # Domain-level error handling
end

Over-Simplification: Facades that hide critical details can lead to incorrect usage:

# Anti-pattern: Hidden complexity
class TransactionFacade
  def transfer_funds(from_account, to_account, amount)
    # Transaction boundaries hidden from client
    Database.transaction do
      from_account.withdraw(amount)
      to_account.deposit(amount)
    end
  end
end

# Client doesn't realize this creates a transaction
facade.transfer_funds(account1, account2, 100)
facade.transfer_funds(account2, account3, 50) # Separate transaction!

If clients need transaction control, the facade should expose it:

# Better: Expose important details
class TransactionFacade
  def transfer_funds(from_account, to_account, amount, transaction: nil)
    operation = -> {
      from_account.withdraw(amount)
      to_account.deposit(amount)
    }
    
    if transaction
      operation.call
    else
      Database.transaction(&operation)
    end
  end
  
  def with_transaction
    Database.transaction { yield }
  end
end

# Client controls transaction scope
facade.with_transaction do
  facade.transfer_funds(account1, account2, 100, transaction: true)
  facade.transfer_funds(account2, account3, 50, transaction: true)
end

Testing Challenges: Tightly coupled facades complicate testing:

# Anti-pattern: Hard-to-test facade
class OrderFacade
  def initialize
    # Hard-coded dependencies
    @db = ProductionDatabase.new
    @payment = StripePaymentProcessor.new(ENV['STRIPE_KEY'])
    @mailer = SendGridMailer.new(ENV['SENDGRID_KEY'])
  end
end

# Tests hit real external services

Dependency injection enables testing with mocks:

# Better: Testable facade
class OrderFacade
  def initialize(database: ProductionDatabase.new,
                 payment: StripePaymentProcessor.new(ENV['STRIPE_KEY']),
                 mailer: SendGridMailer.new(ENV['SENDGRID_KEY']))
    @db = database
    @payment = payment
    @mailer = mailer
  end
end

# Tests use test doubles
facade = OrderFacade.new(
  database: MockDatabase.new,
  payment: MockPaymentProcessor.new,
  mailer: MockMailer.new
)

Reference

Pattern Components

Component Description Responsibility
Facade Simplified interface class Coordinates subsystem operations and translates client requests
Subsystem Classes Complex implementation classes Perform actual work and contain business logic
Client Code using the facade Initiates requests through facade interface

When to Apply

Scenario Use Facade Skip Facade
Multiple interdependent classes Yes No - simple single class
Complex initialization sequences Yes No - straightforward setup
External API integration Yes No - API already simple
Need occasional direct access Yes No - always need full control
Changing subsystem implementation Yes No - stable implementation
Multiple clients with similar needs Yes No - single specialized client

Implementation Checklist

Step Action Consideration
1 Identify complex subsystem Look for multiple related classes
2 Define simplified operations Focus on common client use cases
3 Create facade class Single responsibility for one domain
4 Initialize subsystem objects Use dependency injection when possible
5 Implement coordination methods Delegate to subsystem, handle sequencing
6 Add error translation Convert subsystem exceptions to domain exceptions
7 Document direct access Explain when to bypass facade
8 Write tests Use mocks for subsystem dependencies

Design Comparison

Pattern Purpose Interface Directionality
Facade Simplify complexity New simplified interface Client to subsystem
Adapter Convert interfaces Implements target interface Bidirectional possible
Proxy Control access Same as wrapped object Client to target
Mediator Reduce coupling Central communication hub Between peers

Common Methods

Operation Signature Example Purpose
Initialize def initialize(dependencies) Set up subsystem references
High-level operation def process_order(cart, customer) Coordinate multiple subsystems
Batch operation def notify_users(users, message) Handle multiple items efficiently
Configuration def configure(&block) Set up subsystem components
Transaction wrapper def with_transaction(&block) Provide transaction scope

Anti-Pattern Indicators

Symptom Problem Solution
Facade has 20+ methods God object Split into domain-specific facades
Methods return subsystem types Leaky abstraction Return domain objects instead
Clients catch subsystem exceptions Poor encapsulation Translate to domain exceptions
Hard to test Tight coupling Use dependency injection
Facade contains business logic Wrong responsibility Move logic to subsystem
All clients bypass facade Over-simplified interface Expand facade API or remove it