DEV Community

Mahdi Shamlou | Structural Design Patterns 2026: Facade, Flyweight, Proxy — Production Examples

Mahdi Shamlou here.

Welcome back to the Structural Design Patterns series.

In Part 1, we explored:

  • Adapter — Making incompatible systems work together
  • Bridge — Decoupling abstraction from implementation
  • Composite — Building tree structures elegantly
  • Decorator — Adding behavior without modification

Mahdi Shamlou

Now it's time for the final three structural patterns that will complete your arsenal.

These patterns solve different but equally important problems:

  • Simplifying complex systems (Facade)
  • Saving memory with shared objects (Flyweight)
  • Controlling access to objects (Proxy)

Let's dive into the code.


Part 2: The Final Structural Patterns

5. Facade Pattern

The Facade pattern provides a unified, simplified interface to a set of interfaces in a subsystem.

It's about hiding complexity behind a simple interface.

Imagine using a movie theater. Behind the scenes:

  • The lights need to dim
  • The sound system needs to adjust
  • The projector needs to start
  • The curtains need to open
  • The audience needs to settle

Without Facade, the client would interact with all these systems directly:

lights.dim()
sound_system.set_volume(50)
projector.start()
curtains.open()
Enter fullscreen mode Exit fullscreen mode

With Facade, the client just calls:

theater.watch_movie()
Enter fullscreen mode Exit fullscreen mode

When to use it:

  • Simplifying complex subsystems
  • Decoupling clients from subsystem components
  • Creating a simple entry point to a complex system
  • Legacy system integration
  • Providing a versioning layer
  • API simplification

Bad code (without pattern)

# Complex subsystem with many classes
class Lights:
    def dim(self, percentage):
        return f"Lights dimmed to {percentage}%"

class SoundSystem:
    def set_volume(self, level):
        return f"Volume set to {level}"

    def enable_surround(self):
        return "Surround sound enabled"

class Projector:
    def turn_on(self):
        return "Projector is on"

    def set_resolution(self, resolution):
        return f"Resolution set to {resolution}"

class Curtains:
    def open(self):
        return "Curtains opening"

    def close(self):
        return "Curtains closing"

class PopcornMachine:
    def start(self):
        return "Popcorn machine started"

# Client needs to know about all these systems
def watch_movie():
    lights = Lights()
    sound = SoundSystem()
    projector = Projector()
    curtains = Curtains()
    popcorn = PopcornMachine()

    print(lights.dim(30))
    print(sound.set_volume(70))
    print(sound.enable_surround())
    print(projector.turn_on())
    print(projector.set_resolution("4K"))
    print(curtains.open())
    print(popcorn.start())
    # 7 steps for a simple action!

watch_movie()
Enter fullscreen mode Exit fullscreen mode

The problem: Too many details. The client needs to understand the entire theater system.

Good code (with Facade)

# Keep the complex subsystem (same as before)
class Lights:
    def dim(self, percentage):
        return f"Lights dimmed to {percentage}%"

class SoundSystem:
    def set_volume(self, level):
        return f"Volume set to {level}"

    def enable_surround(self):
        return "Surround sound enabled"

class Projector:
    def turn_on(self):
        return "Projector is on"

    def set_resolution(self, resolution):
        return f"Resolution set to {resolution}"

class Curtains:
    def open(self):
        return "Curtains opening"

class PopcornMachine:
    def start(self):
        return "Popcorn machine started"

# Facade: Single entry point
class TheaterFacade:
    def __init__(self):
        self.lights = Lights()
        self.sound = SoundSystem()
        self.projector = Projector()
        self.curtains = Curtains()
        self.popcorn = PopcornMachine()

    def watch_movie(self):
        """Simple interface hides complexity"""
        print(self.lights.dim(30))
        print(self.sound.set_volume(70))
        print(self.sound.enable_surround())
        print(self.projector.turn_on())
        print(self.projector.set_resolution("4K"))
        print(self.curtains.open())
        print(self.popcorn.start())
        print("\n🎬 Movie starting!\n")

    def end_movie(self):
        """Cleanup is also simplified"""
        print(self.curtains.open())  # Actually should close, but you get the idea
        print(self.lights.dim(100))  # Lights back to normal
        print("Theater ready for next showing")

