Structural Design Patterns (GoF)
The seven GoF structural patterns deal with how classes and objects are composed to form larger structures, keeping the structures flexible and efficient.
Reference: refactoring.guru / Design Patterns — illustrated explanations of every GoF pattern; cross-reference each pattern below against the canonical descriptions there.
The seven structural patterns:
| Pattern | Intent | refactoring.guru |
|---|---|---|
| Adapter | Convert incompatible interface | https://refactoring.guru/design-patterns/adapter |
| Bridge | Decouple abstraction from implementation | https://refactoring.guru/design-patterns/bridge |
| Composite | Tree of part-whole objects | https://refactoring.guru/design-patterns/composite |
| Decorator | Add behavior via wrappers | https://refactoring.guru/design-patterns/decorator |
| Facade | Simplified interface to a subsystem | https://refactoring.guru/design-patterns/facade |
| Flyweight | Share state to support many objects | https://refactoring.guru/design-patterns/flyweight |
| Proxy | Surrogate / placeholder for another object | https://refactoring.guru/design-patterns/proxy |
For creational patterns see 05_creational_patterns.md; for behavioral 07_behavioral_patterns.md.
Adapter
Intent: Convert the interface of a class into another interface clients expect. Lets classes work together that couldn’t otherwise because of incompatible interfaces. Reference: https://refactoring.guru/design-patterns/adapter
Problem: You have a class with the data you need but the wrong API. The client expects method request(), your class has specific_request(). You can’t modify the class (third-party library, legacy code).
Solution: Wrap the incompatible class in an adapter that translates calls.
# Existing class with the wrong interface
class LegacyXMLParser:
def parse_xml(self, xml_str: str) -> dict:
return {"parsed": xml_str} # imagine real XML parsing
# Client expects this interface
class JSONParser:
def parse(self, json_str: str) -> dict: ...
# Adapter
class XMLToJSONParserAdapter(JSONParser):
def __init__(self, legacy: LegacyXMLParser):
self.legacy = legacy
def parse(self, json_str: str) -> dict:
# Convert JSON → XML → call legacy → return result
xml = self._json_to_xml(json_str)
return self.legacy.parse_xml(xml)
def _json_to_xml(self, j): return f"<root>{j}</root>"
# Client code is unchanged
def process(parser: JSONParser, data: str):
return parser.parse(data)
result = process(XMLToJSONParserAdapter(LegacyXMLParser()), '{"key": "value"}')
Two flavors:
- Object Adapter (composition — above example): adapter holds a reference to the adaptee.
- Class Adapter (multiple inheritance): adapter inherits from both interfaces. Python supports but rarely needed.
When to use:
- Integrating third-party libraries with mismatched APIs.
- Legacy code modernization without rewriting.
- Anti-corruption layer in DDD (translate external models to domain models).
Common pitfalls:
- “Just expose the legacy method directly” — caller now depends on the wrong abstraction. Use an adapter to keep the boundary clean.
- Adapter accumulating logic beyond translation — turns into a god class.
Bridge
Intent: Decouple an abstraction from its implementation so that the two can vary independently. Reference: https://refactoring.guru/design-patterns/bridge
Problem: A class hierarchy has two orthogonal dimensions (e.g., shape × color). Trying to express both via inheritance produces a Cartesian explosion: RedCircle, BlueCircle, RedSquare, BlueSquare…
Solution: Split the hierarchy into two: one for the abstraction (Shape), one for the implementation (Color). The abstraction holds a reference to the implementation.
from abc import ABC, abstractmethod
# Implementation hierarchy
class Color(ABC):
@abstractmethod
def fill(self) -> str: ...
class Red(Color):
def fill(self): return "red"
class Blue(Color):
def fill(self): return "blue"
# Abstraction hierarchy
class Shape(ABC):
def __init__(self, color: Color):
self.color = color
@abstractmethod
def draw(self) -> str: ...
class Circle(Shape):
def draw(self): return f"Circle painted {self.color.fill()}"
class Square(Shape):
def draw(self): return f"Square painted {self.color.fill()}"
print(Circle(Red()).draw()) # "Circle painted red"
print(Square(Blue()).draw()) # "Square painted blue"
Adding a new color: one new class. Adding a new shape: one new class. Without Bridge: each combination is a class.
When to use:
- Two orthogonal axes of variation.
- Avoiding exponential class explosion from multiple-inheritance hierarchies.
- Need to switch the implementation at runtime.
Bridge vs Adapter:
- Adapter retrofits incompatible interfaces; designed-in after the fact.
- Bridge separates two dimensions of variation upfront; designed-in from the start.
Common pitfalls:
- Over-applying when one hierarchy suffices.
- Confusion with Strategy (similar structure; Strategy is for interchangeable algorithms, Bridge for implementations).
Composite
Intent: Compose objects into tree structures to represent part-whole hierarchies. Lets clients treat individual objects and compositions of objects uniformly. Reference: https://refactoring.guru/design-patterns/composite
Problem: A tree-shaped structure (file system, UI widget hierarchy, organization chart) where leaves and branches need similar operations. You don’t want client code to check if isinstance(x, Leaf) everywhere.
Solution: A common interface for both leaves and composites; composites recurse on their children.
from abc import ABC, abstractmethod
class FileSystemNode(ABC):
@abstractmethod
def size(self) -> int: ...
@abstractmethod
def display(self, indent: int = 0) -> str: ...
class File(FileSystemNode):
def __init__(self, name: str, size: int):
self.name = name
self._size = size
def size(self): return self._size
def display(self, indent=0):
return " " * indent + f"{self.name} ({self._size} bytes)"
class Directory(FileSystemNode):
def __init__(self, name: str):
self.name = name
self.children: list[FileSystemNode] = []
def add(self, child): self.children.append(child)
def size(self): return sum(c.size() for c in self.children)
def display(self, indent=0):
result = [" " * indent + f"{self.name}/"]
for child in self.children:
result.append(child.display(indent + 2))
return "\n".join(result)
root = Directory("project")
root.add(File("README.md", 1024))
src = Directory("src")
src.add(File("main.py", 2048))
src.add(File("utils.py", 512))
root.add(src)
print(root.display())
print(f"Total: {root.size()} bytes")
When to use:
- Genuine tree structures.
- Operations apply uniformly to nodes and to subtrees (
size,render,print). - Client code shouldn’t distinguish leaves from branches.
Common pitfalls:
- The Component interface includes operations that don’t make sense for leaves (
add,remove). The classic trade-off: type safety vs uniformity. Pythonic answer: leaves raise onaddor use separate interfaces.
Decorator
Intent: Attach additional responsibilities to an object dynamically. Provides a flexible alternative to subclassing for extending functionality. Reference: https://refactoring.guru/design-patterns/decorator
Problem: You want to add behavior to specific instances of a class at runtime, not all instances via inheritance. You may want multiple combinations (logged + cached + retried, or just cached + retried).
Solution: Wrap the object in a “decorator” that has the same interface and forwards calls, adding behavior.
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self) -> float: ...
@abstractmethod
def description(self) -> str: ...
class SimpleCoffee(Coffee):
def cost(self): return 5.0
def description(self): return "coffee"
class CoffeeDecorator(Coffee):
def __init__(self, wrappee: Coffee):
self._wrappee = wrappee
def cost(self): return self._wrappee.cost()
def description(self): return self._wrappee.description()
class Milk(CoffeeDecorator):
def cost(self): return super().cost() + 1.5
def description(self): return super().description() + ", milk"
class Sugar(CoffeeDecorator):
def cost(self): return super().cost() + 0.5
def description(self): return super().description() + ", sugar"
class WhippedCream(CoffeeDecorator):
def cost(self): return super().cost() + 2.0
def description(self): return super().description() + ", whipped cream"
# Stack decorators dynamically
order = WhippedCream(Sugar(Milk(SimpleCoffee())))
print(order.description()) # "coffee, milk, sugar, whipped cream"
print(f"${order.cost():.2f}") # "$9.00"
Pythonic note: don’t confuse with Python decorator syntax (@functools.wraps). They share the name and concept (wrapping behavior), but Python decorators wrap functions/classes via syntax; the GoF Decorator wraps objects via composition.
When to use:
- Adding responsibilities to objects without subclass explosion.
- Behavior is opt-in per-instance.
- Combining multiple optional behaviors (logging × caching × retry × encryption).
Common pitfalls:
- Wrapping order matters for non-commutative operations.
- Identity confusion:
wrapped is originalis False;isinstance(wrapped, Original)may be False. - Deeply nested decorators hide the underlying object’s behavior; harder to debug.
Facade
Intent: Provide a unified, simpler interface to a set of interfaces in a subsystem. Defines a higher-level interface that makes the subsystem easier to use. Reference: https://refactoring.guru/design-patterns/facade
Problem: A complex subsystem has many classes with intricate interactions. Client code shouldn’t deal with the internal complexity for common operations.
Solution: A single class (the Facade) exposes the simplified operations clients need; internally it coordinates the subsystem.
# Complex subsystem
class VideoFile:
def __init__(self, path): self.path = path
class CodecFactory:
def extract(self, file: VideoFile): return "h264_codec" if "h264" in file.path else "mpeg4_codec"
class BitrateReader:
@staticmethod
def read(file, codec): return f"read {file.path} with {codec}"
@staticmethod
def convert(buffer, codec): return f"converted to {codec}"
class AudioMixer:
def fix(self, output): return f"audio fixed in {output}"
# Facade — one-method API for converting video
class VideoConverter:
def convert(self, filename: str, target_format: str) -> str:
file = VideoFile(filename)
source_codec = CodecFactory().extract(file)
destination_codec = "ogg_codec" if target_format == "ogg" else "mpeg4_codec"
buffer = BitrateReader.read(file, source_codec)
result = BitrateReader.convert(buffer, destination_codec)
result = AudioMixer().fix(result)
return f"final: {result}"
# Client uses one simple call
converter = VideoConverter()
print(converter.convert("input.mp4", "ogg"))
Facade vs Adapter:
- Facade: simplifies a complex subsystem (you control the subsystem).
- Adapter: makes incompatible interface compatible (you adapt to an external API).
When to use:
- Layered architecture — each layer’s API is a facade over the next.
- Wrapping a complex third-party library with a project-specific API.
- Microservice “API gateway” — facade over many internal services.
Common pitfalls:
- Facade becoming a god object as more operations are added.
- Hiding the subsystem too completely — power users need to bypass.
Flyweight
Intent: Use sharing to support large numbers of fine-grained objects efficiently. Reference: https://refactoring.guru/design-patterns/flyweight
Problem: You need many instances of similar objects (millions of game particles, every character in a document) and creating one per occurrence runs out of memory.
Solution: Separate the intrinsic state (shared, immutable) from the extrinsic state (varies per instance, supplied per call). Cache intrinsic state and share instances.
class TreeType:
"""Intrinsic state — shared across many trees."""
def __init__(self, name: str, color: str, texture: str):
self.name = name
self.color = color
self.texture = texture
def draw(self, x: int, y: int):
return f"Drawing {self.name} ({self.color}) at ({x}, {y})"
class TreeTypeFactory:
_types: dict[tuple, TreeType] = {}
@classmethod
def get(cls, name: str, color: str, texture: str) -> TreeType:
key = (name, color, texture)
if key not in cls._types:
cls._types[key] = TreeType(name, color, texture)
return cls._types[key] # return shared instance
class Tree:
"""Each tree has unique position (extrinsic); shares its type."""
def __init__(self, x: int, y: int, type_: TreeType):
self.x = x
self.y = y
self.type = type_
def draw(self):
return self.type.draw(self.x, self.y)
# Forest: a million trees, but only a handful of TreeTypes
forest = []
for i in range(1_000_000):
type_ = TreeTypeFactory.get("Oak", "green", "rough") # all share one TreeType
forest.append(Tree(x=i % 1000, y=i // 1000, type_=type_))
Without Flyweight: 1M trees × (name + color + texture + position) ≈ huge memory. With Flyweight: 1M positions + 1 shared TreeType. The expensive fields are stored once.
Python’s string interning is a built-in Flyweight: short strings and identifier-like strings are cached, so multiple "hello" literals share one object.
When to use:
- Many similar objects (graphics, game entities, document characters, CSS styles).
- Memory pressure is real.
- Most of the object state is intrinsic (immutable, shareable).
When NOT to use:
- The objects aren’t actually similar.
- Memory isn’t the bottleneck.
- Extracting extrinsic state is more complex than the savings.
Common pitfalls:
- Mutating “intrinsic” state breaks the sharing assumption.
- Hash key collisions in the factory cache.
- Thread safety — the factory cache needs locking under concurrent access.
Proxy
Intent: Provide a surrogate or placeholder for another object to control access to it. Reference: https://refactoring.guru/design-patterns/proxy
Problem: You want to add behavior around accessing an object — lazy loading, access control, caching, logging — without modifying the object itself or its clients.
Solution: A proxy class with the same interface as the real object. The proxy delegates to the real object, adding its own logic before/after/instead.
Common proxy types:
| Type | Purpose |
|---|---|
| Virtual Proxy | lazy-load expensive objects on first use |
| Protection Proxy | enforce access control |
| Remote Proxy | local stand-in for remote object (RPC clients) |
| Caching Proxy | cache results |
| Logging / Smart Reference Proxy | reference counting, logging, monitoring |
from abc import ABC, abstractmethod
import time
class Image(ABC):
@abstractmethod
def display(self) -> str: ...
class RealImage(Image):
def __init__(self, filename: str):
self.filename = filename
self._load() # expensive: load from disk
def _load(self):
print(f"Loading {self.filename}...")
time.sleep(0.1) # imagine 100ms disk I/O
def display(self):
return f"Displaying {self.filename}"
class ImageProxy(Image):
def __init__(self, filename: str):
self.filename = filename
self._real_image: RealImage | None = None # lazy
def display(self):
if self._real_image is None:
self._real_image = RealImage(self.filename)
return self._real_image.display()
# Constructing a proxy is cheap; loading deferred
images = [ImageProxy(f"photo_{i}.jpg") for i in range(1000)]
# Only loads when display() is called
print(images[5].display())
Protection proxy:
class AdminProxy:
def __init__(self, real_service, user):
self.real = real_service
self.user = user
def delete_user(self, user_id):
if not self.user.is_admin:
raise PermissionError("admin required")
return self.real.delete_user(user_id)
Proxy vs Decorator:
- Proxy controls access — same lifetime/identity boundary as the real object.
- Decorator adds responsibilities — multiple decorators stack.
- Structurally similar; intent differs.
Proxy vs Adapter:
- Adapter: different interface, makes compatible.
- Proxy: same interface, controls access.
When to use:
- Lazy initialization for expensive objects.
- Authorization/security wrapping.
- Remote access (RPC clients are proxies).
- Caching (memoization wrappers).
- Logging and metrics injection.
Common pitfalls:
- Proxy operations that mask the real object’s exceptions or behavior subtly.
- Forgetting that
proxy is real_objectis False. - Deep proxy chains complicating debugging.
Choosing among structural patterns
| Situation | Pattern |
|---|---|
| Incompatible interfaces | Adapter |
| Two orthogonal axes of variation | Bridge |
| Tree-shaped data, uniform operations | Composite |
| Add behavior to specific instances | Decorator |
| Simplified interface to a complex subsystem | Facade |
| Many similar objects, save memory | Flyweight |
| Control access (lazy / cache / auth / remote) | Proxy |
Pattern relationships
- Adapter + Facade: both wrap something; Adapter for compatibility, Facade for simplification.
- Decorator + Proxy: structurally identical; Decorator stacks for behavior, Proxy controls access.
- Composite + Decorator: composites use decorators internally; decorators can be composed.
- Bridge + Strategy: structurally identical; Bridge is a design-time decision about abstraction/implementation split, Strategy is runtime algorithm selection (Strategy is behavioral, see 07_behavioral_patterns.md).
Common interview confusions
- “Adapter and Facade are the same.” — overlap but differ in intent. Adapter changes one interface; Facade simplifies many.
- “Python’s @decorator is the Decorator pattern.” — related but different. The syntax wraps functions; the GoF pattern wraps objects with classes.
- “Proxy and Decorator are interchangeable.” — same structure, different intent. Don’t mix them up in design discussions.
Interview angle
- “What are the GoF structural patterns?” — seven patterns about composing classes and objects: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy.
- “Adapter vs Facade?” — Adapter wraps a class with an incompatible interface to match what clients expect (single class). Facade provides a simplified interface over a complex subsystem (many classes).
- “Decorator vs Proxy?” — same structure (wrapper with the same interface as the wrapped object); intent differs. Decorator adds behavior (stacks for combinations); Proxy controls access (one wrapper; lifetime/identity-bound to the real object).
- “What’s Flyweight and when do you use it?” — share intrinsic (immutable) state across many similar objects; supply extrinsic (per-instance) state externally. Use for many similar objects under memory pressure (game particles, document characters, CSS styles). Python’s string interning is a built-in example.
- “How does the Composite pattern work?” — leaves and composites share a common interface; composites recurse on children. Client code treats both uniformly. Common in file systems, UI hierarchies, expression trees.
- “What is the Bridge pattern’s main benefit?” — separates two orthogonal axes of variation (e.g., shape × color) so each can vary independently. Avoids Cartesian-product class explosion from multi-dimensional inheritance.
- “Real-world examples of Proxy?” — ORM lazy loading (loading a related object only on access), Django/Flask permission checks, gRPC client stubs (remote proxy),
@lru_cache(caching proxy concept), HTTP reverse proxies (different but related concept).