DEV Community

Mahdi Shamlou | Structural Design Patterns 2026: Adapter, Bridge, Composite & Decorator — Production Examples

Mahdi Shamlou here.

After breaking down Creational Design Patterns, you now understand how to build objects the right way.

But building objects is only half the battle.

The real challenge in production systems is how you organize relationships between those objects.

That's where Structural Design Patterns come in.

Mahdi Shamlou

Structural patterns show you:

  • How to compose objects into larger structures
  • How to maintain flexibility when your system grows
  • How to simplify complex relationships
  • How to add behavior without touching existing code

I've seen systems where:

  • Tight coupling made changes impossible
  • New features required modifying dozens of files
  • Adapting to new requirements meant rewriting entire modules
  • Code became harder to test and maintain

Most of these problems vanish when you apply the right structural pattern.

Let's dive into the patterns that actually matter in 2026.


What Are Structural Design Patterns?

Structural patterns deal with object composition and relationships.

While Creational patterns answer: "How do I create objects?"

Structural patterns answer: "How do I organize objects to create larger, more flexible systems?"

The main structural patterns are:

  1. Adapter — Make incompatible objects work together
  2. Bridge — Decouple abstraction from implementation
  3. Composite — Build tree-like structures
  4. Decorator — Add behavior without modification
  5. Facade — Simplify complex subsystems
  6. Flyweight — Share data to save memory
  7. Proxy — Control access to another object

Let's explore each one with real production code.


Structural Patterns That Actually Matter

1. Adapter Pattern

The Adapter pattern makes incompatible interfaces work together.

Imagine you have a payment system that expects:

processor.charge(amount, card_number)
Enter fullscreen mode Exit fullscreen mode

But your new payment provider expects:

processor.send_payment(amount, card_details_object)
Enter fullscreen mode Exit fullscreen mode

Without the Adapter pattern, you'd need to modify your entire codebase. With it, you create an adapter that translates between the two.

When to use it:

  • Integrating third-party libraries
  • Working with legacy code
  • Using multiple vendors with different APIs
  • Creating unified interfaces for different providers
  • Supporting multiple database drivers

Bad code (without pattern)

# You have two different payment systems with incompatible interfaces

class OldPaymentGateway:
    def pay(self, amount, card_number):
        return f"Processing ${amount} with card {card_number}"

class NewPaymentGateway:
    def send_payment(self, amount, payment_details):
        return f"Processing ${amount} with {payment_details['method']}"

# Your application expects a unified interface
def checkout(payment_system, amount, card_number):
    if isinstance(payment_system, OldPaymentGateway):
        return payment_system.pay(amount, card_number)
    elif isinstance(payment_system, NewPaymentGateway):
        # This doesn't work! Different interface
        return payment_system.send_payment(
            amount, 
            {"method": "card", "number": card_number}
        )
    else:
        raise ValueError("Unknown payment system")

# Every time you add a new provider, you modify checkout()
Enter fullscreen mode Exit fullscreen mode

The problem: Your checkout function becomes a conditional nightmare.

Good code (with Adapter)

from abc import ABC, abstractmethod

# Define your expected interface
class PaymentProcessor(ABC):
    @abstractmethod
    def process(self, amount, card_number):
        pass

# Legacy system - doesn't match your interface
class OldPaymentGateway:
    def pay(self, amount, card_number):
        return f"Processing ${amount} with card {card_number}"

# New system - also doesn't match your interface
class NewPaymentGateway:
    def send_payment(self, amount, payment_details):
        return f"Processing ${amount} with {payment_details['method']}"

# Create adapters that translate to your expected interface
class OldPaymentAdapter(PaymentProcessor):
    def __init__(self, old_gateway):
        self.gateway = old_gateway

    def process(self, amount, card_number):
        return self.gateway.pay(amount, card_number)

class NewPaymentAdapter(PaymentProcessor):
    def __init__(self, new_gateway):
        self.gateway = new_gateway

    def process(self, amount, card_number):
        payment_details = {"method": "card", "number": card_number}
        return self.gateway.send_payment(amount, payment_details)

# Now your checkout function is simple and never changes
def checkout(processor: PaymentProcessor, amount, card_number):
    return processor.process(amount, card_number)

# Usage
old_gateway = OldPaymentGateway()
old_processor = OldPaymentAdapter(old_gateway)
checkout(old_processor, 100, "4111111111111111")

new_gateway = NewPaymentGateway()
new_processor = NewPaymentAdapter(new_gateway)
checkout(new_processor, 100, "4111111111111111")