# Client code is simple
theater = TheaterFacade()
theater.watch_movie()
theater.end_movie()
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Simplifies client code dramatically
  • Decouples clients from subsystem details
  • Makes the system easier to use
  • Can provide different facades for different needs
  • Good for versioning and API design

Cons:

  • Facade can become a "god object" if it does too much
  • Clients lose fine-grained control
  • Can hide important details when you need them
  • Adds another layer of indirection

My Take:

Facade is one of the most practical patterns in the real world.

Every well-designed library, framework, and API uses Facade principles. Flask, Django, FastAPI—they all provide simple interfaces on top of complex systems.

If your subsystem is growing complex and forcing clients to understand many classes, Facade is the answer.

But don't overuse it. One Facade is good. Ten Facades means your architecture is messy.


6. Flyweight Pattern

The Flyweight pattern uses sharing to support large numbers of fine-grained objects efficiently.

When you have thousands (or millions) of similar objects, you can't afford to create each one from scratch.

Flyweight extracts the shared, immutable data (intrinsic state) from the unique, changeable data (extrinsic state).

Classic example: A text editor with millions of characters.

Each character has:

  • Intrinsic state (never changes): Font family, size, style
  • Extrinsic state (changes): Position, color for this specific instance

Instead of storing both in each character object, flyweight stores intrinsic state once and shares it.

When to use it:

  • Managing large numbers of similar objects
  • Memory optimization is critical
  • Game development (particles, trees, NPCs)
  • Text editors with millions of characters
  • Caches and object pools
  • UI rendering of repeated elements

Bad code (without pattern)

# Without Flyweight, each character object stores everything
class Character:
    def __init__(self, char, font, size, color, x, y):
        self.char = char
        self.font = font  # Repeated thousands of times
        self.size = size  # Repeated thousands of times
        self.color = color  # Same for all red text
        self.x = x  # Unique per character
        self.y = y  # Unique per character

# Creating a document with 100,000 characters
document = []
for i in range(100000):
    # Each character duplicates font and size information
    char = Character(
        chr(ord('A') + (i % 26)),
        font="Arial",  # Repeated 100,000 times!
        size=12,  # Repeated 100,000 times!
        color="black",  # Repeated 100,000 times!
        x=i % 100,
        y=i // 100
    )
    document.append(char)

# Memory usage: Massive waste from duplication
Enter fullscreen mode Exit fullscreen mode

The problem: Redundant data stored in every object.

Good code (with Flyweight)

# Intrinsic state: Shared and immutable
class CharacterStyle:
    def __init__(self, font, size, color):
        self.font = font
        self.size = size
        self.color = color

    def __eq__(self, other):
        return (self.font == other.font and 
                self.size == other.size and 
                self.color == other.color)

    def __hash__(self):
        return hash((self.font, self.size, self.color))

# Flyweight Factory: Ensures only one instance per unique intrinsic state
class StyleFactory:
    _styles = {}

    @staticmethod
    def get_style(font, size, color):
        style = CharacterStyle(font, size, color)

        # Return existing style if we've seen it before
        if style not in StyleFactory._styles:
            StyleFactory._styles[style] = style

        return StyleFactory._styles[style]

# Extrinsic state: Unique per instance
class Character:
    def __init__(self, char, style, x, y):
        self.char = char
        self.style = style  # Shared reference
        self.x = x  # Unique
        self.y = y  # Unique

    def render(self):
        return f"{self.char} at ({self.x}, {self.y}) in {self.style.font}"

# Client code
document = []
style_factory = StyleFactory()

for i in range(100000):
    # All characters share the same Arial/12/black style
    style = style_factory.get_style("Arial", 12, "black")

    char = Character(
        chr(ord('A') + (i % 26)),
        style,  # Shared, not duplicated
        x=i % 100,
        y=i // 100
    )
    document.append(char)

# Check memory efficiency
arial_12_black = style_factory.get_style("Arial", 12, "black")
arial_12_blue = style_factory.get_style("Arial", 12, "blue")
arial_12_black_again = style_factory.get_style("Arial", 12, "black")

print(arial_12_black is arial_12_black_again)  # True - same object in memory
print(arial_12_black is arial_12_blue)  # False - different style
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Dramatically reduces memory usage for many similar objects
  • Improves performance through object sharing
  • Single source of truth for shared data
  • Works well with object pooling

