LLD Cohort · Week 4 · Sun 12 Apr 2026 · recording 2h58m
Facade, Strategy & Proxy
Facade hides a messy multi-step subsystem behind one simple method. Strategy makes algorithms swappable at runtime (and works together with Factory). Proxy stands in front of a real object to add access control, lazy loading or caching. The class ends with a comparison of the structural patterns and two short extras: Java maps vs Python dict, and a wallet-system sketch.
Each topic: one-line idea → the problem → real-life picture → the design step by step → Python code → doubts & corrections → remember
- Facade pattern
- Strategy pattern (+ Strategy with Factory)
- Proxy pattern (virtual, protection, caching)
- Adapter vs Facade vs Proxy vs Decorator
- Extras: Java maps vs dict, wallet system sketch
Facade pattern ▶ 1:07:48 – 1:31:00
Put one simple, unified class in front of a complex subsystem, so clients press one button instead of orchestrating many services themselves.
The problem
A login flow needs four independent services: PasswordValidator, OtpService, TokenService, AuditLogger. In the bad design the client (a controller, another team, another service) calls all four, in the right order, with its own checks. The teacher's list of what goes wrong:
- The controller knows too much. Your business flow (validate, then OTP, then token, then audit) is exposed. You cannot hide it even if you want to.
- Flow logic is duplicated in every client that needs to log in.
- Any auth change means changing every client (e.g. a constructor of one service changes).
- Hard to test and evolve.
Car: imagine a car where you see every wire, the fuel line and the fan, next to the steering wheel and brakes. You only need the steering, pedals and a start button; everything else should be hidden. That is abstraction: hide unnecessary details from the user.
ChatGPT / Claude: a facade over an LLM. You type a question and get an answer. You never see who does authentication, which DB stores your chats, or that the reply is streamed with server-sent events.
Induction cooker / ATM: press one button, the internal steps run in the right order.
The word: a facade is the front wall of a building, the face you see from outside, not what is really behind it.
The idea in one line
Facade = an orchestration layer that hides subsystem complexity behind a few simple methods. Before: client calls service 1, 2, 3, 4. After: client calls the facade; the facade calls 1, 2, 3, 4.
Use it when clients are overwhelmed by many classes and a fragile call order, or the same multi-step flow is copied in several places. Also when you finish a microservice or library: expose a facade (start, stop, replay) to its users.
Building it step by step
Before (bad): the client does the orchestration itself.
# BAD: every client repeats this flow and knows every service
validator, otp, tokens, audit = PasswordValidator(), OtpService(), TokenService(), AuditLogger()
if validator.validate(user, password):
otp.send_otp(user)
token = tokens.generate_token(user)
audit.log(f"{user} logged in")After (good): move exactly that work into AuthenticationFacade.login(). The work still has to happen; it just lives in one place.
# --- subsystem: four independent services -------------------------------
class PasswordValidator:
def validate(self, user: str, password: str) -> bool:
return password == "secret" # pretend DB check
class OtpService:
def send_otp(self, user: str) -> None:
print(f"OTP sent to {user}")
class TokenService:
def generate_token(self, user: str) -> str:
return f"token-{user}"
class AuditLogger:
def log(self, msg: str) -> None:
print(f"AUDIT: {msg}")
# --- the facade: one simple door in front of the subsystem ----------------
class AuthenticationFacade:
def __init__(self):
self.validator = PasswordValidator()
self.otp = OtpService()
self.tokens = TokenService()
self.audit = AuditLogger()
def login(self, user: str, password: str) -> str | None:
if not self.validator.validate(user, password): # step 1
self.audit.log(f"failed login for {user}")
return None
self.otp.send_otp(user) # step 2
token = self.tokens.generate_token(user) # step 3
self.audit.log(f"{user} logged in") # step 4
return token
# usage: the client knows ONE class and ONE method
auth = AuthenticationFacade()
print(auth.login("ganesh", "secret"))
# OTP sent to ganesh
# AUDIT: ganesh logged in
# token-ganeshLive-coded example (car): the client used to create Engine, TireController, FuelInjection, SafetyController and call them. Move them into a CarController with one start():
class Engine:
def start(self) -> None: print("Engine started")
class FuelInjection:
def inject(self) -> None: print("Fuel injected")
class TireController:
def check(self) -> None: print("Tyre pressure OK")
class SafetyController:
def arm(self) -> None: print("Airbags armed")
class CarController: # facade
def __init__(self):
self.engine = Engine()
self.fuel = FuelInjection()
self.tires = TireController()
self.safety = SafetyController()
def start(self) -> None: # the order lives HERE, not in clients
self.safety.arm()
self.tires.check()
self.fuel.inject()
self.engine.start()
# usage
CarController().start()
# Airbags armed
# Tyre pressure OK
# Fuel injected
# Engine startedUML / structure
classDiagram
class Client
class AuthenticationFacade {
+login(user, password)
}
class PasswordValidator {
+validate(user, password)
}
class OtpService {
+send_otp(user)
}
class TokenService {
+generate_token(user)
}
class AuditLogger {
+log(msg)
}
Client --> AuthenticationFacade
AuthenticationFacade --> PasswordValidator
AuthenticationFacade --> OtpService
AuthenticationFacade --> TokenService
AuthenticationFacade --> AuditLogger
Facade does not add new behaviour and does not need a shared interface. It only moves orchestration out of the client into one class. Facades can be layered (a facade over facades). The subsystem classes still exist and can still be used directly when needed.
Pros, cons, pitfalls
- Pros: thin, simple clients; call order is enforced in one place; subsystem changes touch only the facade; easier testing.
- Pitfall: a facade that grows to know everything becomes a "god object". Keep it a thin orchestrator; split into several facades if needed.
- Tip from class: look at your own codebase; if a client is "clumsy", orchestrating many services, suggesting a facade is a good refactor.
→
There is no one-liner. Every pattern exists to fix a specific problem, so learn the why of each (Decorator: add behaviour without class explosion; Composite: treat single and group uniformly). After 10-15 practice problems you will recognise the problem shape and the matching pattern.
- Facade = one simple front door over a complex subsystem.
- It fixes: controller knows too much, duplicated flow, every client changes on every subsystem change.
- Facade is about the client side: making it easy to use.
- Examples:
AuthenticationFacade.login(),CarController.start(), ChatGPT over an LLM.
Strategy pattern (+ Strategy with Factory) ▶ 1:31:32 – 2:00:50
Put each way of doing a task (each algorithm) in its own class behind one interface, and let the business logic hold "a strategy" that can be swapped at runtime.
The problem
Many problems have many ways to solve them. Sorting: merge, quick, heap, bubble, selection, insertion, bucket sort... Load balancing: round robin, least connections... Rate limiting: token bucket, leaky bucket, sliding window. Payments: credit card, UPI, PayPal.
The naive code is a big if/elif over the type inside the business logic:
# BAD: every new payment method means editing this function
def pay(method: str, amount: int) -> None:
if method == "card":
print(f"Paid {amount} by credit card")
elif method == "upi":
print(f"Paid {amount} by UPI")
elif method == "paypal":
print(f"Paid {amount} by PayPal")Problems: violates Open/Closed, hard to test, the algorithms are not reusable, and "the code smells like a junior design".
Strategy literally means "a way" or "an approach". Every sorting algorithm sorts in the end; they differ only in how. If they all share a common method (sort), you can plug in any of them and the caller does not care which.
The idea in one line
Encapsulate algorithms and make them interchangeable at runtime without changing the client code.
Use it when the same action has several interchangeable algorithms, or you expect new ones later. The teacher called it the most used pattern in LLD interviews: "extensibility is nothing but strategy".
Building it step by step
- Interface
SortingStrategywithsort(arr). Why an interface? It is a contract: anyone implementing it surely hassort, so you can call it blindly. It also hides the implementation. - Concrete strategies:
MergeSort,QuickSort,HeapSort. - Context (the business logic, here a
SortLibrary) has aSortingStrategy. It uses a default if none is given, and exposeschange_strategy(). - The context's
sort()just callsself.strategy.sort(arr): noif/else.
from abc import ABC, abstractmethod
class SortingStrategy(ABC): # the common contract
@abstractmethod
def sort(self, arr: list[int]) -> list[int]: ...
class MergeSort(SortingStrategy):
def sort(self, arr: list[int]) -> list[int]:
print("Sorting using Merge Sort")
return sorted(arr) # real algorithm skipped for brevity
class QuickSort(SortingStrategy):
def sort(self, arr: list[int]) -> list[int]:
print("Sorting using Quick Sort")
return sorted(arr)
class HeapSort(SortingStrategy):
def sort(self, arr: list[int]) -> list[int]:
print("Sorting using Heap Sort")
return sorted(arr)
class SortLibrary: # the "context" (business logic)
def __init__(self, strategy: SortingStrategy | None = None):
self.strategy = strategy or MergeSort() # sensible default
def change_strategy(self, strategy: SortingStrategy) -> None:
self.strategy = strategy
print("Strategy changed")
def sort(self, arr: list[int]) -> list[int]:
return self.strategy.sort(arr) # delegate; no if/else here
# usage
lib = SortLibrary(HeapSort())
print(lib.sort([3, 1, 2]))
lib.change_strategy(QuickSort()) # swapped at runtime
print(lib.sort([3, 1, 2]))
# Sorting using Heap Sort
# [1, 2, 3]
# Strategy changed
# Sorting using Quick Sort
# [1, 2, 3]Adding BubbleSort tomorrow = one new class. SortLibrary is not touched, because it is not tightly coupled to any concrete algorithm.
Strategy + Factory together
The payment version has the same shape: PaymentStrategy with pay; CreditCard, PayPal, UPI; and a PaymentContext. A student (Ajay) asked: "We also pass new CreditCard() here, so how is this different from Factory?" The teacher's answer: they solve different halves of the job and combine nicely.
- Strategy structures the code: the business logic is decoupled from the algorithms and just waits for some strategy object to be given to it.
- Factory creates the object: wherever you see
new QuickSort()/CreditCard()scattered around, ask a factory instead (factory.get_instance("quick")).
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: int) -> None: ...
class CreditCard(PaymentStrategy):
def pay(self, amount: int) -> None: print(f"Paid {amount} by credit card")
class PayPal(PaymentStrategy):
def pay(self, amount: int) -> None: print(f"Paid {amount} by PayPal")
class UPI(PaymentStrategy):
def pay(self, amount: int) -> None: print(f"Paid {amount} by UPI")
class PaymentContext: # business logic (STRATEGY user)
def __init__(self, strategy: PaymentStrategy):
self.strategy = strategy
def set_strategy(self, strategy: PaymentStrategy) -> None:
self.strategy = strategy
def pay(self, amount: int) -> None:
self.strategy.pay(amount)
class PaymentFactory: # FACTORY: only creates objects
_registry = {"card": CreditCard, "paypal": PayPal, "upi": UPI}
@classmethod
def get_instance(cls, kind: str) -> PaymentStrategy:
return cls._registry[kind]()
# usage: factory builds the object, strategy uses it
ctx = PaymentContext(PaymentFactory.get_instance("card"))
ctx.pay(500)
ctx.set_strategy(PaymentFactory.get_instance("upi"))
ctx.pay(200)
# Paid 500 by credit card
# Paid 200 by UPIUML / structure
classDiagram
class PaymentStrategy {
+pay(amount)
}
class PaymentContext {
-strategy
+set_strategy(strategy)
+pay(amount)
}
class PaymentFactory {
+get_instance(kind)
}
CreditCard ..|> PaymentStrategy
PayPal ..|> PaymentStrategy
UPI ..|> PaymentStrategy
PaymentContext o-- PaymentStrategy : uses
PaymentFactory --> PaymentStrategy : creates
| Factory | Strategy | |
|---|---|---|
| Category | Creational | Behavioural |
| Question it answers | "Give me an object of this type." | "Do this task, using whichever algorithm I plugged in." |
| Returns | An object | The result of the work (business logic runs) |
| Main benefit | No scattered new X() calls | Business logic decoupled from algorithms; swap at runtime |
| Teacher's one-liner | Factory helps run things (makes the objects) | Strategy is about structuring things |
In Python, functions are first-class, so a strategy can simply be a function (sorted(data, key=...) is a strategy in disguise). In interviews, still show the class version: it states the contract clearly and matches what the interviewer expects.
A student was asked at Costco to design a notification service, then "why Factory and not Strategy?" He used the word "decoupling" and was immediately asked to write pseudocode for both. Lesson: be able to explain (and code) exactly what Strategy decouples, and where Factory fits.
→
The client creates the context with a strategy (PaymentContext(CreditCard()), or via the factory) and calls ctx.pay(amount). Later it can call set_strategy(UPI()). Whatever object is inside is the one that runs.
→
A PaymentFactory.get_instance("card") returns the object. That replaces the new CreditCard() in the client. The context and strategies stay exactly the same. That is the combination shown above.
- Strategy = interface + many interchangeable algorithm classes + a context that holds one.
- No
if/elseon type; new algorithm = new class (Open/Closed). - Behaviour can change at runtime via
change_strategy/set_strategy. - Factory creates the strategy objects; Strategy uses them. They complement each other.
Proxy pattern (virtual, protection, caching) ▶ 2:01:50 – 2:43:30
A stand-in object with the same interface as the real object; it controls access to the real object and does extra work (auth, lazy loading, caching, logging) before forwarding the call.
The problem
You have a real object that is expensive or sensitive: a DB, a network or remote call, a file. You cannot (or should not) touch its business logic, but new requirements arrive: "only admins can read documents", "add audit logging", "cache results", "rate limit". Without a proxy the client talks to the real object directly, so there is no access control, no logging, no caching, and the client is tightly coupled to the real object. Putting all of that inside the real class violates Single Responsibility and Open/Closed.
"Proxy" means someone acting on your behalf. When you open google.com you never talk to Google's backend servers; you talk to their proxies (firewalls, load balancers) which validate the request and forward it only if needed. Cloudflare is the largest proxy provider. A student's version: a VPN sits before every request that goes out to the internet.
The idea in one line
Client → Proxy (same interface) → Real object. The proxy does its checks or work, then forwards (or does not).
Use it when you need to control access to or the lifecycle of an object without changing it or its clients.
Types of proxy covered in class
| Type | What it adds | Class example |
|---|---|---|
| Protection proxy | Access control: checks role/permission before forwarding. | "Only ADMIN can read documents." |
| Virtual proxy | Lazy initialisation: creates the expensive real object only when first needed. | Document loaded from DB on first read(). |
| Caching proxy | Stores results; repeated requests skip the slow real call. | VideoServiceProxy in front of a slow remote video server. |
| Logging / rate-limiting ("smart") proxy | Audit logs, rate limits, load balancing before forwarding. | Mentioned as extra behaviours the proxy layer can take on. |
Another type you may read about, not covered in class: the remote proxy (a local object that stands in for an object on another machine, e.g. an RPC stub).
Building it step by step: protection + virtual proxy
- Subject interface
Documentwithread(). Both the real object and the proxy implement it, so the client cannot tell them apart and nothing breaks. - Real subject
RealDocument: the actual, expensive work. - Proxy
DocumentProxy: has the sameread(); checks the role, creates the real document lazily, logs, then forwards.
from abc import ABC, abstractmethod
class Document(ABC): # same interface for real + proxy
@abstractmethod
def read(self) -> str: ...
class RealDocument(Document):
def __init__(self, name: str):
print(f"Loading {name} from DB...") # expensive
self.name = name
def read(self) -> str:
return f"secret contents of {self.name}"
class DocumentProxy(Document):
def __init__(self, name: str, role: str):
self.name, self.role = name, role
self._real: RealDocument | None = None # not created yet
def read(self) -> str:
if self.role != "ADMIN": # protection proxy
raise PermissionError("Only admin can read documents")
if self._real is None: # virtual proxy (lazy)
self._real = RealDocument(self.name)
print(f"AUDIT: {self.role} read {self.name}") # logging
return self._real.read() # forward to real object
# usage
doc = DocumentProxy("salary.pdf", "ADMIN")
print(doc.read())
try:
DocumentProxy("salary.pdf", "USER").read()
except PermissionError as e:
print("Blocked:", e)
# Loading salary.pdf from DB...
# AUDIT: ADMIN read salary.pdf
# secret contents of salary.pdf
# Blocked: Only admin can read documentsLive-coded: caching proxy
A video service fetches from a remote server, which is slow. Every request for the same video hits the server again. Add a proxy that keeps a cache: on a hit, return from the cache; on a miss, call the real service, store the result, return it. "For LLD, a HashMap (a dict) is your memory/database."
import time
from abc import ABC, abstractmethod
class VideoService(ABC): # step 1: subject interface
@abstractmethod
def get_video(self, video_id: str) -> str: ...
class RealVideoService(VideoService): # step 2: real, slow object
def get_video(self, video_id: str) -> str:
print("Fetching from remote server...")
time.sleep(1) # simulate network latency
return f"Video data for {video_id}"
class VideoServiceProxy(VideoService): # step 3: proxy with a cache
def __init__(self):
self.real_service = RealVideoService() # needed to forward misses
self.cache: dict[str, str] = {} # a dict is our "memory"
def get_video(self, video_id: str) -> str:
if video_id in self.cache:
print("Returning from cache")
return self.cache[video_id]
data = self.real_service.get_video(video_id) # cache miss
self.cache[video_id] = data
return data
# usage: client only knows the VideoService interface
service: VideoService = VideoServiceProxy()
print(service.get_video("abc")) # slow
print(service.get_video("abc")) # fast
print(service.get_video("xyz")) # slow again (new id)
# Fetching from remote server...
# Video data for abc
# Returning from cache
# Video data for abc
# Fetching from remote server...
# Video data for xyzUML / structure
classDiagram
class VideoService {
+get_video(video_id)
}
class RealVideoService {
+get_video(video_id)
}
class VideoServiceProxy {
-real_service
-cache
+get_video(video_id)
}
class Client
Client --> VideoService
RealVideoService ..|> VideoService
VideoServiceProxy ..|> VideoService
VideoServiceProxy --> RealVideoService : forwards misses
Proxy vs Decorator vs Facade (as explained in class)
Proxy vs Decorator: the code looks almost the same: same interface, hold a reference, do work, then forward. The teacher's rule of thumb: a decorator is a small touch-up on top of the base ("your decoration should not be bigger than your cake"; e.g. a retry or a timestamp), while a proxy is a separate, possibly much bigger layer (authentication, rate limiting, caching) guarding the real object.
Size is a useful memory hook, but the textbook difference is intent. A proxy controls access to one real object and often manages its lifecycle (creates it lazily, may refuse to forward). The client usually does not know a proxy is there. A decorator adds responsibilities, always forwards, and is designed to be stacked by the client (Timestamp around JSON around File). A tiny access check is still a proxy; a large formatting layer is still a decorator.
Proxy vs Facade: facade cares about the client side ("client is clumsy? press two buttons, I'll handle the rest"). Proxy cares about the server/backend side ("protect and optimise the real object"). They can be combined: a facade can call a proxy.
The teacher simulated latency with Thread.sleep() inside a try/catch for InterruptedException; Python just uses time.sleep(). For a thread-safe cache in Java use ConcurrentHashMap; in Python guard the check-then-store with a threading.Lock, or use functools.lru_cache for simple function-level caching.
→
On a cache miss (or after the checks pass) the proxy must forward the request. It can only do that if it has the real object. The client makes a single call to the proxy and never calls the real service itself.
→
Parity. The client was calling get_video; it must keep calling get_video without knowing whether a proxy is in front. If the proxy had a different method name (get_video_fast), clients would have to handle two methods, which causes confusion. In real systems you never know the real server; you only know the proxy.
→
(Shared by a student from a real interview.) The facade gives the client one API that combines status, invoice and documents. A proxy would sit between that API and the server for caching, auth and token checks. Different sides, different jobs.
- Proxy = same interface as the real object + reference to it + extra control before forwarding.
- Types: protection (access), virtual (lazy creation), caching, plus logging/rate-limiting.
- The client codes against the interface and never sees the real object.
- Proxy guards the server side; Facade simplifies the client side.
Adapter vs Facade vs Proxy vs Decorator ▶ 2:02:06 – 2:09:30
All four are structural patterns that put a class in front of another class, so they look alike in code. Tell them apart by intent.
| Pattern | Intent (one line) | Interface it exposes | Example from class |
|---|---|---|---|
| Adapter | Make an incompatible class fit the interface the client already expects. | Different from the wrapped class (translates) | RazorPayAdapter.pay() → make_payment() |
| Facade | Hide a complex multi-class subsystem behind one simple entry point. | New, simpler interface over many classes | AuthenticationFacade.login() |
| Proxy | Control access to one real object (auth, lazy load, cache) on its behalf. | Same as the real object | VideoServiceProxy with cache |
| Decorator | Add extra behaviour to an object dynamically, stackable, without subclassing. | Same as the wrapped object | TimestampDecorator(FileLogger()) |
Adapter converts. Facade simplifies. Proxy controls. Decorator adds. Composite (c04) is the fifth structural pattern: it groups objects into a tree.
Extras: Java maps vs dict, wallet system sketch ▶ 2:43:30 – 2:56:50
Two short student questions at the end: which map type to use in LLD code, and how to start designing a wallet system.
HashMap vs Hashtable vs Map vs dictionary
| Name | What it is | Thread-safe? | Nulls |
|---|---|---|---|
Java Map | The interface (HashMap, LinkedHashMap, TreeMap, Hashtable implement it) | n/a | n/a |
Java HashMap | Key-value store; fast; the default choice | No | One null key, many null values |
Java Hashtable | Old synchronized map; slower | Yes | No null keys or values |
Java ConcurrentHashMap | Modern thread-safe map | Yes | No nulls |
Python dict | Same idea as HashMap ("a word and its meaning") | Single operations are safe in CPython, but use a Lock for check-then-update | None allowed as key/value |
For LLD: HashMap (dict) for storage, ConcurrentHashMap when the interviewer asks for thread safety, and a set for unique items. How HashMap works internally (array of buckets) will be a later LLD question.
Wallet system: first sketch
A student was asked "design a wallet system" in an interview. The teacher's approach: first write the requirements (deposit money, check balance, pay using the wallet, keep transaction history, one wallet per user, many users), then layers:
- Wallet: balance + list of transactions.
- Repository layer: everything about data; a dict of
user_id -> Wallet. - WalletManager (service layer): deposit, pay, balance; called with a user id.
from dataclasses import dataclass, field
@dataclass
class Wallet:
user_id: str
balance: int = 0
transactions: list[str] = field(default_factory=list)
class WalletRepository: # data layer: a dict as the "DB"
def __init__(self):
self._wallets: dict[str, Wallet] = {}
def get(self, user_id: str) -> Wallet:
return self._wallets.setdefault(user_id, Wallet(user_id))
class WalletManager: # service layer: business rules
def __init__(self, repo: WalletRepository):
self.repo = repo
def deposit(self, user_id: str, amount: int) -> None:
w = self.repo.get(user_id)
w.balance += amount
w.transactions.append(f"+{amount}")
def pay(self, user_id: str, amount: int) -> None:
w = self.repo.get(user_id)
if amount > w.balance:
raise ValueError("Insufficient balance")
w.balance -= amount
w.transactions.append(f"-{amount}")
def balance(self, user_id: str) -> int:
return self.repo.get(user_id).balance
# usage
mgr = WalletManager(WalletRepository())
mgr.deposit("u1", 500)
mgr.pay("u1", 120)
print(mgr.balance("u1"), mgr.repo.get("u1").transactions) # 380 ['+500', '-120']The teacher rated it easy-to-medium and promised a full version later. From next week classes move from individual patterns to solving full LLD problems (LRU cache is next), so practise every pattern this week.