# Add a third provider? Just create another adapter
# checkout() never changes
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Allows incompatible interfaces to work together
  • Doesn't require changing existing classes
  • Keeps business logic separate from integration logic
  • Makes testing easier

Cons:

  • Creates additional classes
  • Can hide poor design choices
  • Overuse leads to extra complexity

My Take:

The Adapter pattern is essential when working with real-world systems.

You'll constantly integrate third-party libraries, legacy systems, and multiple vendors. Adapters keep your code clean and flexible.

Every well-designed system should have an adapter layer for external dependencies. It's not optional if you care about maintainability.


2. Bridge Pattern

The Bridge pattern decouples an abstraction from its implementation so they can vary independently.

Think about database drivers.

You want your application to work with any database. But each database has a different implementation:

Application
    ↓
Abstraction (UserRepository interface)
    ↓
Bridge
    ↓
Concrete Implementation (PostgreSQL, MySQL, MongoDB)
Enter fullscreen mode Exit fullscreen mode

Without the Bridge pattern, your abstraction gets tightly coupled to specific implementations.

When to use it:

  • Avoiding permanent binding between abstraction and implementation
  • Supporting multiple implementations that can change independently
  • Reducing class explosion (especially with multiple dimensions)
  • Allowing independent evolution of abstractions and implementations
  • Plugin architectures

Bad code (without pattern)

# Without Bridge, you end up with exponential class explosion

# One dimension: Database
class PostgreSQLUserRepository:
    def find_user(self, id):
        return f"Finding user {id} from PostgreSQL"

class MongoDBUserRepository:
    def find_user(self, id):
        return f"Finding user {id} from MongoDB"

# Another dimension: Caching
class PostgreSQLUserRepositoryWithCache:
    def find_user(self, id):
        if self.cache.has(id):
            return self.cache.get(id)
        return f"Finding user {id} from PostgreSQL (with cache)"

class MongoDBUserRepositoryWithCache:
    def find_user(self, id):
        if self.cache.has(id):
            return self.cache.get(id)
        return f"Finding user {id} from MongoDB (with cache)"

# Add another dimension (encryption)?
# Add another database? (SQLite)
# Now you have 3 × 2 = 6 classes
# Add one more dimension and you have 3 × 2 × 2 = 12 classes
# This is the "class explosion" problem
Enter fullscreen mode Exit fullscreen mode

Good code (with Bridge)

from abc import ABC, abstractmethod

# Abstraction: What the client wants
class UserRepository(ABC):
    def __init__(self, implementation):
        self.implementation = implementation

    @abstractmethod
    def find_user(self, user_id):
        pass

# Concrete Abstraction
class CachedUserRepository(UserRepository):
    def __init__(self, implementation):
        super().__init__(implementation)
        self.cache = {}

    def find_user(self, user_id):
        if user_id in self.cache:
            return self.cache[user_id]

        result = self.implementation.fetch_user(user_id)
        self.cache[user_id] = result
        return result

class SimpleUserRepository(UserRepository):
    def find_user(self, user_id):
        return self.implementation.fetch_user(user_id)

# Implementation: How to fetch the data
class DatabaseImplementation(ABC):
    @abstractmethod
    def fetch_user(self, user_id):
        pass

# Concrete Implementations
class PostgreSQLImplementation(DatabaseImplementation):
    def fetch_user(self, user_id):
        return f"User {user_id} from PostgreSQL"

class MongoDBImplementation(DatabaseImplementation):
    def fetch_user(self, user_id):
        return f"User {user_id} from MongoDB"

class SQLiteImplementation(DatabaseImplementation):
    def fetch_user(self, user_id):
        return f"User {user_id} from SQLite"

# Usage: Mix and match any abstraction with any implementation
postgres = PostgreSQLImplementation()
simple_repo = SimpleUserRepository(postgres)
print(simple_repo.find_user(1))  # User 1 from PostgreSQL

cached_repo = CachedUserRepository(postgres)
print(cached_repo.find_user(1))  # User 1 from PostgreSQL
print(cached_repo.find_user(1))  # Uses cache

# Switch to MongoDB without changing repository code
mongo = MongoDBImplementation()
cached_repo = CachedUserRepository(mongo)
print(cached_repo.find_user(1))  # User 1 from MongoDB

# 2 Abstractions × 3 Implementations = 5 classes total
# vs 6 classes without the pattern
# And now you can add implementations without creating new abstraction classes
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Avoids permanent binding between abstraction and implementation
  • Reduces class explosion
  • Allows independent evolution of abstractions and implementations
  • Supports runtime switching of implementations
  • Single Responsibility Principle

