Skip to main content

Facade Pattern

Provide a unified interface to a complex subsystem—simplify integration and reduce coupling

TL;DR

Facade provides a single, simplified interface to a complex subsystem. Instead of clients navigating 20 classes and 100 methods, Facade exposes 5 simple methods that handle the complexity internally. Perfect for wrapping legacy systems, integrating third-party libraries, or decoupling clients from implementation details.

Learning Objectives

  • You will be able to identify opportunities to create Facades for complex subsystems.
  • You will be able to design a Facade that simplifies without sacrificing necessary control.
  • You will be able to distinguish Facade from Adapter and Decorator.
  • You will be able to balance convenience with flexibility in Facade design.

Motivating Scenario

You're integrating a PDF library with 50 classes, complex builder patterns, and non-intuitive method names. Clients need: "create PDF", "add text", "save to file". Rather than making every client learn the library's API, Facade wraps it: pdf_facade.create_document().add_text("Hello").save("/tmp/out.pdf").

Core Concepts

Facade provides a unified, simplified interface to a complex subsystem. It doesn't add new functionality; it orchestrates existing complexity into a cleaner contract.

Key characteristics:

  • Subsystem classes remain unchanged: Facade wraps them, doesn't modify them
  • Simplification by composition: Facade delegates to subsystem classes
  • Decoupling: Clients depend on Facade, not on subsystem details
  • Optional direct access: Clients can still use subsystem directly if needed
Facade structure

Practical Example

# Subsystem: complex PDF library
class PDFDocument:
def __init__(self):
self.pages = []

def add_page(self):
self.pages.append({})
return len(self.pages) - 1

def add_text_to_page(self, page_id: int, text: str):
self.pages[page_id]["text"] = text

class PDFFormatter:
def format_text(self, text: str, font_size: int) -> str:
return f"<text size={font_size}>{text}</text>"

class PDFWriter:
def write_to_file(self, doc: PDFDocument, filename: str):
content = "".join(str(page) for page in doc.pages)
with open(filename, "w") as f:
f.write(content)

# Facade: simple interface to complex subsystem
class SimplePDFFacade:
def __init__(self):
self.doc = PDFDocument()
self.formatter = PDFFormatter()
self.writer = PDFWriter()
self.current_page = None

def create_document(self):
self.current_page = self.doc.add_page()
return self

def add_text(self, text: str, font_size: int = 12):
formatted = self.formatter.format_text(text, font_size)
self.doc.add_text_to_page(self.current_page, formatted)
return self

def new_page(self):
self.current_page = self.doc.add_page()
return self

def save(self, filename: str):
self.writer.write_to_file(self.doc, filename)

# Client: simple, clean usage
facade = SimplePDFFacade()
facade.create_document() \
.add_text("Hello PDF World", 16) \
.new_page() \
.add_text("Second page", 12) \
.save("/tmp/output.pdf")

When to Use / When NOT to Use

Use Facade when:
  1. Integrating complex subsystems with many interdependent classes
  2. You want to decouple clients from subsystem implementation details
  3. Providing a simpler entry point for common use cases
  4. Wrapping legacy systems with confusing or bloated APIs
  5. Organizing many related classes into a cohesive interface
Don't use Facade when:
  1. The subsystem is already simple (overhead without benefit)
  2. Hiding complexity that clients need direct access to
  3. Facade becomes as complex as the subsystem it wraps
  4. Fine-grained control over subsystem behavior is essential
  5. Different clients need different simplified views

Patterns and Pitfalls

Patterns and Pitfalls

Hide subsystem classes, expose only the Facade.
If Facade assumes too much, it limits flexibility.
Allow direct subsystem access when Facade is too limiting.

Design Review Checklist

  • Facade simplifies the subsystem API without limiting necessary functionality
  • Subsystem classes are not modified by Facade (wrapping, not inheritance)
  • Facade encapsulates subsystem dependencies and interactions
  • Common use cases have simple Facade methods
  • Advanced use cases can still access subsystem directly if needed
  • Documentation explains what the Facade hides and why
  • Facade doesn't add significant overhead to simple operations
  • Tests cover both simple Facade usage and advanced subsystem access

Advanced Facade Patterns

Facade Hierarchy for Complex Systems

# Fine-grained facades for different client types
class SimplePDFFacade:
"""For basic PDF creation"""
def create_document(self):
# ... simple setup
return self

def add_text(self, text):
# ... basic text
return self

class AdvancedPDFFacade:
"""For power users needing more control"""
def create_document(self):
# ... setup
return self

def add_text(self, text, **kwargs):
# ... advanced text with font, positioning, etc.
return self

def add_watermark(self, text):
# ... watermark support
return self

# Clients choose appropriate facade for their needs

Facade with Adapter for Legacy Integration

class LegacyPDFLibrary:
"""Old, complicated API"""
def initialize(self, config):
pass

def open_document(self, name):
pass

def allocate_page(self):
pass

def set_text_properties(self, font, size, color):
pass

def write_text(self, x, y, text):
pass

def close_document(self):
pass

class PDFAdapter:
"""Adapter to modern interface"""
def __init__(self, legacy_lib):
self.lib = legacy_lib
self.lib.initialize({})

def add_text(self, text):
self.lib.set_text_properties("Arial", 12, "black")
self.lib.write_text(10, 10, text)

class ModernPDFFacade:
"""Facade uses adapted legacy library"""
def __init__(self):
self.adapter = PDFAdapter(LegacyPDFLibrary())

def create_document(self):
self.adapter.lib.open_document("output.pdf")
return self

def add_text(self, text):
self.adapter.add_text(text)
return self

def save(self, filename):
self.adapter.lib.close_document()

Facade with Feature Flags for Gradual Migration

