System Design Road

LLD concepts · part 5 of 7

Structural patterns: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Creational patterns decide how objects are made. Structural patterns decide how they fit together once they exist. Most of them look alike on a class diagram: one object holds another and forwards calls to it. What differs is why, and that’s what interviewers listen for. This chapter builds all seven from real problems, with code that compiles and runs, and ends with a way to tell the look-alikes apart.

11 min read7 diagrams10 sections

The short version

  • Structural patterns are about how objects are put together: wrapped, combined, shared or hidden behind a simpler face.
  • Adapter makes an existing class fit the interface you need, and is the right place to fix units, names and error codes.
  • Decorator adds behaviour by wrapping an object in another with the same interface. Wrappers stack, and their order changes what happens.
  • Facade gives a group of classes one simple front door. Composite lets a group be treated like a single item. Proxy stands in for an object to control when or whether it’s used.
  • Bridge splits two things that vary independently so M + N classes replace M × N. Flyweight shares the parts of many objects that are the same.

Before we start

Four of the seven patterns here (Adapter, Decorator, Proxy and Facade) are wrappers: one object holds another and passes calls through. If you only look at the diagram, they’re nearly the same. The difference is the intent:

WrapperInterface it showsWhy it wraps
Adaptera different one from the object insideto make it fit
Decoratorthe same oneto add behaviour
Proxythe same oneto control access
Facadea simpler one, over many objectsto hide complexity

Keep that table in mind. The other three (Bridge, Composite, Flyweight) are about structure in a different sense: splitting hierarchies, building trees, and sharing data.

Each pattern below follows the same shape: the problem, the pattern, the code with its real output, mistakes and variations, when not to use it, and where it shows up in the problems in this track.

Adapter: make it fit

The problem

Your app sends notifications through a Notifier interface: send(phone, text), returning true or false. Then the company signs with a new SMS vendor whose library looks nothing like that:

Java
// The vendor's class. You can't change it.
final class BulkSmsVendor {
    int dispatch(String apiKey, String msisdn, String body, int priority) {
        if (!msisdn.startsWith("91") || msisdn.length() != 12) return 4001;  // bad number
        return 0;                                                            // sent
    }
}

Different method name, different argument order, phone numbers as “91” plus ten digits instead of “+91 98…”, and integer status codes instead of true or false. You could change every caller to speak the vendor’s language. Then, when you switch vendors again next year, you’d change every caller again.

The pattern

Write one small class that implements your interface and translates each call into the vendor’s. Callers never learn the vendor exists.

«interface»Notifier+ send(phone, text): booleanVendorSmsAdapter- vendor: BulkSmsVendor- apiKey: String+ send(phone, text): booleanBulkSmsVendor+ dispatch(key, msisdn, body, prio): inttranslates to

The code

Java
interface Notifier {
    boolean send(String phone, String text);
}

final class VendorSmsAdapter implements Notifier {
    private final BulkSmsVendor vendor;
    private final String apiKey;

    VendorSmsAdapter(BulkSmsVendor vendor, String apiKey) {
        this.vendor = vendor;
        this.apiKey = apiKey;
    }

    public boolean send(String phone, String text) {
        String msisdn = phone.replaceAll("[^0-9]", ""); 1
        if (msisdn.length() == 10) msisdn = "91" + msisdn;   //     -> "919876543210"
        int code = vendor.dispatch(apiKey, msisdn, text, 1); // (2)
        return code == 0;                                    // (3)
    }
}

class AdapterDemo {
    public static void main(String[] args) {
        Notifier sms = new VendorSmsAdapter(new BulkSmsVendor(), "key-123");
        System.out.println("+91 98765 43210 -> " + sms.send("+91 98765 43210", "OTP 48"));
        System.out.println("9876543210      -> " + sms.send("9876543210", "OTP 48"));
        System.out.println("12345           -> " + sms.send("12345", "OTP 48"));
    }
}
  1. 1“+91 98765 43210”
  1. Translating data (number formats, units, currencies) is part of the adapter’s job, not something callers should do.
  2. Translating the call: name, argument order, extra arguments with sensible values.
  3. Translating results, including errors. Callers see true or false, never code 4001.
Text
+91 98765 43210 -> true
9876543210      -> true
12345           -> false

