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
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()
With Facade, the client just calls:
theater.watch_movie()
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()
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()
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
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
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:
- You have many objects (thousands or millions)
- Memory usage is actually a problem (not hypothetical)
- 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()
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()
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!
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:
- Protecting expensive operations
- Controlling access
- Monitoring and logging
- 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")
Decorator in Action
@app.route("/users")
@login_required # Decorator pattern
@cache(timeout=300) # Another decorator
def get_users():
pass
Proxy in Action
user = User.objects.get(id=1) # Django ORM proxy
print(user.posts.all()) # Posts only loaded when accessed (lazy loading)
Adapter in Action
from sqlalchemy import create_engine
# Adapter between Python code and multiple database types
engine = create_engine("postgresql://user:pass@host/db")
Key Takeaways
- Adapter bridges incompatible systems
- Bridge prevents class explosion from multiple dimensions
- Composite treats parts and wholes uniformly
- Decorator adds behavior without modification
- Facade hides complexity behind a simple interface
- Flyweight saves memory through sharing
- 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?
If you enjoyed this article, check out my other production-focused guides:
- Message Brokers in 2026: Kafka, RabbitMQ, NATS — Architecture decisions
- OWASP Top 10 for Developers (2026 Edition) — Security patterns
- Injection Attacks Are Not Dead — Attack patterns and solutions
- Durable Workflow Engines: Temporal vs dbt OS vs Transact vs Prefect — System patterns
🔗 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)