Part 01 · Low Level Design

Structural Patterns: Adapter, Decorator, Proxy

AdapterDecoratorProxyFacadeComposite

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

SituationPattern
A third-party API has the wrong shape for your codeAdapter
You want to add retries, logging or timing around something, without editing itDecorator
You want to cache, control access to, or delay creating something expensiveProxy
A subsystem is messy and callers need one simple entry pointFacade
Single items and groups of items should be treated the sameComposite

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.

checkoutSend(to, msg)RetrydecoratorBulkSMSAdapteradaptervendor SDKDispatch([]num, ...)try 1timeouttry 2200 OKeach wrapper has the same Send() shape, so checkout never knows they're there
Press play. Checkout calls Send. The call goes through the Retry decorator and the adapter to the vendor's SDK, which has a different shape. The first try times out; Retry sends again and it gets through. Checkout never knows the wrappers are there.

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.

Predict first

Notifications go through Retry{inner: BulkSMSAdapter}. The company switches SMS vendors again. What changes?

Five ways to fit objects together

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Composite: a tree where single items and groups share one interface. An Item has a price; a Combo of 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.
  6. All five rely on interfaces and composition. None of them need inheritance; each wraps or holds other objects.

A proxy saving slow calls

Step through it

rates.Rate(USD) twice, then Rate(EUR)

CallerCachedRates proxyCacheSlowRates

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?

Try each case

Name the pattern

type SSLCommerzAdapter struct{ sdk *sslc.Client }
works: Adapter. Translates the vendor's shape into yours.

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())
	}
}

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's Dispatch with 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; Flaky uses Cell to count calls through a shared reference. Java's computeIfAbsent(c, real::rate) is a one-line caching proxy. C++ shares combo parts with shared_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 with send fits.

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 (usually main).
  • 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

Try each case

Pick the wrapper that will hurt

func Notify(resp *bulksms.Response) ...
breaks: No adapter boundary: the next vendor switch touches every file that mentions bulksms. Convert to your own types inside the adapter.

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 →