LLD Cohort · Week 4 · Sat 11 Apr 2026 · recording 2h52m

Composite & Adapter

Two structural patterns: Composite (treat a single item and a group of items the same way, like files and folders) and Adapter (a middle layer that makes a third-party class fit your interface). The class also has a quick revision of Singleton and Decorator, and a short preview of Facade (covered fully in c05).

Each topic: one-line idea → the problem → real-life picture → the design step by step → Python code → doubts & corrections → remember

Composite pattern ▶ 1:31:00 – 2:18:32

Compose objects into a tree, so the client can treat one object (a leaf) and a group of objects (a composite) in exactly the same way.

The problem

Some data is naturally a hierarchy (a tree). A file system is the classic one: a folder can hold files and other folders, and those folders can hold more files and folders.

Now you want operations like size, ls, delete, rename, cp. Without a pattern, the client code fills up with type checks: "if this is a file do X, else if it is a folder loop over its children and do Y". Every new node type means editing every one of those if blocks, which breaks the Open/Closed principle (open for extension, closed for modification).

The teacher showed it live in a terminal: ls works on LLD and on LLD/Pradeep without caring which is the parent and which is the child. rm -rf on a folder deletes everything inside it, one level at a time. The command treats parent and child the same way. That is the Composite idea.

Real-life picture

File system: a folder "Documents" holds a file "notes.txt" and a folder "Movies". Ask "Documents" for its size, and it asks each child for its size and adds them up. A file just answers with its own size.

Org chart: the CEO has VPs, VPs have staff, staff have engineers. CEO is also an employee. HR wants the total cost (salary) of the CEO's whole tree, or wants to apply an appraisal to everyone. The same operation runs on the CEO, the VP and the engineer.

UI trees: a window holds panels, a panel holds buttons and text boxes. Swing, JavaFX, Android and React all work this way: same interface, same recursive behaviour, the client does not care about the type.

The idea in one line

Give leaves and containers one shared interface. The container keeps a list of that interface type (not a list of files plus a list of folders) and implements each operation by calling the same method on its children.

Use it when your objects form a part-whole tree and you want the same operations on the single item and on the group.

Key words

Building it step by step

Step 1, the bad version. The client must know the type of every node:

Python
# BAD: client does type checking everywhere
def size(node) -> int:
    if isinstance(node, File):
        return node.size
    elif isinstance(node, Folder):
        total = 0
        for f in node.files:          # separate list of files
            total += f.size
        for d in node.folders:        # separate list of folders
            total += size(d)
        return total
    # a new node type (e.g. Shortcut) means editing this function again

Step 2, one interface. "If there is a need to treat everything uniformly, interface is the thing." Create FileSystemNode with the operations every node must support.

Step 3, leaf and composite both implement it. In class the teacher asked: should a folder keep a list of folders, a list of files, or a list of FileSystemNode? Answer: a list of FileSystemNode, so one list holds both and the folder never cares which is which.

Python
from abc import ABC, abstractmethod

class FileSystemNode(ABC):
    """Common contract: files AND folders both support these methods."""
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def size(self) -> int: ...

    @abstractmethod
    def ls(self, indent: int = 0) -> None: ...


class File(FileSystemNode):          # the LEAF: cannot contain anything
    def __init__(self, name: str, size: int):
        super().__init__(name)
        self._size = size

    def size(self) -> int:
        return self._size            # a file just knows its own size

    def ls(self, indent: int = 0) -> None:
        print("  " * indent + f"File: {self.name}")


class Folder(FileSystemNode):        # the COMPOSITE: holds other nodes
    def __init__(self, name: str):
        super().__init__(name)
        self.children: list[FileSystemNode] = []   # files OR folders

    def add(self, node: FileSystemNode) -> None:
        self.children.append(node)

    def remove(self, node: FileSystemNode) -> None:
        self.children.remove(node)

    def size(self) -> int:
        # ask every child for its size; a child folder does the same in turn
        total = sum(child.size() for child in self.children)
        print(f"Total size of folder {self.name}: {total}")
        return total

    def ls(self, indent: int = 0) -> None:
        print("  " * indent + f"Folder: {self.name}")
        for child in self.children:  # same call, no type checks
            child.ls(indent + 1)


