Structural Patterns: Adapter, Decorator & Facade
Adapter, Decorator, Facade, Proxy and Composite patterns explained with practical backend examples.
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:
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:
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.
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:
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:
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:
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.
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:
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.
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:
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.
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:
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.