Mistakes and variations

  • Leaking the vendor’s types. If send returned the vendor’s status code, every caller would know about the vendor again.
  • Units are the classic bug. Payment gateways disagree on rupees versus paise, seconds versus milliseconds, and UTC versus local time. Convert once, in the adapter, and nowhere else.
  • Adapting in both directions. When a vendor calls you back (webhooks), an adapter turns their payload into your event types too.

When not to use it

When you control both sides. If the “other” class is yours, change it to fit, rather than writing a translator for your own code.

Where it shows up

In the vending machine, the coin acceptor and the motor are hardware behind interfaces; the classes that talk to the real devices are adapters. In the movie booking walkthrough, the class behind PaymentGateway that calls a real provider is an adapter, and it’s where rupees become paise.

Bridge: two things that vary separately

The problem

An operations team sends two kinds of alerts: critical (send now, repeat until someone acknowledges) and digest (collect and send once an hour). Each can go out by SMS, email or Slack. Model that with inheritance and you get a class for every pair:

SMSEmailSlack
CriticalCriticalSmsAlertCriticalEmailAlertCriticalSlackAlert
DigestDigestSmsAlertDigestEmailAlertDigestSlackAlert

Six classes, and each new channel adds two more; each new alert type adds three. And the “how to send an SMS” code is copied into every row.

The pattern

Split the two things that vary into two separate hierarchies, and connect them with a reference: an alert has a sender. Now there are 2 alert classes plus 3 sender classes: 5 instead of 6, and 6 instead of 9 when a third alert type arrives. The reference between the two hierarchies is the “bridge”.

«abstract»Alert# sender: Sender+ raise(msg): ListCriticalAlertDigestAlert«interface»Sender+ deliver(to, text): StringSmsSenderSlackSenderbridge

The code

Java
interface Sender {
    String deliver(String to, String text);
}

final class SmsSender implements Sender {
    public String deliver(String to, String text) { return "SMS " + to + ": " + text; }
}

final class SlackSender implements Sender {
    public String deliver(String to, String text) { return "Slack #" + to + ": " + text; }
}

abstract class Alert {
    protected final Sender sender; 1
    Alert(Sender sender) { this.sender = sender; }
    abstract List raise(String msg);
}

final class CriticalAlert extends Alert {
    CriticalAlert(Sender s) { super(s); }
    List raise(String msg) {                         // (2)
        List out = new ArrayList<>();
        for (int i = 1; i <= 3; i++)
            out.add(sender.deliver("oncall", i + "/3 " + msg));
        return out;
    }
}

final class DigestAlert extends Alert {
    private final List pending = new ArrayList<>();
    DigestAlert(Sender s) { super(s); }
    List raise(String msg) {                         // (3)
        pending.add(msg);
        if (pending.size() < 3) return List.of();
        String text = String.join("; ", pending);
        pending.clear();
        return List.of(sender.deliver("ops", "digest: " + text));
    }
}

class BridgeDemo {
    public static void main(String[] args) {
        System.out.println(new CriticalAlert(new SmsSender()).raise("db down"));
        Alert digest = new DigestAlert(new SlackSender());
        for (String m : List.of("disk 81%", "slow query", "cert expires in 9 days"))
            System.out.println(digest.raise(m));
    }
}
  1. 1the bridge
  1. The alert holds a sender. Any alert can use any sender, chosen when it’s created.
  2. How a critical alert behaves (here, sending three times) is written once, whatever the channel.
  3. Same for digests, which collect messages and send them as one batch.