# usage
snehil = Folder("snehil")
snehil.add(File("LLDNotes", 10))
snehil.add(Folder("HLDNotes"))
payal = Folder("payal")
payal.add(File("DSANotes", 20))
root = Folder("root")
root.add(snehil)
root.add(payal)

root.size()
root.ls()
# Total size of folder HLDNotes: 0
# Total size of folder snehil: 10
# Total size of folder payal: 20
# Total size of folder root: 30
# Folder: root
#   Folder: snehil
#     File: LLDNotes
#     Folder: HLDNotes
#   Folder: payal
#     File: DSANotes

Notice: from the client side, ls and size look the same on a file and a folder. Inside, the folder does more work (a loop), the file just returns. "I don't care which type of object you are, file or folder, give me ls."

Second example (org chart), same shape:

Python
from abc import ABC, abstractmethod

class Employee(ABC):
    def __init__(self, name: str, salary: int):
        self.name, self.salary = name, salary

    @abstractmethod
    def cost(self) -> int: ...


class Engineer(Employee):                 # leaf
    def cost(self) -> int:
        return self.salary


class Manager(Employee):                  # composite (CEO, VP, ...)
    def __init__(self, name: str, salary: int):
        super().__init__(name, salary)
        self.reports: list[Employee] = []

    def add(self, e: Employee) -> None:
        self.reports.append(e)

    def cost(self) -> int:
        return self.salary + sum(r.cost() for r in self.reports)


# usage
ceo = Manager("CEO", 100)
vp = Manager("VP", 50)
vp.add(Engineer("Asha", 20))
vp.add(Engineer("Ravi", 20))
ceo.add(vp)
print(ceo.cost())   # 190
print(vp.cost())    # 90

UML / structure

classDiagram
  class FileSystemNode {
    +name
    +size() int
    +ls()
  }
  class File {
    -size_value
    +size() int
    +ls()
  }
  class Folder {
    -children
    +add(node)
    +remove(node)
    +size() int
    +ls()
  }
  File ..|> FileSystemNode
  Folder ..|> FileSystemNode
  Folder *-- FileSystemNode : children

Read the diagram with the teacher's two relationships:

That is also why the pattern is called Composite: a thing of type A contains a list of things of type A. "Folder is a file system node, and folder has file system nodes."

Remember

Composite = a structural pattern for hierarchies. If you see a tree and the same operation must run on the leaf and on the group, think Composite. One interface, a leaf, and a container that holds a list of the interface type.

Is it recursion?

A student asked whether Folder.ls() calling child.ls() is recursion. The teacher said it is "a different, multi-directional kind of recursion" because the same method is called on different objects. In standard terms this is simply recursion over a tree (structural recursion): the base case is the leaf (File), the recursive case is the folder. Both views are fine; the important part is that each node handles its own data.

Java vs Python

The teacher used a Java interface FileSystemNode and List<FileSystemNode> child = new ArrayList<>(). In Python we use abc.ABC with @abstractmethod, and a plain typed list list[FileSystemNode]. In Java, a variable of type FileSystemNode cannot call add() (the teacher hit this compile error live) because only Folder has it. Python does not check this at compile time, but you should still only call add on a Folder.

Pros, cons, pitfalls

ProsCons / pitfalls
Client code is simple: no isinstance checks.Where do add/remove live? On the interface (then a File must reject them) or only on Folder (then the client must know it has a folder). The teacher kept them only on Folder.
Recursive operations (size, cost, delete, search) come almost free.A very general interface can make it hard to restrict which children are allowed (e.g. "a panel may not contain a window").
Open/Closed: add a new leaf type (e.g. Shortcut) without touching the client.Deep trees mean deep call stacks; very deep trees may need an iterative walk.

Homework from class: build your own small file system (folders, files, ls, size, then add rename, delete, and a parent link to walk back up). The teacher said a past student was asked "design a file system" at Rubrik and this pattern was the key.

Doubt: Apart from file systems, where can we use Composite?
→

