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 |