Cons:

  • Adds an extra layer of abstraction
  • Can be overkill for simple cases
  • Makes code harder to follow if over-applied

My Take:

The Bridge pattern solves a real problem: class explosion when you have multiple independent dimensions of variation.

Use it when you have:

  • Multiple abstractions that need multiple implementations
  • Need to support multiple variants that can change independently

But don't use it for simple, one-dimensional cases. Start simple, add Bridge only when you see actual class explosion.


3. Composite Pattern

The Composite pattern composes objects into tree structures to represent part-whole hierarchies.

It lets clients treat individual objects and compositions of objects uniformly.

Think about a file system:

/root
├── file1.txt
├── folder1
│   ├── file2.txt
│   └── file3.txt
└── folder2
    └── file4.txt
Enter fullscreen mode Exit fullscreen mode

A file and a folder both have a size() method. A folder's size is the sum of all its contents.

Without the Composite pattern, you handle files and folders differently everywhere.

With it, you treat them the same.

When to use it:

  • File system structures
  • Menu hierarchies
  • Organization charts
  • DOM trees
  • Comment threads (parent comments with nested replies)
  • Feature trees in games

Bad code (without pattern)

class File:
    def __init__(self, name, size):
        self.name = name
        self.size = size

    def get_size(self):
        return self.size

class Folder:
    def __init__(self, name):
        self.name = name
        self.items = []

    def add_item(self, item):
        self.items.append(item)

    def get_size(self):
        # You need special logic to handle folders
        total = 0
        for item in self.items:
            if isinstance(item, File):
                total += item.get_size()
            elif isinstance(item, Folder):
                total += item.get_size()  # Recursive but explicit
        return total

# Client code is full of type checking
def calculate_storage(item):
    if isinstance(item, File):
        return item.get_size()
    elif isinstance(item, Folder):
        total = 0
        for sub_item in item.items:
            total += calculate_storage(sub_item)  # Repeated logic
        return total
    else:
        raise ValueError("Unknown type")
Enter fullscreen mode Exit fullscreen mode

Good code (with Composite)

from abc import ABC, abstractmethod

# Common interface for both files and folders
class FileSystemComponent(ABC):
    @abstractmethod
    def get_size(self):
        pass

    @abstractmethod
    def get_name(self):
        pass

# Leaf: File
class File(FileSystemComponent):
    def __init__(self, name, size):
        self.name = name
        self.size = size

    def get_size(self):
        return self.size

    def get_name(self):
        return self.name

# Composite: Folder
class Folder(FileSystemComponent):
    def __init__(self, name):
        self.name = name
        self.items = []

    def add_item(self, item: FileSystemComponent):
        self.items.append(item)

    def get_size(self):
        # Uniform interface: treat files and folders the same
        return sum(item.get_size() for item in self.items)

    def get_name(self):
        return self.name

# Client code is simple and consistent
def print_structure(component: FileSystemComponent, indent=0):
    print("  " * indent + f"{component.get_name()} ({component.get_size()} bytes)")

    # Only folders have items, but we don't need to check
    if isinstance(component, Folder):
        for item in component.items:
            print_structure(item, indent + 1)

# Usage
root = Folder("root")
root.add_item(File("file1.txt", 100))

folder1 = Folder("folder1")
folder1.add_item(File("file2.txt", 200))
folder1.add_item(File("file3.txt", 300))
root.add_item(folder1)

root.add_item(File("file4.txt", 150))

print(f"Total size: {root.get_size()} bytes")  # 750
print_structure(root)
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Simplifies client code
  • Clients treat individual and composite objects uniformly
  • Easy to add new component types
  • Natural way to represent hierarchical data

Cons:

  • Can make design too general
  • Performance overhead for large trees
  • Difficult to restrict what can be added to composites

My Take:

Composite is one of the most elegant patterns when applied correctly.

Any time you work with tree structures (file systems, UI components, organizational charts, comment threads), Composite makes your code dramatically simpler.

The key insight: Treat the part and the whole the same way. This eliminates tons of type checking and special cases.

Use it whenever you have hierarchical data. It's worth the abstraction.


4. Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically.

It's an alternative to subclassing that allows behavior to be added at runtime.

Imagine a coffee shop:

  • Base coffee: $3
  • Add cream: +$0.50
  • Add caramel: +$0.75
  • Add whipped cream: +$0.50