Any hierarchy where the same operation must run on every level. Example: an org chart (CEO, VP, staff, engineers). HR wants the total cost of the CEO's tree, or wants to apply an appraisal to everyone. UI component trees (window, panel, button) are another standard example.

Doubt: So whenever there is a hierarchy, we create one interface and inherit from it?
→

Yes. Composite is a structural pattern for objects arranged hierarchically. The interface (or abstract class) is the contract: it defines which operations work on every node. Leaf and composite are the concrete classes that implement them.

Doubt: Is the base type an interface (no objects) or a normal class with overridable methods?
→

Either an interface or an abstract class. You never create a plain "FileSystemNode"; you create Files and Folders and hold them through the base type. When the client calls ls on a base reference, the real object's version runs (polymorphism).

Doubt: File and Folder are IS-A FileSystemNode. How is Folder also HAS-A?
→

Say it in English. "A team has a list of players." "A folder has a list of file system nodes." You cannot say "a folder is a list of nodes". IS-A comes from inheritance; HAS-A comes from the list the folder owns. Both are true at the same time.

Key takeaways
  • Composite is for tree-shaped data where leaf and group need the same operations.
  • Three parts: component interface, leaf, composite holding list[Component].
  • The composite implements each operation by delegating to its children (recursion over the tree).
  • It removes isinstance checks from client code and respects Open/Closed.
  • Folder IS-A node and HAS-A list of nodes (composition, because children die with the parent).

Adapter pattern ▶ 2:18:40 – 2:52:27

A middle class that converts the interface of an existing (often third-party) class into the interface your client already expects, so two incompatible pieces can work together.

The problem

Your codebase has a PaymentProcessor interface with a pay(amount) method. Your in-house gateway (PhonePe) implements it, and every client calls processor.pay(...). Life is good.

Then management says: "Let's use Razorpay (or Stripe), they are doing good work." But Razorpay's class has a different method, make_payment(amount), and it does not implement your interface. You cannot change their code, and you do not want to rename your pay because who knows how many clients would break. The work is the same (take a payment), only the names do not match. That is an incompatible interface.

The tempting fix is an if in business code:

Python
# BAD: business logic polluted with third-party details
def checkout(gateway_type: str, amount: int) -> None:
    if gateway_type == "stripe":
        StripeGateway().make_payment(amount)
    elif gateway_type == "razorpay":
        RazorPay().make_payment(amount)
    else:
        PhonePay().pay(amount)

Why it is bad: it violates Open/Closed (every new gateway = a new if), the business logic is polluted with third-party details, and testing becomes a nightmare.

Real-life picture

Your Indian charger does not fit a US wall socket. You do not change the socket and you do not change the charger. You put a travel adapter in between. The adapter's one side knows what the US socket expects; its other side knows what your Indian charger wants. In code: the adapter knows Razorpay (holds a reference to it) and looks like your PaymentProcessor (implements pay).

The idea in one line

Write a new class that implements the client's interface and holds a reference to the incompatible object (the "adaptee"); each client call is translated into the adaptee's call.

Use it when you must plug a class you cannot (or should not) modify into code that expects a different interface: third-party SDKs, legacy code, deprecated APIs.

Creating a new class breaks no principle: "You guys do your own thing. I will create a new class, I will manage." Neither side is disturbed.

Building it step by step

  1. Keep your target interface (PaymentProcessor.pay) exactly as it is.
  2. Leave the third-party class (RazorPay.make_payment) untouched.
  3. Create RazorPayAdapter that implements PaymentProcessor. Who are we adapting for? The client, because the client is the one making the call. Razorpay will not adapt to us; we adapt to Razorpay.
  4. Pass the Razorpay object into the adapter's constructor and keep it.
  5. Inside pay(), call self.razorpay.make_payment(amount).
Python
from abc import ABC, abstractmethod

class PaymentProcessor(ABC):              # OUR interface; clients only know pay()
    @abstractmethod
    def pay(self, amount: int) -> None: ...


class PhonePay(PaymentProcessor):         # our in-house gateway, already fits
    def pay(self, amount: int) -> None:
        print(f"Payment of {amount} via PhonePe done")


class RazorPay:                           # THIRD-PARTY class: we cannot edit it
    def make_payment(self, amount: int) -> None:
        print(f"RazorPay processed payment of {amount}")


