Design Patterns Lesson 4 of 6 ⏱️ 7 min read

Structural Patterns: Adapter, Decorator & Facade

Adapter, Decorator, Facade, Proxy and Composite patterns explained with practical backend examples.

Updated Oct 30, 2026

Why structural patterns?

Structural patterns describe how to compose classes and objects into larger structures while keeping them flexible. They're everywhere in backend code: wrapping third-party SDKs, adding logging or caching around services, and hiding complex subsystems behind simple APIs.

Pattern In one sentence Backend example
Adapter Makes an incompatible interface fit the one you expect Wrapping a legacy SMS SDK
Decorator Adds behaviour by wrapping an object with the same interface Caching/logging around a repository; middleware
Facade Gives a simple front door to a complex subsystem checkout() hiding inventory, payment and email
Proxy Stands in for another object to control access Lazy loading, access control, remote calls
Composite Treats single objects and groups uniformly Course → modules → lessons trees

Adapter

An adapter converts one interface into another that clients expect — like a travel plug adapter.

Your app expects every SMS provider to have send(phone, text). A legacy provider's SDK looks different:

Python
class LegacySmsClient:
    """Third-party SDK we can't change."""
    def transmit(self, payload: dict) -> int:
        return 202 if payload["msisdn"].startswith("+92") else 400

class SmsSender:
    """The interface our application uses everywhere."""
    def send(self, phone: str, text: str) -> bool: ...

class LegacySmsAdapter(SmsSender):
    def __init__(self, client: LegacySmsClient):
        self.client = client

    def send(self, phone: str, text: str) -> bool:
        status = self.client.transmit({"msisdn": phone, "body": text})    # translate the call
        return status == 202                                              # translate the result

sms: SmsSender = LegacySmsAdapter(LegacySmsClient())
print(sms.send("+923001234567", "Your OTP is 482913"))

Output:

Plaintext
True

The rest of the codebase never touches the SDK, so replacing the provider later means writing one new adapter. This is also called an anti-corruption layer when it protects your domain model from an external system's concepts.

Decorator

A decorator wraps an object that has the same interface and adds behaviour before or after delegating to it. Decorators can be stacked, and each one has a single responsibility.

Python
import time

class CourseRepository:
    def find(self, course_id: int) -> dict:
        time.sleep(0.2)                                  # pretend this is a slow database query
        return {"id": course_id, "title": "Operating Systems"}

class CachingRepository:
    def __init__(self, inner):
        self.inner, self.cache = inner, {}

    def find(self, course_id):
        if course_id not in self.cache:
            self.cache[course_id] = self.inner.find(course_id)
        return self.cache[course_id]

class LoggingRepository:
    def __init__(self, inner):
        self.inner = inner

    def find(self, course_id):
        start = time.perf_counter()
        result = self.inner.find(course_id)
        print(f"find({course_id}) took {time.perf_counter() - start:.1f} s")
        return result

repo = LoggingRepository(CachingRepository(CourseRepository()))   # stack the decorators
repo.find(7)
repo.find(7)

Output:

Plaintext
find(7) took 0.2 s
find(7) took 0.0 s

CourseRepository knows nothing about caching or logging. HTTP middleware in Laravel, Express and Django is the same idea applied to request handlers.

Python's @decorator syntax wraps functions rather than objects, but serves the same purpose:

Python
import functools

def retry(times: int):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except ConnectionError:
                    print(f"attempt {attempt} failed")
            raise ConnectionError(f"gave up after {times} attempts")
        return wrapper
    return decorator

calls = {"n": 0}

@retry(times=3)
def call_payment_api():
    calls["n"] += 1
    if calls["n"] < 3:
        raise ConnectionError
    return "payment ok"

print(call_payment_api())

Output:

Plaintext
attempt 1 failed
attempt 2 failed
payment ok

Facade

A facade provides one simple method in front of a complex subsystem. Callers don't need to know the order of steps or which services are involved.

Python
class Inventory:
    def reserve(self, course_id): return f"seat reserved for course {course_id}"

class Payments:
    def charge(self, student, amount): return f"charged {student} {amount}"

class Mailer:
    def send_receipt(self, student): return f"receipt sent to {student}"

class EnrollmentFacade:
    def __init__(self):
        self.inventory, self.payments, self.mailer = Inventory(), Payments(), Mailer()

    def enroll(self, student: str, course_id: int, fee: int) -> list[str]:
        return [
            self.inventory.reserve(course_id),
            self.payments.charge(student, fee),
            self.mailer.send_receipt(student),
        ]

for step in EnrollmentFacade().enroll("ayesha", 7, 5000):
    print(step)

Output:

Plaintext
seat reserved for course 7
charged ayesha 5000
receipt sent to ayesha

Controllers call EnrollmentFacade().enroll(...) — one line — and the workflow lives in one place. (Laravel's "facades" such as Cache::get() are a related idea: a static-looking front door to a service in the container.)

Proxy

A proxy has the same interface as the real object but controls access to it: creating it lazily, checking permissions, caching, or forwarding calls over the network.

Python
class VideoFile:
    def __init__(self, path):
        print(f"loading {path} from storage…")      # expensive
        self.path = path

    def play(self):
        return f"playing {self.path}"

class LazyVideoProxy:
    def __init__(self, path):
        self.path, self._video = path, None

    def play(self):
        if self._video is None:                    # load only when actually needed
            self._video = VideoFile(self.path)
        return self._video.play()

lessons = [LazyVideoProxy(f"lesson-{i}.mp4") for i in range(1, 4)]   # nothing loaded yet
print(lessons[1].play())
print(lessons[1].play())

Output:

Plaintext
loading lesson-2.mp4 from storage…
playing lesson-2.mp4
playing lesson-2.mp4

ORMs use proxies for lazy-loaded relationships, and RPC/gRPC client stubs are remote proxies. Decorator and proxy look similar in code; the difference is intent — decorators add behaviour, proxies control access.

Composite

A composite lets you treat individual objects and groups of objects the same way, forming a tree.

Python
class Lesson:
    def __init__(self, title, minutes):
        self.title, self.minutes = title, minutes

    def duration(self):
        return self.minutes

class Module:
    def __init__(self, title, children):
        self.title, self.children = title, children

    def duration(self):                              # same method, sums its children
        return sum(child.duration() for child in self.children)

course = Module("Operating Systems", [
    Module("Processes", [Lesson("Threads", 20), Lesson("Scheduling", 35)]),
    Module("Memory", [Lesson("Paging", 30)]),
    Lesson("Interview questions", 25),
])
print("total minutes:", course.duration())

Output:

Plaintext
total minutes: 110

File systems (files and folders), UI component trees and organisation charts all follow this pattern.

Common mistakes

  • Adapters that leak the third-party types into your domain.
  • Decorators that change the interface — then they're not interchangeable.
  • Facades that grow into god objects — keep them thin and delegate.
  • Confusing proxy and decorator — ask whether you're adding behaviour or controlling access.

Key takeaways

  • Adapter converts interfaces; Facade simplifies a subsystem.
  • Decorator stacks behaviour around an object with the same interface (like middleware).
  • Proxy controls access — lazy loading, permissions, remote calls.
  • Composite treats leaves and groups uniformly in tree structures.
Finished this lesson?
Tick it off to track your Design Patterns progress.
Next lesson