Part 01 · Low Level Design
Structural Patterns: Adapter, Decorator, Proxy
Make a foreign API fit, add behaviour without subclassing, put a checking or caching stand-in in front, give a messy subsystem one door, and treat a tree like a leaf.
⏱ 2-3 hours · Station 9 of 80
The vendor changed, and so did everything else
An e-commerce app sends OTPs through an SMS company's SDK. The SDK's method is Dispatch(numbers []string, text, senderID) int, and that call is used directly in 12 places.
Then the company switches to a cheaper SMS vendor with a different SDK: send_message(to, body, options) -> Response. All 12 places change. A month later someone adds retries for flaky networks by copying a for loop into 9 of them. Then someone notices the currency-rate API is called on every page view and costs money per call, and adds a cache in three different ways.
The app didn't need new behaviour as much as it needed better wrapping: a way to fit, extend or guard an object without touching the code that uses it. That's what structural patterns are about.
The problem: changing how objects fit together
| Situation | Pattern |
|---|---|
| A third-party API has the wrong shape for your code | Adapter |
| You want to add retries, logging or timing around something, without editing it | Decorator |
| You want to cache, control access to, or delay creating something expensive | Proxy |
| A subsystem is messy and callers need one simple entry point | Facade |
| Single items and groups of items should be treated the same | Composite |
Think of plugs, phone covers and a receptionist
Your laptop charger has a UK plug and the wall has a Bangladeshi socket. You don't rewire the laptop; you use a travel adapter (Adapter).
A phone cover adds grip and protection, and the phone still works exactly the same inside it. You can even put a card holder over the cover. That's a decorator: same shape outside, more behaviour, and they stack.
At a busy office, the receptionist answers common questions directly and only calls the manager when needed. Callers can't tell whether they spoke to the manager or not. That's a proxy.
Where the comparison breaks down. Physical adapters cost almost nothing. Every software wrapper adds a layer to read and debug, so add one when it removes real duplication or isolates a real dependency, not by habit.
Notifications go through Retry{inner: BulkSMSAdapter}. The company switches SMS vendors again. What changes?
Five ways to fit objects together
- Adapter: wraps an object with the wrong interface and presents the interface your code expects. It translates calls (
Send(to, msg)→Dispatch([]string{to}, msg, senderID)) and errors. Put one at every boundary with code you don't own: vendor SDKs, legacy modules, other teams' APIs. - Decorator: wraps an object with the same interface and adds behaviour before or after delegating: retries, logging, metrics, caching, rate limiting. Decorators stack (
Logging{Retry{Adapter}}), and the inner object doesn't change. - Proxy: also has the same interface, but its job is to control access to the real object: cache results, check permissions, create the real object lazily, or stand in for a remote one. The line between proxy and decorator is about intent: a decorator adds features; a proxy guards or substitutes.
- Facade: one simple object in front of a messy subsystem.
Checkout.PlaceOrder(cart)hides stock checks, payment, invoice creation and notifications. Callers get one door; the subsystem can change behind it. - Composite: a tree where single items and groups share one interface. An
Itemhas a price; aComboof items (and other combos) also has a price, computed by asking its parts. Callers don't care which they hold. File systems, org charts and menus are composites. - All five rely on interfaces and composition. None of them need inheritance; each wraps or holds other objects.
A proxy saving slow calls
rates.Rate(USD) twice, then Rate(EUR)
Step 1/5 The caller asks a RateService
It holds a RateService. It doesn't know it's a proxy.
- ask
- USD
Same outside, different inside
All the wrappers in this chapter look the same from outside, so ask what the wrapper is for:
- Different shape outside than inside? → Adapter.
- Same shape, extra behaviour? → Decorator.
- Same shape, guards or replaces access? → Proxy.
- One simple shape over many objects? → Facade.
- The same shape for one thing and for a group of things? → Composite.
Try it: which structural pattern?
Name the pattern
type SSLCommerzAdapter struct{ sdk *sslc.Client }Adapters, decorators, proxies and combos in code
An adapter for a vendor SDK, a retry decorator around any notifier, a caching proxy in front of a slow rate service, and a composite menu where combos contain items and other combos.
package main
import (
"errors"
"fmt"
)
// Our own interface: what the rest of the app expects from any notifier.
type Notifier interface {
Send(to, msg string) error
}
// ---- Adapter: a third-party SMS API with a different shape.
type BulkSMS struct{} // pretend this comes from a vendor's SDK
func (BulkSMS) Dispatch(numbers []string, text, senderID string) int {
fmt.Printf("adapter: bulksms dispatched to %s from %s\n", numbers[0], senderID)
return 200
}
// BulkSMSAdapter makes BulkSMS look like a Notifier, so nothing else has to change.
type BulkSMSAdapter struct{ api BulkSMS }
func (a BulkSMSAdapter) Send(to, msg string) error {
if code := a.api.Dispatch([]string{to}, msg, "CodingFoding"); code != 200 {
return fmt.Errorf("bulksms code %d", code)
}
return nil
}
// ---- Decorator: adds retries around ANY Notifier, without changing it.
type Retry struct {
Inner Notifier
Attempts int
}
func (r Retry) Send(to, msg string) error {
var err error
for i := 1; i <= r.Attempts; i++ {
if err = r.Inner.Send(to, msg); err == nil {
fmt.Printf("decorator: attempt %d ok\n", i)
return nil
}
fmt.Printf("decorator: attempt %d failed: %v\n", i, err)
}
return err
}
// flaky fails the first time, then works (to show the retry).
type flaky struct{ calls *int }
func (f flaky) Send(to, msg string) error {
*f.calls++
if *f.calls == 1 {
return errors.New("timeout")
}
return nil
}
// ---- Proxy: same interface as the real service, but remembers answers.
type RateService interface{ Rate(currency string) int }
type SlowRates struct{ Calls int }
func (s *SlowRates) Rate(c string) int {
s.Calls++ // imagine a 2-second network call here
return map[string]int{"USD": 122, "EUR": 131}[c]
}
type CachedRates struct {
real RateService
cache map[string]int
}
func (p *CachedRates) Rate(c string) int {
if r, ok := p.cache[c]; ok {
return r
}
r := p.real.Rate(c)
p.cache[c] = r
return r
}
// ---- Composite: a single item and a combo of items share one interface.
type MenuPart interface{ Price() int }
type Item struct {
Name string
Tk int
}
func (i Item) Price() int { return i.Tk }
type Combo struct {
Name string
Parts []MenuPart // items or other combos
}
func (c Combo) Price() int {
total := 0
for _, p := range c.Parts {
total += p.Price()
}
return total
}
func main() {
var n Notifier = BulkSMSAdapter{}
_ = n.Send("01711000000", "Your OTP is 4821")
calls := 0
_ = Retry{Inner: flaky{&calls}, Attempts: 3}.Send("01711000000", "Your order is on the way")
slow := &SlowRates{}
var rates RateService = &CachedRates{real: slow, cache: map[string]int{}}
for _, c := range []string{"USD", "USD", "EUR"} {
r := rates.Rate(c)
fmt.Printf("proxy: %s rate %d (calls to slow service: %d)\n", c, r, slow.Calls)
}
drinks := Combo{"Drinks", []MenuPart{Item{"Coke", 60}, Item{"Coke", 60}}}
family := Combo{"Family Combo", []MenuPart{Item{"Biryani", 450}, Item{"Biryani", 450}, drinks, Item{"Firni", 150}}}
for _, c := range []Combo{family, drinks} {
fmt.Printf("composite: %s = %d tk\n", c.Name, c.Price())
}
}from __future__ import annotations
from typing import Protocol
class Notifier(Protocol):
"""Our own interface: what the rest of the app expects from any notifier."""
def send(self, to: str, msg: str) -> None: ...
# ---- Adapter: a third-party SMS API with a different shape.
class BulkSMS: # pretend this comes from a vendor's SDK
def dispatch(self, numbers: list[str], text: str, sender_id: str) -> int:
print(f"adapter: bulksms dispatched to {numbers[0]} from {sender_id}")
return 200
class BulkSMSAdapter:
"""Makes BulkSMS look like a Notifier, so nothing else has to change."""
def __init__(self, api: BulkSMS):
self.api = api
def send(self, to: str, msg: str) -> None:
code = self.api.dispatch([to], msg, "CodingFoding")
if code != 200:
raise RuntimeError(f"bulksms code {code}")
# ---- Decorator: adds retries around ANY Notifier, without changing it.
class Retry:
def __init__(self, inner: Notifier, attempts: int):
self.inner, self.attempts = inner, attempts
def send(self, to: str, msg: str) -> None:
for i in range(1, self.attempts + 1):
try:
self.inner.send(to, msg)
print(f"decorator: attempt {i} ok")
return
except RuntimeError as e:
print(f"decorator: attempt {i} failed: {e}")
if i == self.attempts:
raise
class Flaky:
"""Fails the first time, then works (to show the retry)."""
def __init__(self) -> None:
self.calls = 0
def send(self, to: str, msg: str) -> None:
self.calls += 1
if self.calls == 1:
raise RuntimeError("timeout")
# ---- Proxy: same interface as the real service, but remembers answers.
class SlowRates:
def __init__(self) -> None:
self.calls = 0
def rate(self, currency: str) -> int:
self.calls += 1 # imagine a 2-second network call here
return {"USD": 122, "EUR": 131}[currency]
class CachedRates:
def __init__(self, real: SlowRates):
self.real = real
self.cache: dict[str, int] = {}
def rate(self, currency: str) -> int:
if currency not in self.cache:
self.cache[currency] = self.real.rate(currency)
return self.cache[currency]
# ---- Composite: a single item and a combo of items share one interface.
class MenuPart(Protocol):
def price(self) -> int: ...
class Item:
def __init__(self, name: str, tk: int):
self.name, self.tk = name, tk
def price(self) -> int:
return self.tk
class Combo:
def __init__(self, name: str, parts: list[MenuPart]):
self.name, self.parts = name, parts # items or other combos
def price(self) -> int:
return sum(p.price() for p in self.parts)
n: Notifier = BulkSMSAdapter(BulkSMS())
n.send("01711000000", "Your OTP is 4821")
Retry(Flaky(), 3).send("01711000000", "Your order is on the way")
slow = SlowRates()
rates = CachedRates(slow)
for c in ("USD", "USD", "EUR"):
r = rates.rate(c)
print(f"proxy: {c} rate {r} (calls to slow service: {slow.calls})")
drinks = Combo("Drinks", [Item("Coke", 60), Item("Coke", 60)])
family = Combo("Family Combo", [Item("Biryani", 450), Item("Biryani", 450), drinks, Item("Firni", 150)])
for combo in (family, drinks):
print(f"composite: {combo.name} = {combo.price()} tk")// Our own interface: what the rest of the app expects from any notifier.
interface Notifier {
send(to: string, msg: string): void;
}
// ---- Adapter: a third-party SMS API with a different shape.
class BulkSMS {
// pretend this comes from a vendor's SDK
dispatch(numbers: string[], _text: string, senderId: string): number {
console.log(`adapter: bulksms dispatched to ${numbers[0]} from ${senderId}`);
return 200;
}
}
// Makes BulkSMS look like a Notifier, so nothing else has to change.
class BulkSMSAdapter implements Notifier {
readonly api: BulkSMS;
constructor(api: BulkSMS) {
this.api = api;
}
send(to: string, msg: string): void {
const code = this.api.dispatch([to], msg, "CodingFoding");
if (code !== 200) throw new Error(`bulksms code ${code}`);
}
}
// ---- Decorator: adds retries around ANY Notifier, without changing it.
class Retry implements Notifier {
readonly inner: Notifier;
readonly attempts: number;
constructor(inner: Notifier, attempts: number) {
this.inner = inner;
this.attempts = attempts;
}
send(to: string, msg: string): void {
for (let i = 1; i <= this.attempts; i++) {
try {
this.inner.send(to, msg);
console.log(`decorator: attempt ${i} ok`);
return;
} catch (e) {
console.log(`decorator: attempt ${i} failed: ${(e as Error).message}`);
if (i === this.attempts) throw e;
}
}
}
}
// Fails the first time, then works (to show the retry).
class Flaky implements Notifier {
calls = 0;
send(): void {
this.calls++;
if (this.calls === 1) throw new Error("timeout");
}
}
// ---- Proxy: same interface as the real service, but remembers answers.
interface RateService {
rate(currency: string): number;
}
class SlowRates implements RateService {
calls = 0;
rate(currency: string): number {
this.calls++; // imagine a 2-second network call here
return ({ USD: 122, EUR: 131 } as Record<string, number>)[currency]!;
}
}
class CachedRates implements RateService {
readonly real: RateService;
readonly cache = new Map<string, number>();
constructor(real: RateService) {
this.real = real;
}
rate(currency: string): number {
if (!this.cache.has(currency)) this.cache.set(currency, this.real.rate(currency));
return this.cache.get(currency)!;
}
}
// ---- Composite: a single item and a combo of items share one interface.
interface MenuPart {
price(): number;
}
class Item implements MenuPart {
readonly name: string;
readonly tk: number;
constructor(name: string, tk: number) {
this.name = name;
this.tk = tk;
}
price(): number {
return this.tk;
}
}
class Combo implements MenuPart {
readonly name: string;
readonly parts: MenuPart[]; // items or other combos
constructor(name: string, parts: MenuPart[]) {
this.name = name;
this.parts = parts;
}
price(): number {
return this.parts.reduce((sum, p) => sum + p.price(), 0);
}
}
const n: Notifier = new BulkSMSAdapter(new BulkSMS());
n.send("01711000000", "Your OTP is 4821");
new Retry(new Flaky(), 3).send("01711000000", "Your order is on the way");
const slow = new SlowRates();
const rates: RateService = new CachedRates(slow);
for (const c of ["USD", "USD", "EUR"]) {
const r = rates.rate(c);
console.log(`proxy: ${c} rate ${r} (calls to slow service: ${slow.calls})`);
}
const drinks = new Combo("Drinks", [new Item("Coke", 60), new Item("Coke", 60)]);
const family = new Combo("Family Combo", [new Item("Biryani", 450), new Item("Biryani", 450), drinks, new Item("Firni", 150)]);
for (const combo of [family, drinks]) console.log(`composite: ${combo.name} = ${combo.price()} tk`);use std::cell::Cell;
use std::collections::HashMap;
// Our own interface: what the rest of the app expects from any notifier.
trait Notifier { fn send(&self, to: &str, msg: &str) -> Result<(), String>; }
// ---- Adapter: a third-party SMS API with a different shape.
struct BulkSms; // pretend this comes from a vendor's SDK
impl BulkSms {
fn dispatch(&self, numbers: &[&str], _text: &str, sender_id: &str) -> u16 {
println!("adapter: bulksms dispatched to {} from {sender_id}", numbers[0]);
200
}
}
// Makes BulkSms look like a Notifier, so nothing else has to change.
struct BulkSmsAdapter { api: BulkSms }
impl Notifier for BulkSmsAdapter {
fn send(&self, to: &str, msg: &str) -> Result<(), String> {
match self.api.dispatch(&[to], msg, "CodingFoding") {
200 => Ok(()),
code => Err(format!("bulksms code {code}")),
}
}
}
// ---- Decorator: adds retries around ANY Notifier, without changing it.
struct Retry<N: Notifier> { inner: N, attempts: u32 }
impl<N: Notifier> Notifier for Retry<N> {
fn send(&self, to: &str, msg: &str) -> Result<(), String> {
let mut last = Err(String::new());
for i in 1..=self.attempts {
last = self.inner.send(to, msg);
match &last {
Ok(()) => { println!("decorator: attempt {i} ok"); return Ok(()); }
Err(e) => println!("decorator: attempt {i} failed: {e}"),
}
}
last
}
}
// Fails the first time, then works (to show the retry).
struct Flaky { calls: Cell<u32> }
impl Notifier for Flaky {
fn send(&self, _: &str, _: &str) -> Result<(), String> {
self.calls.set(self.calls.get() + 1);
if self.calls.get() == 1 { Err("timeout".into()) } else { Ok(()) }
}
}
// ---- Proxy: same interface as the real service, but remembers answers.
trait RateService { fn rate(&mut self, currency: &str) -> u32; }
struct SlowRates { calls: u32 }
impl RateService for SlowRates {
fn rate(&mut self, c: &str) -> u32 {
self.calls += 1; // imagine a 2-second network call here
match c { "USD" => 122, "EUR" => 131, _ => 0 }
}
}
struct CachedRates<R: RateService> { real: R, cache: HashMap<String, u32> }
impl<R: RateService> RateService for CachedRates<R> {
fn rate(&mut self, c: &str) -> u32 {
if let Some(&r) = self.cache.get(c) { return r; }
let r = self.real.rate(c);
self.cache.insert(c.to_string(), r);
r
}
}
// ---- Composite: a single item and a combo of items share one interface.
trait MenuPart { fn price(&self) -> u32; }
struct Item { _name: &'static str, tk: u32 }
impl MenuPart for Item { fn price(&self) -> u32 { self.tk } }
struct Combo { name: &'static str, parts: Vec<Box<dyn MenuPart>> } // items or other combos
impl MenuPart for Combo { fn price(&self) -> u32 { self.parts.iter().map(|p| p.price()).sum() } }
fn main() {
let n: Box<dyn Notifier> = Box::new(BulkSmsAdapter { api: BulkSms });
let _ = n.send("01711000000", "Your OTP is 4821");
let _ = Retry { inner: Flaky { calls: Cell::new(0) }, attempts: 3 }.send("01711000000", "Your order is on the way");
let mut rates = CachedRates { real: SlowRates { calls: 0 }, cache: HashMap::new() };
for c in ["USD", "USD", "EUR"] {
let r = rates.rate(c);
println!("proxy: {c} rate {r} (calls to slow service: {})", rates.real.calls);
}
let drinks = || Combo { name: "Drinks", parts: vec![Box::new(Item { _name: "Coke", tk: 60 }), Box::new(Item { _name: "Coke", tk: 60 })] };
let family = Combo { name: "Family Combo", parts: vec![
Box::new(Item { _name: "Biryani", tk: 450 }), Box::new(Item { _name: "Biryani", tk: 450 }),
Box::new(drinks()), Box::new(Item { _name: "Firni", tk: 150 }),
] };
for c in [&family, &drinks()] { println!("composite: {} = {} tk", c.name, c.price()); }
}import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
// Our own interface: what the rest of the app expects from any notifier.
interface Notifier { void send(String to, String msg); }
// ---- Adapter: a third-party SMS API with a different shape.
static final class BulkSms { // pretend this comes from a vendor's SDK
int dispatch(List<String> numbers, String text, String senderId) {
System.out.printf("adapter: bulksms dispatched to %s from %s%n", numbers.get(0), senderId);
return 200;
}
}
// Makes BulkSms look like a Notifier, so nothing else has to change.
record BulkSmsAdapter(BulkSms api) implements Notifier {
public void send(String to, String msg) {
int code = api.dispatch(List.of(to), msg, "CodingFoding");
if (code != 200) throw new IllegalStateException("bulksms code " + code);
}
}
// ---- Decorator: adds retries around ANY Notifier, without changing it.
record Retry(Notifier inner, int attempts) implements Notifier {
public void send(String to, String msg) {
for (int i = 1; i <= attempts; i++) {
try {
inner.send(to, msg);
System.out.printf("decorator: attempt %d ok%n", i);
return;
} catch (IllegalStateException e) {
System.out.printf("decorator: attempt %d failed: %s%n", i, e.getMessage());
if (i == attempts) throw e;
}
}
}
}
// Fails the first time, then works (to show the retry).
static final class Flaky implements Notifier {
private int calls = 0;
public void send(String to, String msg) {
if (++calls == 1) throw new IllegalStateException("timeout");
}
}
// ---- Proxy: same interface as the real service, but remembers answers.
interface RateService { int rate(String currency); }
static final class SlowRates implements RateService {
int calls = 0;
public int rate(String c) {
calls++; // imagine a 2-second network call here
return Map.of("USD", 122, "EUR", 131).get(c);
}
}
static final class CachedRates implements RateService {
private final RateService real;
private final Map<String, Integer> cache = new HashMap<>();
CachedRates(RateService real) { this.real = real; }
public int rate(String c) { return cache.computeIfAbsent(c, real::rate); }
}
// ---- Composite: a single item and a combo of items share one interface.
interface MenuPart { int price(); }
record Item(String name, int tk) implements MenuPart {
public int price() { return tk; }
}
record Combo(String name, List<MenuPart> parts) implements MenuPart { // items or other combos
public int price() { return parts.stream().mapToInt(MenuPart::price).sum(); }
}
public static void main(String[] args) {
Notifier n = new BulkSmsAdapter(new BulkSms());
n.send("01711000000", "Your OTP is 4821");
new Retry(new Flaky(), 3).send("01711000000", "Your order is on the way");
SlowRates slow = new SlowRates();
RateService rates = new CachedRates(slow);
for (String c : List.of("USD", "USD", "EUR")) {
int r = rates.rate(c);
System.out.printf("proxy: %s rate %d (calls to slow service: %d)%n", c, r, slow.calls);
}
Combo drinks = new Combo("Drinks", List.of(new Item("Coke", 60), new Item("Coke", 60)));
Combo family = new Combo("Family Combo", List.of(new Item("Biryani", 450), new Item("Biryani", 450), drinks, new Item("Firni", 150)));
for (Combo c : List.of(family, drinks)) System.out.printf("composite: %s = %d tk%n", c.name(), c.price());
}
}#include <cstdio>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
// Our own interface: what the rest of the app expects from any notifier.
class Notifier {
public:
virtual ~Notifier() = default;
virtual void send(const std::string& to, const std::string& msg) = 0;
};
// ---- Adapter: a third-party SMS API with a different shape.
class BulkSms { // pretend this comes from a vendor's SDK
public:
int dispatch(const std::vector<std::string>& numbers, const std::string&, const std::string& senderId) {
std::printf("adapter: bulksms dispatched to %s from %s\n", numbers[0].c_str(), senderId.c_str());
return 200;
}
};
// Makes BulkSms look like a Notifier, so nothing else has to change.
class BulkSmsAdapter : public Notifier {
public:
void send(const std::string& to, const std::string& msg) override {
int code = api_.dispatch({to}, msg, "CodingFoding");
if (code != 200) throw std::runtime_error("bulksms code " + std::to_string(code));
}
private:
BulkSms api_;
};
// ---- Decorator: adds retries around ANY Notifier, without changing it.
class Retry : public Notifier {
public:
Retry(Notifier& inner, int attempts) : inner_(inner), attempts_(attempts) {}
void send(const std::string& to, const std::string& msg) override {
for (int i = 1; i <= attempts_; i++) {
try {
inner_.send(to, msg);
std::printf("decorator: attempt %d ok\n", i);
return;
} catch (const std::runtime_error& e) {
std::printf("decorator: attempt %d failed: %s\n", i, e.what());
if (i == attempts_) throw;
}
}
}
private:
Notifier& inner_;
int attempts_;
};
// Fails the first time, then works (to show the retry).
class Flaky : public Notifier {
public:
void send(const std::string&, const std::string&) override {
if (++calls_ == 1) throw std::runtime_error("timeout");
}
private:
int calls_ = 0;
};
// ---- Proxy: same interface as the real service, but remembers answers.
class RateService {
public:
virtual ~RateService() = default;
virtual int rate(const std::string& currency) = 0;
};
class SlowRates : public RateService {
public:
int calls = 0;
int rate(const std::string& c) override {
calls++; // imagine a 2-second network call here
return c == "USD" ? 122 : c == "EUR" ? 131 : 0;
}
};
class CachedRates : public RateService {
public:
explicit CachedRates(RateService& real) : real_(real) {}
int rate(const std::string& c) override {
auto it = cache_.find(c);
if (it != cache_.end()) return it->second;
return cache_[c] = real_.rate(c);
}
private:
RateService& real_;
std::map<std::string, int> cache_;
};
// ---- Composite: a single item and a combo of items share one interface.
class MenuPart {
public:
virtual ~MenuPart() = default;
virtual int price() const = 0;
};
class Item : public MenuPart {
public:
Item(std::string name, int tk) : name_(std::move(name)), tk_(tk) {}
int price() const override { return tk_; }
private:
std::string name_;
int tk_;
};
class Combo : public MenuPart {
public:
explicit Combo(std::string name) : name(std::move(name)) {}
Combo& add(std::shared_ptr<MenuPart> p) { parts_.push_back(std::move(p)); return *this; } // items or other combos
int price() const override {
int total = 0;
for (const auto& p : parts_) total += p->price();
return total;
}
const std::string name;
private:
std::vector<std::shared_ptr<MenuPart>> parts_;
};
int main() {
BulkSmsAdapter sms;
Notifier& n = sms;
n.send("01711000000", "Your OTP is 4821");
Flaky flaky;
Retry(flaky, 3).send("01711000000", "Your order is on the way");
SlowRates slow;
CachedRates rates(slow);
for (std::string c : {"USD", "USD", "EUR"}) {
int r = rates.rate(c);
std::printf("proxy: %s rate %d (calls to slow service: %d)\n", c.c_str(), r, slow.calls);
}
auto drinks = std::make_shared<Combo>("Drinks");
drinks->add(std::make_shared<Item>("Coke", 60)).add(std::make_shared<Item>("Coke", 60));
auto family = std::make_shared<Combo>("Family Combo");
family->add(std::make_shared<Item>("Biryani", 450)).add(std::make_shared<Item>("Biryani", 450)).add(drinks);
family->add(std::make_shared<Item>("Firni", 150));
for (const auto& c : {family, drinks}) std::printf("composite: %s = %d tk\n", c->name.c_str(), c->price());
}All six print exactly this:
adapter: bulksms dispatched to 01711000000 from CodingFoding
decorator: attempt 1 failed: timeout
decorator: attempt 2 ok
proxy: USD rate 122 (calls to slow service: 1)
proxy: USD rate 122 (calls to slow service: 1)
proxy: EUR rate 131 (calls to slow service: 2)
composite: Family Combo = 1170 tk
composite: Drinks = 120 tk
Reading the output:
- adapter: the caller used
Notifier.Send; the adapter turned it into the vendor'sDispatchwith a slice of numbers and a sender ID. - decorator: the first attempt timed out and Retry tried again. The flaky sender has no retry code of its own, and Retry would work around the adapter, a mock, or anything else that implements
Notifier. - proxy: the slow service was called once for USD even though USD was asked twice, and once for EUR. The caller never knew a cache existed.
- composite: Family Combo = 450 + 450 + (60 + 60) + 150 = 1170. The Drinks combo inside it is just another
MenuPart. - What each language changed: Rust's decorator and proxy are generic over the inner type (
Retry<N: Notifier>), so there's no virtual call;FlakyusesCellto count calls through a shared reference. Java'scomputeIfAbsent(c, real::rate)is a one-line caching proxy. C++ shares combo parts withshared_ptr, because the same Drinks combo appears inside Family Combo and on its own. Python and TypeScript need no interface declarations for the decorator: anything withsendfits.
Wrappers in real code
- Adapt at the boundary, once. Every vendor SDK, payment gateway or legacy API gets one adapter that speaks your interface. Vendor types never leak into business code, so switching vendors is a one-file change.
- Keep decorators small and ordered on purpose.
Metrics{Retry{Timeout{Adapter}}}means: measure everything, retry the whole timed call, time out each attempt. Order changes behaviour; write it in one place (usuallymain). - Caching proxies need an invalidation story. TTLs, size limits and what happens when the real service is down. A cache that never expires serves yesterday's exchange rate forever.
- Facades are great for onboarding and for tests, but don't let one grow into a god object that also holds business rules. It should coordinate, not decide.
- Watch the depth. Five wrappers around one call make stack traces long and debugging slow. Wrap where it pays.
Designs that cause problems later
Pick the wrapper that will hurt
func Notify(resp *bulksms.Response) ...
Pause and think
You want both logging and retries around the SMS adapter. Should the logger wrap the retry, or the retry wrap the logger?
Reveal
It depends on what you want to see. Logging{Retry{Adapter}} logs one line per send, with the final outcome: good for business logs. Retry{Logging{Adapter}} logs one line per attempt, including the failures: good for debugging flaky vendors. Both are correct designs; the point is that decorators let you choose by changing one line of wiring, with no change to the adapter, the retry or the logger.
Quick quiz
1. What's the difference between an Adapter and a Decorator?
Answer
An adapter changes the interface (wrong shape in, right shape out). A decorator keeps the same interface and adds behaviour around the call.
2. Why put an adapter around every third-party SDK?
Answer
So vendor types and quirks stay in one file. Business code depends on your interface, and switching vendors changes only the adapter.
3. How is a Proxy different from a Decorator, if both keep the same interface?
Answer
By purpose: a decorator adds features (logging, retries); a proxy controls access to the real object (caching, permission checks, lazy creation, remote stand-in).
4. What makes Composite useful for menus or file systems?
Answer
Items and groups share one interface, so callers can ask any node for its price or size without checking whether it's a leaf or a group; groups simply ask their children.
Your challenge: a logging decorator and a bigger combo
- Write a Logging decorator that prints
log: send to <to>before delegating. Wrap it outside the retry:Logging{Retry{flaky}}. - Add a Party Pack combo that contains two Family Combos.
Before you run it, predict the new lines.
You've succeeded when the retry section starts with log: send to 01711000000 (printed once, before both attempts), a new line composite: Party Pack = 2340 tk appears, and neither Retry, Flaky nor Combo was edited.
In one line
Wrap instead of rewriting: an adapter gives a foreign object your interface, a decorator adds behaviour with the same interface and stacks, a proxy guards or caches access to the real thing, a facade gives a messy subsystem one door, and a composite lets one item and a group of items answer the same question.
Station stamp
Finish the chapter
Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.
Go Pro to earn XP →