class RazorPayAdapter(PaymentProcessor):  # the middle layer
    def __init__(self, razorpay: RazorPay):
        self.razorpay = razorpay          # keep a reference to the adaptee

    def pay(self, amount: int) -> None:   # speak the client's language...
        self.razorpay.make_payment(amount)  # ...translate to theirs


def checkout(processor: PaymentProcessor, amount: int) -> None:
    processor.pay(amount)                 # client code never changes


# usage
checkout(PhonePay(), 100)
checkout(RazorPayAdapter(RazorPay()), 200)
# Payment of 100 via PhonePe done
# RazorPay processed payment of 200

The teacher's slides used Stripe (StripeGateway.makePayment with a StripePaymentAdapter); the live code used Razorpay. Same structure.

UML / structure

classDiagram
  class PaymentProcessor {
    +pay(amount)
  }
  class PhonePay {
    +pay(amount)
  }
  class RazorPayAdapter {
    -razorpay
    +pay(amount)
  }
  class RazorPay {
    +make_payment(amount)
  }
  class Client
  Client --> PaymentProcessor
  PhonePay ..|> PaymentProcessor
  RazorPayAdapter ..|> PaymentProcessor
  RazorPayAdapter --> RazorPay : adaptee
Remember

Adapter = "implement what the client expects, hold a reference to what you have, translate in between." The client keeps calling pay and never learns that Razorpay is behind it.

Java vs Python

In Java, PaymentProcessor p = new RazorPay(); is a compile error because RazorPay does not implement the interface; that error is the "incompatibility" the teacher showed. Python would accept the assignment and fail only at runtime with AttributeError: 'RazorPay' object has no attribute 'pay'. Same problem, found later. What we built is an object adapter (composition). Python can also do a class adapter via multiple inheritance (class RazorPayAdapter(PaymentProcessor, RazorPay)), but composition is the usual, more flexible choice.

Pros, cons, pitfalls

Doubt: If the customer still calls order.pay(), why would anything break? Only the internal payment method changed.
→

Because the object behind pay is now a Razorpay object, and Razorpay has no pay; it has make_payment. Writing processor: PaymentProcessor = RazorPay() does not fit the contract (Java refuses to compile it). The adapter is exactly the thing that keeps order.pay() working while the real work goes to Razorpay.

Doubt: Where would I use this at work?
→

Students suggested "compatibility" and "when an API gets deprecated". Both are right: wrapping a third-party SDK, a legacy service, or a new version of an API behind the interface your code already uses.

Preview at the end of class: Facade