Text
[SMS oncall: 1/3 db down, SMS oncall: 2/3 db down, SMS oncall: 3/3 db down]
[]
[]
[Slack #ops: digest: disk 81%; slow query; cert expires in 9 days]

Mistakes and variations

  • Confusing it with Adapter. Both hold a reference to another object, but Bridge is designed up front to keep two hierarchies separate; Adapter is added later to make something existing fit.
  • In practice, Bridge often looks just like “composition over inheritance” plus Strategy. That’s fine: the name is useful mainly when you’re explaining why you didn’t make a class for every combination.

When not to use it

When only one thing varies. If every alert goes by SMS, there’s nothing to bridge.

Where it shows up

The notification system in the interview practice track has exactly this shape: message types and delivery channels varying independently.

Composite: treat a group like one item

The problem

A storage app shows the size of every folder. A folder contains files and other folders, which contain more files and folders. Without a pattern, code that totals sizes needs instanceof checks and loops at every level, and so does code that searches, counts or prints.

The pattern

Give files and folders one common interface. A folder implements each method by asking its children and combining the answers. Callers treat a single file and a whole tree the same way.

«interface»Node+ size(): long+ print(indent): StringFile- bytes: longFolder- children: List+ add(n: Node): Folderchildren

The code

Java
interface Node {
    long size();
    String print(String indent);
}

record File(String name, long bytes) implements Node {
    public long size() { return bytes; }
    public String print(String indent) { return indent + name + " (" + bytes + ")\n"; }
}

final class Folder implements Node {
    private final String name;
    private final List children = new ArrayList<>();
    Folder(String name) { this.name = name; }
    Folder add(Node n) { children.add(n); return this; }

    public long size() { 1
        long total = 0;
        for (Node c : children) total += c.size();
        return total;
    }

    public String print(String indent) {
        StringBuilder sb = new StringBuilder(indent + name + "/ (" + size() + ")\n");
        for (Node c : children) sb.append(c.print(indent + "  "));
        return sb.toString();
    }
}

class CompositeDemo {
    public static void main(String[] args) {
        Folder trip = new Folder("goa-trip")
            .add(new File("itinerary.pdf", 120))
            .add(new Folder("photos")
                .add(new File("beach.jpg", 2_400))
                .add(new File("sunset.jpg", 3_100)));
        Folder home = new Folder("home").add(trip).add(new File("notes.txt", 8));
        System.out.print(home.print(""));
        System.out.println("one file: " + new File("x", 5).size()
            + ", whole tree: " + home.size());
    }
}
  1. 1ask the children
  1. A folder’s size is the sum of its children’s sizes, whether they’re files or folders. The recursion happens naturally.
Text
home/ (5628)
  goa-trip/ (5620)
    itinerary.pdf (120)
    photos/ (5500)
      beach.jpg (2400)
      sunset.jpg (3100)
  notes.txt (8)
one file: 5, whole tree: 5628

Mistakes and variations

  • *Putting add on the shared interface* so files have an add that throws. Some textbooks do this for uniformity; keeping add on Folder only is usually clearer.
  • Cycles. If a folder can be added inside itself, size() never finishes. Check on add, or make the tree impossible to loop by construction.
  • Recomputing large trees. print above calls size() on every level, which re-walks subtrees. For big trees, cache sizes and update them when children change.

When not to use it

When the data isn’t really a tree, or callers always need to know whether they have a group or a single item.

Where it shows up

Menus with sub-menus, organisation charts, UI layouts (a panel contains buttons and other panels), and pricing rules made of smaller rules.

Decorator: add behaviour by wrapping

The problem

Your code calls a payments API through an HttpClient interface. Now you need retries for flaky failures, logging of every call, and caching for some reads. Putting all three inside one client class makes it long, and not every caller wants all three. Subclasses don’t help either: RetryingLoggingCachingClient, RetryingLoggingClient, LoggingCachingClient… every combination becomes a class.

The pattern

Each extra behaviour is a small class that implements the same interface and wraps another instance of it. Add behaviours by wrapping, in whatever combination and order you need. Callers can’t tell a wrapped client from a plain one.

«interface»HttpClient+ get(path): StringFlakyServerRetryingClient- inner: HttpClient- attempts: intCachingClient- inner: HttpClient- cache: Mapwraps

The code

Java
interface HttpClient {
    String get(String path);
}

final class FlakyServer implements HttpClient {             // fails the first 2 calls
    int calls;
    public String get(String path) {
        calls++;
        if (calls <= 2) throw new RuntimeException("503 from " + path);
        return "200 " + path;
    }
}

final class RetryingClient implements HttpClient {
    private final HttpClient inner;
    private final int attempts;
    RetryingClient(HttpClient inner, int attempts) {
        this.inner = inner;
        this.attempts = attempts;
    }

    public String get(String path) {
        RuntimeException last = null;
        for (int i = 0; i < attempts; i++) {
            try { return inner.get(path); } 1
            catch (RuntimeException e) { last = e; }
        }
        throw last;
    }
}

final class CachingClient implements HttpClient {
    private final HttpClient inner;
    private final Map cache = new HashMap<>();
    CachingClient(HttpClient inner) { this.inner = inner; }
    public String get(String path) { return cache.computeIfAbsent(path, inner::get); }
}

final class LoggingClient implements HttpClient {
    private final HttpClient inner;
    final List log = new ArrayList<>();
    LoggingClient(HttpClient inner) { this.inner = inner; }
    public String get(String path) {
        try {
            String r = inner.get(path);
            log.add("ok " + path);
            return r;
        } catch (RuntimeException e) {
            log.add("fail " + path);
            throw e;
        }
    }
}

class DecoratorDemo {
    public static void main(String[] args) {
        FlakyServer s1 = new FlakyServer();
        LoggingClient inside = new LoggingClient(s1);
        HttpClient a = new RetryingClient(inside, 3); 2
        System.out.println("retry(log(server)): " + a.get("/rates")
            + ", log " + inside.log);

        FlakyServer s2 = new FlakyServer();
        LoggingClient outside = new LoggingClient(new RetryingClient(s2, 3));
        System.out.println("log(retry(server)): " + outside.get("/rates")
            + ", log " + outside.log);

        FlakyServer s3 = new FlakyServer();
        HttpClient c = new CachingClient(new RetryingClient(s3, 3));
        for (int i = 0; i < 5; i++) c.get("/rates");
        System.out.println("cache(retry(server)), 5 gets: server called "
            + s3.calls + " times");
    }
}
  1. 1same interface, inside
  2. 2retry outside logging
  1. A decorator holds something of the same interface and calls it, adding its own behaviour before or after.
  2. The order of wrapping changes what happens, as the output shows.
Text
retry(log(server)): 200 /rates, log [fail /rates, fail /rates, ok /rates]
log(retry(server)): 200 /rates, log [ok /rates]
cache(retry(server)), 5 gets: server called 3 times

With logging inside the retry, the log shows every attempt, including the two failures. With logging outside, the log shows one successful call and hides the failures completely. Neither is wrong; they answer different questions (“how flaky is the server?” versus “did the caller’s request work?”). And caching outside retrying means five requests cost three server calls in total: two failures and one success, then the cache.

Mistakes and variations

  • Not knowing the order matters. Say which order you chose and why; interviewers sometimes ask exactly this.
  • Wrappers that change the interface aren’t decorators; they’re adapters.
  • Java’s I/O streams are the textbook example: new BufferedReader(new InputStreamReader(new FileInputStream(f))) is three decorators.

When not to use it

When there’s only ever one combination. Then a single class is simpler than three wrappers.

Where it shows up

The parking lot adds weekend rates, daily caps and EV charging as pricing decorators: new DailyCap(new WeekendMultiplier(new HourlyPricing(...), 1.5), 50000). The LRU cache adds expiry and hit-rate statistics by wrapping the tested cache, and movie booking adds peak pricing the same way.

Facade: one simple front door

The problem

Placing a food order touches four subsystems: reserve stock at the restaurant, charge the customer, book a delivery partner, and send a confirmation. Every screen that places orders (the app, the website, the call-centre tool) would have to call all four in the right order, and undo the earlier ones if a later one fails.

The pattern

One class with a simple method, placeOrder, that coordinates the subsystems. Callers use the front door; the subsystems stay available for anyone who really needs them.

OrderFacade+ placeOrder(user, dish, paise): StringKitchen+ reserve(dish): boolean+ release(dish): voidPayments+ charge(user, paise): booleanRiders+ assign(): String

The code

Java
final class Kitchen {
    private int biryanis = 1;
    boolean reserve(String dish) { return biryanis-- > 0; }
    void release(String dish) { biryanis++; }
}

final class Payments {
    boolean charge(String user, long paise) { return !user.equals("broke"); }
}

final class Riders {
    String assign() { return "rider-17"; }
}

final class OrderFacade {
    private final Kitchen kitchen;
    private final Payments payments;
    private final Riders riders;
    OrderFacade(Kitchen k, Payments p, Riders r) {
        kitchen = k;
        payments = p;
        riders = r;
    }

    String placeOrder(String user, String dish, long paise) {
        if (!kitchen.reserve(dish)) return "sold out";
        if (!payments.charge(user, paise)) {
            kitchen.release(dish); 1
            return "payment failed";
        }
        return "confirmed, " + riders.assign();
    }
}

class FacadeDemo {
    public static void main(String[] args) {
        OrderFacade orders = new OrderFacade(new Kitchen(), new Payments(), new Riders());
        System.out.println("broke: " + orders.placeOrder("broke", "biryani", 25_000));
        System.out.println("asha:  " + orders.placeOrder("asha", "biryani", 25_000));
        System.out.println("ravi:  " + orders.placeOrder("ravi", "biryani", 25_000));
    }
}
  1. 1undo the earlier step
  1. The facade knows the right order of steps and how to undo them. Callers get that for free.
Text
broke: payment failed
asha:  confirmed, rider-17
ravi:  sold out

The failed payment gave the last biryani back to the kitchen, so Asha could still order it.

Mistakes and variations

  • A facade that grows into a god class. It should coordinate, not contain the subsystems’ logic. If OrderFacade starts working out delivery fees, that logic belongs in a subsystem.
  • Hiding the subsystems completely. A facade is a convenience, not a wall. Admin tools may still need the kitchen directly.

When not to use it

When there’s only one subsystem. A facade over one class is just an extra layer.

Where it shows up

Almost every problem in this track has one: ParkingLot with park and unpark, Game in chess with play and undo, BookingService in movie booking.

Flyweight: share what’s the same

The problem

A map app shows a million restaurant pins. Each pin has a position and a name, which differ, and an icon image, which is one of only five (veg, non-veg, café, bar, bakery). Store the icon’s pixels inside every pin and you store the same five images a million times.

The pattern

Split each object’s data into two parts. The intrinsic part (the icon) is the same across many objects, never changes, and is shared from a small cache. The extrinsic part (position, name) is unique to each object and stays in it. Objects hold a reference to the shared part instead of a copy.

Pin- lat, lng: double- name: String- icon: IconIcon- pixels: byte[]IconCache+ get(kind): Iconshared5 kinds

The code

Java
record Icon(String kind, byte[] pixels) { }

final class IconCache {
    private final Map icons = new HashMap<>();
    Icon get(String kind) {
        return icons.computeIfAbsent(kind, k -> new Icon(k, new byte[4096]));   // (1)
    }
    int size() { return icons.size(); }
}

record Pin(double lat, double lng, String name, Icon icon) { }

class FlyweightDemo {
    static final String[] KINDS = { "veg", "nonveg", "cafe", "bar", "bakery" };
    static List keep;                          // keeps pins alive while we measure

    static long usedMb() {
        Runtime rt = Runtime.getRuntime();
        for (int i = 0; i < 3; i++) System.gc();
        return (rt.totalMemory() - rt.freeMemory()) / 1_000_000;
    }

    public static void main(String[] args) {
        int n = 200_000;
        long before = usedMb();
        keep = new ArrayList<>(n);
        for (int i = 0; i < n; i++)
            keep.add(new Pin(i, i, "r" + i,
                new Icon(KINDS[i % 5], new byte[4096])));        // (2)
        long ownMb = usedMb() - before;
        keep = null;

        before = usedMb();
        IconCache cache = new IconCache();
        keep = new ArrayList<>(n);
        for (int i = 0; i < n; i++)
            keep.add(new Pin(i, i, "r" + i, cache.get(KINDS[i % 5])));
        long sharedMb = usedMb() - before;
        System.out.println(n + " pins, own icon each:  about " + ownMb + " MB");
        System.out.println(n + " pins, shared icons:   about " + sharedMb + " MB ("
            + cache.size() + " icons)");
    }
}
  1. Each kind of icon is created once and reused.
  2. The version without the pattern: a fresh 4 KB icon in every pin.
Text
200000 pins, own icon each:  about 848 MB
200000 pins, shared icons:   about 19 MB (5 icons)

About 45 times less memory, for the same pins. (Measured with the JVM’s own memory counters, so the numbers are approximate, but the difference isn’t.)

Mistakes and variations

  • Sharing something that changes. If one pin could recolour its icon, every pin of that kind would change. Flyweights must be immutable.
  • Java does this for you in places. Integer.valueOf caches small numbers, and string literals are shared. That’s flyweight in the standard library.

When not to use it

When there aren’t many objects, or the shared part is small. Sharing a 4-byte field saves nothing worth the extra cache.

Where it shows up

In chess, pieces hold only a colour, so the walkthrough notes that a faster engine would share one object per kind and colour. In snakes and ladders, one immutable board is shared by a million simulated games: the same idea at a bigger scale.

Proxy: a stand-in that controls access

The problem

A document viewer lists hundreds of scanned PDFs, each with a thumbnail. Loading every full document from storage when the list opens would take ages and memory, but code elsewhere expects Document objects it can call render() on. Separately, only some users may see confidential documents, and that check shouldn’t be copied into every place that renders one.

The pattern

A proxy implements the same interface as the real object and stands in for it. It decides when the real object is created (a virtual proxy, for lazy loading), whether a call is allowed (a protection proxy), or where the real object is (a remote proxy, as with RPC clients).

«interface»Document+ render(user): StringStoredDocument+ render(user): StringLazyDocument- real: StoredDocumentConfidentialDocument- inner: Document- allowed: Set

The code

Java
interface Document {
    String render(String user);
}

final class StoredDocument implements Document {
    static int loads;
    private final String id;
    StoredDocument(String id) { this.id = id; loads++; }   // pretend this is slow
    public String render(String user) { return "<" + id + " for " + user + ">"; }
}

final class LazyDocument implements Document { 1
    private final String id;
    private StoredDocument real;
    LazyDocument(String id) { this.id = id; }
    public String render(String user) {
        if (real == null) real = new StoredDocument(id);
        return real.render(user);
    }
}

final class ConfidentialDocument implements Document { 2
    private final Document inner;
    private final Set allowed;
    ConfidentialDocument(Document inner, Set allowed) {
        this.inner = inner;
        this.allowed = allowed;
    }
    public String render(String user) {
        if (!allowed.contains(user))
            throw new SecurityException(user + " may not view this");
        return inner.render(user);
    }
}

class ProxyDemo {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        for (int i = 0; i < 300; i++) list.add(new LazyDocument("scan-" + i));
        System.out.println("listed 300 documents, loaded " + StoredDocument.loads);
        System.out.println(list.get(42).render("asha"));
        System.out.println(list.get(42).render("ravi"));
        System.out.println("after opening one twice, loaded " + StoredDocument.loads);

        Document payroll =
            new ConfidentialDocument(new LazyDocument("payroll"), Set.of("hr"));
        System.out.println(payroll.render("hr"));
        try { payroll.render("asha"); }
        catch (SecurityException e) { System.out.println("refused: " + e.getMessage()); }
    }
}
  1. 1virtual proxy
  2. 2protection proxy
  1. Creates the real document only when someone actually renders it.
  2. Checks permission before passing the call on. The real document never has to know about permissions.