Without decorators, you'd create:

  • Coffee
  • CoffeeWithCream
  • CoffeeWithCaramel
  • CoffeeWithWhippedCream
  • CoffeeWithCreamAndCaramel
  • CoffeeWithCreamAndCaramelAndWhippedCream
  • ... (class explosion again)

With Decorator, you compose behaviors dynamically.

When to use it:

  • Adding features to objects without modifying them
  • Adding responsibilities dynamically
  • When subclassing would create too many classes
  • I/O streams with different filters
  • UI components with additional styling
  • Middleware in web frameworks

Bad code (without pattern)

class Coffee:
    def cost(self):
        return 3.0

    def description(self):
        return "Coffee"

class CoffeeWithCream(Coffee):
    def cost(self):
        return super().cost() + 0.50

    def description(self):
        return super().description() + ", Cream"

class CoffeeWithCaramel(Coffee):
    def cost(self):
        return super().cost() + 0.75

    def description(self):
        return super().description() + ", Caramel"

class CoffeeWithCreamAndCaramel(Coffee):
    def cost(self):
        return super().cost() + 0.50 + 0.75

    def description(self):
        return super().description() + ", Cream, Caramel"

# Every combination needs a new class
# With 5 toppings, you need 2^5 = 32 classes
# This is unmaintainable
Enter fullscreen mode Exit fullscreen mode

Good code (with Decorator)

from abc import ABC, abstractmethod

# Component
class Beverage(ABC):
    @abstractmethod
    def cost(self):
        pass

    @abstractmethod
    def description(self):
        pass

# Concrete Component
class Coffee(Beverage):
    def cost(self):
        return 3.0

    def description(self):
        return "Coffee"

# Decorator Base Class
class BeverageDecorator(Beverage):
    def __init__(self, beverage: Beverage):
        self.beverage = beverage

    def cost(self):
        return self.beverage.cost()

    def description(self):
        return self.beverage.description()

# Concrete Decorators
class CreamDecorator(BeverageDecorator):
    def cost(self):
        return self.beverage.cost() + 0.50

    def description(self):
        return self.beverage.description() + ", Cream"

class CaramelDecorator(BeverageDecorator):
    def cost(self):
        return self.beverage.cost() + 0.75

    def description(self):
        return self.beverage.description() + ", Caramel"

class WhippedCreamDecorator(BeverageDecorator):
    def cost(self):
        return self.beverage.cost() + 0.50

    def description(self):
        return self.beverage.description() + ", Whipped Cream"

# Compose dynamically
coffee = Coffee()
print(f"{coffee.description()}: ${coffee.cost()}")

coffee_with_cream = CreamDecorator(coffee)
print(f"{coffee_with_cream.description()}: ${coffee_with_cream.cost()}")

fancy_coffee = WhippedCreamDecorator(CaramelDecorator(CreamDecorator(Coffee())))
print(f"{fancy_coffee.description()}: ${fancy_coffee.cost()}")

# Add any combination without new classes
iced_fancy = WhippedCreamDecorator(CaramelDecorator(Coffee()))
print(f"{iced_fancy.description()}: ${iced_fancy.cost()}")
Enter fullscreen mode Exit fullscreen mode

Pros:

  • More flexible than subclassing
  • Responsibilities can be added/removed at runtime
  • Single Responsibility Principle
  • Avoids class explosion
  • Can combine decorators in any order

Cons:

  • Many small decorator classes
  • Order of decorators can matter
  • Harder to understand with multiple layers
  • Debugging decorated objects can be tricky

My Take:

Decorator is one of my favorite patterns because it solves real problems in elegant ways.

Every time I see a class hierarchy growing out of control (Coffee, CoffeeWithCream, CoffeeWithCreamAndSugar, ...), I immediately think "Decorator."

Python's context managers, decorators (yes, they use this pattern!), and middleware all use this pattern. It's production-tested and battle-hardened.

Use Decorator whenever you need to add behavior dynamically without modifying existing classes.


Comparison Table

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

Part 2 Coming Next

In the next article, we'll cover the remaining structural patterns:

  • Facade — Simplify complex subsystems
  • Flyweight — Share data to save memory
  • Proxy — Control access to objects

Each with the same production-focused approach: real code, real problems, and when to actually use them.


What's Next in the Design Patterns Series?

  • Part 1 (This Article): Adapter, Bridge, Composite, Decorator
  • Part 2: Facade, Flyweight, Proxy
  • Behavioral Patterns (Coming Soon): Strategy, Observer, Command, State, Template Method, Chain of Responsibility, and more

Want More Deep Dives?

Mahdi Shamlo

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)