Session 8 · Facade pattern#
The big idea#
A real system is made of many small parts: each with its own setup and methods. Using them directly means the caller must know the right steps and the right order. The Facade pattern puts one simple class in front that does the whole job with one or two methods.
Everyday example: a hotel receptionist. You say “I need a room with dinner and a cab at 6”. The receptionist talks to housekeeping, the kitchen and the taxi desk. You never visit those yourself.
In plain words
Give a complicated system one simple front door. Callers make one call; the facade runs all the steps in the right order behind the scenes.
Reach for it when:
callers repeat the same multi-step sequence across several classes
you want one place where the order of steps lives
beginners (or other teams) should use the system without learning every part
Without Facade#
0.no_facade.py: the caller runs every step in the right order.
class Inventory:
def check(self, item): print(f"{item} in stock"); return True
class Payment:
def charge(self, amount): print(f"charged ₹{amount}"); return True
class Shipping:
def book(self, item): print(f"shipping booked for {item}")
class Notifier:
def send(self, msg): print(f"sent: {msg}")
# every page that places an order repeats this:
inv, pay, ship, note = Inventory(), Payment(), Shipping(), Notifier()
if inv.check("laptop") and pay.charge(55000):
ship.book("laptop")
note.send("Order placed")
Five lines, four classes, one fixed order. The checkout page, the mobile API and the admin tool all copy it, and one change means editing all three.
With Facade#
1.facade.py:
class OrderFacade:
def __init__(self):
self._inventory = Inventory()
self._payment = Payment()
self._shipping = Shipping()
self._notifier = Notifier()
def place_order(self, item: str, amount: int) -> bool:
if not self._inventory.check(item):
return False
if not self._payment.charge(amount):
return False
self._shipping.book(item)
self._notifier.send(f"Order placed for {item}")
return True
OrderFacade().place_order("laptop", 55000)
One line for the caller. The order of steps lives in one place.
The facade does not hide the subsystem classes. Advanced users can still use Payment directly.
It just offers an easy front door.
Facade + AI: a RAG facade#
A RAG pipeline has loader, splitter, embedder, vector store, retriever, prompt and LLM
(2.rag_facade.py):
class RagFacade:
def __init__(self, llm, embeddings, vector_store):
self._llm = llm
self._embeddings = embeddings
self._store = vector_store
def ingest(self, text: str):
chunks = [text[i:i + 500] for i in range(0, len(text), 500)]
self._store.add_texts(chunks)
def ask(self, question: str) -> str:
docs = self._store.similarity_search(question, k=3)
context = "\n\n".join(d.page_content for d in docs)
prompt = f"Answer using only this context:\n{context}\n\nQuestion: {question}"
return self._llm.invoke(prompt).content
rag = RagFacade(llm, embeddings, store)
rag.ingest(open("notes.txt").read())
print(rag.ask("What is a Singleton?"))
Your web app only needs ingest() and ask().
Facade vs Adapter#
Adapter: one thing, wrong shape → make it fit. It changes an interface.
Facade: many things, too complex → give one simple entry. It simplifies an interface.
Common mistakes#
A “god” facade with 40 methods. Make several small facades:
OrderFacade,ReportFacade.Business logic inside the facade. It should coordinate, not decide.
Blocking access to the subsystems. Keep them usable for special cases.
Hands-on exercises#
Exercise 1 · Home theatre. Make HomeTheatre.watch(movie) do: lights dim, projector on, sound on,
play movie.
Solution
class Lights:
def dim(self): print("lights dimmed")
class Projector:
def on(self): print("projector on")
class Sound:
def on(self): print("sound on")
class Player:
def play(self, m): print(f"playing {m}")
class HomeTheatre:
def __init__(self):
self.lights, self.projector, self.sound, self.player = Lights(), Projector(), Sound(), Player()
def watch(self, movie):
self.lights.dim()
self.projector.on()
self.sound.on()
self.player.play(movie)
HomeTheatre().watch("Interstellar")
Exercise 2 · Add cancel_order. Use the same subsystems in reverse.
Solution
class Payment:
def charge(self, amount): ...
def refund(self, amount): print(f"refunded ₹{amount}")
class OrderFacade:
...
def cancel_order(self, item, amount):
self._payment.refund(amount)
self._notifier.send(f"Order cancelled for {item}")
Exercise 3 · Video converter. convert(path, fmt) should load the file, extract audio, encode
and save. Hide the steps behind a facade.
Solution
class Loader:
def load(self, p): print(f"loaded {p}"); return p
class Encoder:
def encode(self, data, fmt): print(f"encoded to {fmt}"); return data
class Saver:
def save(self, data, name): print(f"saved {name}")
class VideoConverter:
def convert(self, path, fmt):
data = Loader().load(path)
data = Encoder().encode(data, fmt)
Saver().save(data, path.rsplit(".", 1)[0] + "." + fmt)
VideoConverter().convert("movie.avi", "mp4")
Full source#
1.facade.py
"""Facade: one simple front door that runs the whole order flow."""
class Inventory:
def check(self, item): print(f"{item} in stock"); return True
class Payment:
def charge(self, amount): print(f"charged ₹{amount}"); return True
def refund(self, amount): print(f"refunded ₹{amount}")
class Shipping:
def book(self, item): print(f"shipping booked for {item}")
class Notifier:
def send(self, msg): print(f"sent: {msg}")
class OrderFacade:
def __init__(self):
self._inventory = Inventory()
self._payment = Payment()
self._shipping = Shipping()
self._notifier = Notifier()
def place_order(self, item: str, amount: int) -> bool:
if not self._inventory.check(item):
return False
if not self._payment.charge(amount):
return False
self._shipping.book(item)
self._notifier.send(f"Order placed for {item}")
return True
def cancel_order(self, item: str, amount: int):
self._payment.refund(amount)
self._notifier.send(f"Order cancelled for {item}")
shop = OrderFacade()
shop.place_order("laptop", 55000)
shop.cancel_order("laptop", 55000)
2.rag_facade.py
"""Facade + AI: hide a whole RAG pipeline behind ingest() and ask().
The vector store and LLM are small offline stand-ins with LangChain-shaped methods
(add_texts, similarity_search, invoke), so the file runs without any service.
"""
from types import SimpleNamespace as NS
class FakeLLM:
"""Offline stand-in with the same .invoke(...).content shape as a LangChain chat model."""
def __init__(self):
self.calls = 0
def invoke(self, prompt):
self.calls += 1
return type("Msg", (), {"content": f"(answer #{self.calls} to: {str(prompt)[:40]})"})()
class TinyVectorStore:
"""Keyword-overlap "search", shaped like a LangChain vector store."""
def __init__(self):
self.texts = []
def add_texts(self, texts):
self.texts.extend(texts)
def similarity_search(self, query, k=3):
words = set(query.lower().split())
ranked = sorted(self.texts, key=lambda t: -len(words & set(t.lower().split())))
return [NS(page_content=t) for t in ranked[:k]]
class RagFacade:
def __init__(self, llm, vector_store):
self._llm = llm
self._store = vector_store
def ingest(self, text: str):
chunks = [p.strip() for p in text.split("\n\n") if p.strip()]
self._store.add_texts(chunks)
def ask(self, question: str) -> str:
docs = self._store.similarity_search(question, k=1)
context = "\n\n".join(d.page_content for d in docs)
prompt = f"Answer using only this context:\n{context}\n\nQuestion: {question}"
print("--- prompt sent to the LLM ---\n" + prompt + "\n------------------------------")
return self._llm.invoke(prompt).content
notes = """A Singleton makes sure a class has only one instance.
A Facade gives one simple entry point to a complicated system.
An Adapter makes one interface fit another."""
rag = RagFacade(FakeLLM(), TinyVectorStore())
rag.ingest(notes)
print(rag.ask("What does a Facade give?"))
Downloads:
0.no_facade.py ·
1.facade.py ·
2.rag_facade.py