Cons:

  • Added complexity with intrinsic/extrinsic separation
  • Thread safety considerations for factories
  • Debugging can be trickier with shared objects
  • Only valuable for large-scale scenarios

My Take:

Flyweight is not an everyday pattern. You use it when:

  1. You have many objects (thousands or millions)
  2. Memory usage is actually a problem (not hypothetical)
  3. Most of the data is redundant

Don't prematurely optimize. Start without Flyweight. When profiling shows memory as the bottleneck with repeated objects, then implement it.

Real-world examples:

  • Game engines: Particles (thousands of trees, rocks, bullets)
  • Web browsers: DOM nodes (thousands of same-styled elements)
  • Text editors: Character objects
  • Cache systems: Pooled objects

If you're not handling thousands of objects, Flyweight adds complexity for minimal benefit.


7. Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it.

A proxy sits between the client and the real object, intercepting all interactions.

Proxies can:

  • Lazy load — Create expensive objects only when needed
  • Control access — Allow/deny based on permissions
  • Log and monitor — Track all access
  • Cache — Return cached results
  • Remote access — Access objects across the network

Think about loading an image. You don't want to load the full 10MB image until the user actually views it.

A Proxy can delay loading until necessary.

When to use it:

  • Lazy initialization of expensive objects
  • Access control and permissions
  • Logging and monitoring
  • Caching
  • Remote objects (RPC, REST APIs)
  • Validation before real operations
  • Throttling or rate limiting

Bad code (without pattern)

# Image loading happens immediately, even if not viewed
class Image:
    def __init__(self, url):
        self.url = url
        # Expensive operation
        self.data = self.load_from_url(url)
        print(f"Image loaded: {url}")

    def load_from_url(self, url):
        # Simulate expensive loading
        print(f"Loading {url}... (this takes time!)")
        return "image_data_10mb"

    def display(self):
        print(f"Displaying {self.data}")

# Client code forces loading immediately
def show_webpage():
    images = []
    for i in range(100):
        # All 100 images load immediately
        image = Image(f"http://example.com/image{i}.jpg")
        images.append(image)

    # User only views 3 images, but all 100 were loaded!
    print("Displaying first 3 images:")
    images[0].display()
    images[1].display()
    images[2].display()

show_webpage()
Enter fullscreen mode Exit fullscreen mode

The problem: All images load eagerly, even if never viewed.

Good code (with Proxy)

# Real image class
class RealImage:
    def __init__(self, url):
        self.url = url
        self.data = self._load()

    def _load(self):
        print(f"Loading {self.url}... (expensive operation)")
        return "image_data_10mb"

    def display(self):
        print(f"Displaying image from {self.url}")

# Proxy: Controls access to RealImage
class ImageProxy:
    def __init__(self, url):
        self.url = url
        self._real_image = None  # Not loaded yet

    def _ensure_loaded(self):
        """Lazy loading: Only load when actually needed"""
        if self._real_image is None:
            self._real_image = RealImage(self.url)

    def display(self):
        self._ensure_loaded()  # Load only when display is called
        self._real_image.display()

# Client code using proxy
def show_webpage():
    images = []
    for i in range(100):
        # Create proxies, not actual images
        proxy = ImageProxy(f"http://example.com/image{i}.jpg")
        images.append(proxy)

    print("100 image proxies created (nothing loaded yet)\n")

    # Only display 3 images - only those 3 load
    print("Displaying first 3 images:")
    images[0].display()  # This one loads
    images[1].display()  # This one loads
    images[2].display()  # This one loads
    # 97 images never loaded!

show_webpage()
Enter fullscreen mode Exit fullscreen mode

Practical Example: Access Control Proxy

# Real service
class DatabaseService:
    def execute_query(self, query):
        return f"Executing: {query}"

    def delete_database(self):
        return "Database deleted!"

# Proxy with access control
class DatabaseServiceProxy:
    def __init__(self, service, user_role):
        self.service = service
        self.user_role = user_role

    def execute_query(self, query):
        if self.user_role in ["admin", "analyst"]:
            return self.service.execute_query(query)
        else:
            return "Access denied: insufficient permissions"

    def delete_database(self):
        if self.user_role == "admin":
            return self.service.delete_database()
        else:
            return "Access denied: only admins can delete"