Text
listed 300 documents, loaded 0
42 for asha>
42 for ravi>
after opening one twice, loaded 1

refused: asha may not view this

Mistakes and variations

  • Proxy or decorator? The code looks the same. A decorator adds behaviour the caller wants (logging, retries); a proxy controls access to the object (when it exists, who may use it, where it lives). Many real classes do a bit of both; name the main intent.
  • Lazy loading isn’t thread-safe as written: two threads could both load. Use the same tools as the Singleton section if proxies are shared.
  • Java can generate proxies at runtime (java.lang.reflect.Proxy), which is how many frameworks add transactions or security around your classes without you writing the wrapper.

When not to use it

When the real object is cheap to create and has no access rules. A proxy around it is just indirection.

Where it shows up

The Redis-backed state in the rate limiter follow-up is a remote proxy: it implements the same KeyState interface, but the numbers live on another machine. Every RPC client stub works the same way.

Telling them apart

PatternHoldsInterface it showsIntentOne-line example
Adapterone objectdifferent from the inner onemake it fitvendor SMS library → your Notifier
Decoratorone object, same interfacesameadd behaviour; stackableretries around an HTTP client
Proxyone object, same interfacesamecontrol access or creationload a document on first view
Facadeseveral objectsnew and simplerhide a subsystemplaceOrder over kitchen, payments, riders
Bridgeone object from another hierarchyits ownlet two things vary separatelyalert type × channel
Compositea list of the same interfacesame as its childrentreat groups like itemsfolder sizes
Flyweighta shared, immutable partits ownsave memoryfive icons for a million pins

How this shows up in interviews

Structural patterns come up as soon as an interviewer changes the requirements:

  • ”Now add X without changing the existing class.” Usually a decorator (pricing rules, caching, retries).
  • ”We’re switching providers.” An adapter behind your own interface, so only one class changes.
  • ”How does the client call all this?” A facade, so the outside world sees two or three methods.
Interviewer

Interviewer: Every call to the payment provider should be retried, logged and timed. Where does that code go?

You

You: Not inside the gateway class, and not in every caller. I’d keep the gateway adapter doing only the translation to the provider’s API, and wrap it in small decorators: one that retries with backoff, one that logs, one that records timings. They all implement the same PaymentGateway interface, so the booking service doesn’t know they’re there. I’d put logging inside retrying so every attempt is logged, because for payments I want to see each one. And the retry has to reuse the same idempotency key on every attempt, or a retry could charge twice.