The last minutes introduced Facade: "the front wall of a building that hides what is behind it". A 6-step flow (take card, PIN, amount, contact bank...) is hidden behind one pay() button so the client cannot mess up the order. The teacher also briefly described a proxy as "someone acting on your behalf" (a reverse proxy, firewall or load balancer in front of Google's servers). Both are covered properly in c05.

Key takeaways
  • Adapter fixes an interface mismatch between a client and a class you cannot change.
  • Adapter implements the client's interface and holds the adaptee (composition).
  • The if gateway == ... alternative breaks Open/Closed and pollutes business logic.
  • Think "travel plug adapter": change neither the socket nor the charger.

Quick revision: Singleton & Decorator ▶ 0:48:51 – 1:30:43

Before the new patterns the teacher re-explained Singleton (one object only) and Decorator (add behaviour on top of an object without changing it).

Singleton in 60 seconds

Why: some objects must exist exactly once (config, logger). Two of them cause inconsistent state, wasted resources, race conditions and hard debugging. Singleton is a creational pattern (it controls how objects are created): a class has only one instance and gives global access to it.

How (Java): make the constructor private so nobody outside can call new; expose a static getInstance() that creates the object only if it is still null.

VariantHowVerdict
Lazy (basic)if instance == null: createNot thread-safe: two threads can pass the check together and make two objects.
Synchronized methodLock the whole getInstanceSafe but slow: every call pays for the lock.
Double-checked lockingCheck, lock only the small block, check againSafe and fast, but easy to get wrong (Java needs volatile).
EagerCreate the object when the class loadsSimple and safe, but wastes memory if never used.
Bill PughStatic inner Holder class creates it on first accessClean, lazy, thread-safe without synchronized (uses JVM class-loading guarantees).
Real-life picture

If someone threatens you in Delhi, you protect your house, not the whole city. Double-checked locking locks only the small critical block, not the whole method.

Python
import threading

class Logger:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:              # 1st check: fast path, no lock
            with cls._lock:                    # only one thread enters here
                if cls._instance is None:      # 2nd check: still empty?
                    cls._instance = super().__new__(cls)
        return cls._instance

    def log(self, msg: str) -> None:
        print(f"LOG: {msg}")


# usage
a, b = Logger(), Logger()
print(a is b)   # True
Java vs Python

Python has no private constructors, so we control creation in __new__. Python has no volatile/synchronized; use threading.Lock. The Pythonic "eager singleton" is simply a module-level object: a module is imported once, so logger = Logger() in log.py is shared everywhere.

Decorator in 60 seconds

Problem: you have ConsoleLogger and FileLogger. Product then asks for a timestamp, then JSON formatting, then a log level... Making ConsoleLoggerWithTimestamp, FileLoggerWithTimestampAndLevel and so on causes a class explosion, and editing the existing classes breaks Open/Closed.

Real-life picture

A plain cake is still a cake, but you decorate it for a birthday. A phone cover adds protection and can be taken off at any time. Decoration always sits on top of a base: "there is no decoration without a cake".

Rules: (1) every decorator is a Logger, so the client still calls only log(); (2) every decorator has a Logger (the base it wraps); (3) it runs its own code first, then hands over to the base. Because a decorated logger is still a Logger, decorators can be nested in any order.

Python
import json
from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, msg: str) -> None: ...


class ConsoleLogger(Logger):
    def log(self, msg: str) -> None:
        print(msg)


class FileLogger(Logger):
    def __init__(self):
        self.lines: list[str] = []         # stand-in for a real file

    def log(self, msg: str) -> None:
        self.lines.append(msg)


class LoggerDecorator(Logger, ABC):
    """Every decorator IS a Logger and HAS a Logger (the base it wraps)."""
    def __init__(self, logger: Logger):
        self.logger = logger


class TimestampDecorator(LoggerDecorator):
    def log(self, msg: str) -> None:
        msg = f"[12:00:00] {msg}"          # decorator's own work first...
        self.logger.log(msg)               # ...then hand over to the base


class JsonParserDecorator(LoggerDecorator):
    def log(self, msg: str) -> None:
        self.logger.log(json.dumps({"message": msg}))


# usage: decorators can be stacked in any order
logger = TimestampDecorator(JsonParserDecorator(ConsoleLogger()))
logger.log("user signed in")
# {"message": "[12:00:00] user signed in"}
classDiagram
  class Logger {
    +log(msg)
  }
  class LoggerDecorator {
    -logger
    +log(msg)
  }
  ConsoleLogger ..|> Logger
  FileLogger ..|> Logger
  LoggerDecorator ..|> Logger
  LoggerDecorator o-- Logger : wraps
  TimestampDecorator --|> LoggerDecorator
  JsonParserDecorator --|> LoggerDecorator
Doubt: Isn't "LoggerDecorator implements Logger" just Strategy?
→

No. Only the part "an interface with several interchangeable implementations" looks like Strategy. Decorator's key point is wrapping: the decorator holds another Logger and adds work before passing the call on.

Doubt: Can I write FileLogger(TimestampDecorator(...))?
→

No. The decoration wraps the base, not the other way round. The innermost object must be a real base logger (Console/File); decorators go around it.

Doubt: Are TimestampDecorator and JsonParserDecorator siblings?
→

Yes. Both extend LoggerDecorator, and you can add as many sibling decorators as you like without touching FileLogger or ConsoleLogger.

Key takeaways
  • Singleton: private creation + one controlled access point; know the 5 variants and their trade-offs.
  • Bill Pugh (Java) and module-level objects (Python) are the clean, lazy/safe choices.
  • Decorator: IS-A and HAS-A the same interface; run your code, then delegate.
  • Decorators stack, which avoids class explosion.