class PDFFacade:
def __init__(self, use_legacy=False):
self.use_legacy = use_legacy
if use_legacy:
self.impl = LegacyPDFImpl()
else:
self.impl = ModernPDFImpl()

def add_text(self, text):
if self.use_legacy:
return self.impl.legacy_add_text(text)
else:
return self.impl.modern_add_text(text)

# Feature flag controls which implementation to use
# Allows gradual migration from legacy to modern

Real-World Facade Examples

Database Facade

class DatabaseFacade:
"""Simplifies complex database connection pool, transaction, and query logic"""
def __init__(self, config):
self.pool = ConnectionPool(config)
self.transaction_manager = TransactionManager()

def execute_query(self, sql):
conn = self.pool.get_connection()
try:
result = conn.execute(sql)
return result
finally:
self.pool.return_connection(conn)

def run_transaction(self, operations):
txn = self.transaction_manager.begin()
try:
for op in operations:
op(txn)
txn.commit()
except Exception:
txn.rollback()
raise

Web Framework Facade

class WebFrameworkFacade:
"""Hides routing, middleware, auth, database, caching layers"""
def __init__(self):
self.router = Router()
self.auth = AuthManager()
self.db = DatabaseFacade({})
self.cache = CacheManager()

def register_route(self, path, handler, auth_required=False):
def wrapped_handler(request):
if auth_required and not self.auth.is_authenticated(request):
return {"error": "Unauthorized"}
return handler(request)
self.router.add_route(path, wrapped_handler)

def get_user(self, user_id):
# Check cache first
cached = self.cache.get(f"user:{user_id}")
if cached:
return cached
# Query database
user = self.db.execute_query(f"SELECT * FROM users WHERE id={user_id}")
# Cache result
self.cache.set(f"user:{user_id}", user, ttl=3600)
return user

Cloud Service Facade

class CloudProviderFacade:
"""Abstracts AWS, Azure, GCP differences"""
def __init__(self, provider="aws"):
if provider == "aws":
self.storage = S3Storage()
self.compute = EC2Compute()
self.database = RDSDatabase()
elif provider == "azure":
self.storage = AzureBlob()
self.compute = AzureVM()
self.database = AzureSQLDatabase()

def upload_file(self, filename, data):
return self.storage.put(filename, data)

def create_instance(self, instance_type):
return self.compute.create(instance_type)

def query_database(self, sql):
return self.database.execute(sql)

# Client doesn't care about specific cloud provider
# Facade handles differences transparently

Media Conversion Library Facade

# Complex subsystem with many interdependent classes
class AudioCodec:
def decode(self, data): pass
def encode(self, data): pass

class MP3Codec(AudioCodec):
def decode(self, data): return "decoded_mp3"
def encode(self, data): return "encoded_mp3"

class AACCodec(AudioCodec):
def decode(self, data): return "decoded_aac"
def encode(self, data): return "encoded_aac"

class Resampler:
def resample(self, data, target_rate):
return f"resampled_to_{target_rate}hz"

class EQ:
def apply(self, data, settings):
return "eq_applied"

class Compressor:
def compress(self, data):
return "compressed"

# Facade simplifies common operations
class AudioProcessingFacade:
def __init__(self):
self.decoders = {'mp3': MP3Codec(), 'aac': AACCodec()}
self.resampler = Resampler()
self.eq = EQ()
self.compressor = Compressor()

def convert_mp3_to_aac(self, mp3_data):
"""Convert MP3 to AAC with standard settings"""
# Hide complexity: decoding, resampling, EQ, compression
decoded = self.decoders['mp3'].decode(mp3_data)
resampled = self.resampler.resample(decoded, 44100)
equ = self.eq.apply(resampled, {'bass': 0, 'treble': 0})
compressed = self.compressor.compress(equ)
encoded = self.decoders['aac'].encode(compressed)
return encoded

def normalize_audio(self, audio_data, format='mp3'):
"""Normalize audio to standard format and quality"""
# Another common operation simplified
codec = self.decoders[format]
decoded = codec.decode(audio_data)
resampled = self.resampler.resample(decoded, 48000)
return resampled

# Client code is much simpler
facade = AudioProcessingFacade()
aac_file = facade.convert_mp3_to_aac(mp3_bytes)
normalized = facade.normalize_audio(audio_bytes)

Facade vs Adapter vs Decorator

PatternPurposeWhen to Use
FacadeSimplify complex subsystemWrapping multiple classes with complex interactions
AdapterConvert interface to anotherInterface mismatch between components
DecoratorAdd behavior to objectsDynamic behavior addition; optional features

Self-Check

  1. Identify: Find a complex subsystem that clients struggle to integrate. (Look for: many classes, complex interactions, non-intuitive method sequences)
  2. Design: Create a Facade with methods for common use cases. (Ask: what do most clients need to do? Make that simple)
  3. Verify: Client code is simpler and cleaner with the Facade. (Compare: with/without facade. Facade code should be 50-80% shorter)

Red flags you need a Facade:

  • Clients need to understand 10+ subsystem classes
  • Typical client code is 30+ lines for simple operation
  • Subsystem method names don't match client terminology
  • Clients frequently misuse the subsystem
info

One Takeaway: Facade simplifies integration with complex subsystems by providing a unified, easy-to-use interface. It doesn't add functionality—it orchestrates existing complexity. Always provide an escape hatch for advanced users who need subsystem details, but make the common path simple and obvious.

Next Steps

  • Learn Adapter for connecting incompatible interfaces (different intent).
  • Study Builder for constructing complex objects step-by-step.
  • Explore Mediator for managing complex interactions between objects.

References

  • Gang of Four: Design Patterns (Facade)
  • Head First Design Patterns (Facade chapter)
  • Martin Fowler: Refactoring (Hide Delegates)