# Usage
db = DatabaseService()

# Admin proxy
admin_proxy = DatabaseServiceProxy(db, "admin")
print(admin_proxy.execute_query("SELECT * FROM users"))
print(admin_proxy.delete_database())

# User proxy
user_proxy = DatabaseServiceProxy(db, "user")
print(user_proxy.execute_query("SELECT * FROM users"))
print(user_proxy.delete_database())  # Denied!
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Lazy initialization saves resources
  • Control and restrict access as needed
  • Add logging and monitoring transparently
  • Can implement caching efficiently
  • Decouple clients from real objects
  • Handle remote objects transparently

Cons:

  • Added layer of indirection
  • Slight performance overhead
  • Debugging becomes harder
  • Can hide implementation details excessively

My Take:

Proxy is everywhere in production systems, even if developers don't explicitly recognize it.

Common real-world uses:

  • ORM lazy loading (Django, SQLAlchemy)
  • Web frameworks (middleware, decorators)
  • Mock objects (testing)
  • Caching layers (Redis, memcached)
  • RPC clients (gRPC, REST clients)
  • Database connection pooling

The key insight: A proxy is a controlled gateway.

It's perfect for:

  1. Protecting expensive operations
  2. Controlling access
  3. Monitoring and logging
  4. Adding behavior transparentlyhttps://dev.to/mahdi0shamlou/structural-design-patterns-python-part-1

Use Proxy when you need to intercept or control how a client interacts with an object.


Complete Structural Patterns Comparison

Pattern Purpose Complexity When to Use
Adapter Make incompatible interfaces work together Low Integrating third-party libraries
Bridge Decouple abstraction from implementation Medium Multiple independent dimensions
Composite Build tree structures uniformly Medium Hierarchical data (files, menus, DOM)
Decorator Add behavior without modifying classes Medium Dynamic feature addition, avoid explosion
Facade Simplify complex subsystems Low Complex internal systems, APIs
Flyweight Share data to save memory Medium Thousands of similar objects
Proxy Control access to another object Medium Lazy loading, access control, monitoring

Structural Patterns in Real Production Code

You're already using these patterns whether you recognize it or not:

Facade in Action

import requests  # Simple facade over complex HTTP protocol
response = requests.get("https://api.example.com/users")
Enter fullscreen mode Exit fullscreen mode

Decorator in Action

@app.route("/users")
@login_required  # Decorator pattern
@cache(timeout=300)  # Another decorator
def get_users():
    pass
Enter fullscreen mode Exit fullscreen mode

Proxy in Action

user = User.objects.get(id=1)  # Django ORM proxy
print(user.posts.all())  # Posts only loaded when accessed (lazy loading)
Enter fullscreen mode Exit fullscreen mode

Adapter in Action

from sqlalchemy import create_engine
# Adapter between Python code and multiple database types
engine = create_engine("postgresql://user:pass@host/db")
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Adapter bridges incompatible systems
  2. Bridge prevents class explosion from multiple dimensions
  3. Composite treats parts and wholes uniformly
  4. Decorator adds behavior without modification
  5. Facade hides complexity behind a simple interface
  6. Flyweight saves memory through sharing
  7. Proxy controls access and adds behavior transparently

The Golden Rule: Use structural patterns to make your code more flexible and maintainable, not to show off.

Bad code with patterns is still bad code.


What's Next?

We've covered all Structural Design Patterns.

Coming next in the series:

  • Behavioral Design Patterns
    • Strategy, Observer, Command, State
    • Template Method, Chain of Responsibility
    • Mediator, Memento, Interpreter, Visitor, Iterator

Each with the same production-focused approach: real problems, real solutions, and honest guidance on when to use them.


Want More Deep Dives?

Mahdi Shamlou

If you enjoyed this article, check out my other production-focused guides:


🔗 LinkedIn: https://www.linkedin.com/in/mahdi-shamlou-3b52b8278
📱 Telegram: https://telegram.me/mahdi0shamlou
📸 Instagram: https://www.instagram.com/mahdi0shamlou/

Author: Mahdi Shamlou | مهدی شاملو

Top comments (0)