LLD_LAB

LLD Lab

Low-level design, one set at a time: SOLID and the patterns as violation→refactor pairs, then the machine-coding classics with staged model answers — attempt each stage before you open it. Your journal and re-design schedule live in this page and persist in this browser.

TRACK: SETS L0 → L7 DSA LAB → HLD LAB →

§◇ · YOUR DESK

Today

understood
designed
re-designs due
journal answered
cards to clear

Due for re-design

§00 · THE FULL MAP — MACHINE CODING

The LLD atlas — every set, one table

Low-level design rounds grade one thing above all: when the interviewer says “now also support X”, does your design answer with a new class or with painful edits? Everything in this track builds toward passing that extension test. Learn top to bottom; each set assumes the ones before it.

SetTopicThe skill it buildsAnchor questionsDone
L0Objects from zeroclasses, objects, self, identity vs equality, encapsulation as rule-guardingBankAccount · Money · LibraryCardL0 ✓
L1The four pillarsabstraction/interfaces, inheritance, polymorphism, composition-over-inheritance, UML-litePaymentMethod · Notifier · the engine swapL1 ✓
L2Good design vocabulary + SOLIDcoupling/cohesion, DRY/KISS/YAGNI, each SOLID principle as violation → refactorgod-class autopsies
L3The everyday sixStrategy · Factory · Builder · Singleton (and sins) · Observer · Decoratorpricing engines, notifiers, config builders
L4The situational eightState · Command · Adapter · Facade · Proxy · Chain · Template · Compositevending states, undo stacks, middleware
L5The method + first classicsscope freeze → nouns/verbs → patterns justified → skeleton → extension testTic-Tac-Toe · Parking Lot · Vending Machine
L6Core classicsone dominant pattern each — recognition trainingElevator · Snake & Ladder · Library / Hotel booking
L7Offer-decidersmulti-pattern composition under time pressureSplitwise · BookMyShow · Logger · Rate Limiter · Chess
L8Concurrency bandthreads, races, locks vs atomics, immutability, safe publicationthread-safe LRU · connection pool · producer-consumer
L9Bossesend-to-end designs graded under extension pressureFood delivery · Payment flow · In-memory DB
Where LLD rounds actually happen

Amazon (SDE2+ loops, usually before the HLD round), Atlassian, Uber, Flipkart/Swiggy-tier product companies — almost always as live machine coding. Google and Meta skip the explicit round, but every hour here still pays: this vocabulary is how HLD components are described.

§★ · THE MACHINE-CODING HOUR

The LLD interview script

  1. Freeze the scope (5 min). Enumerate the use cases out loud, and ask the one question that decides your abstractions: “what extensions are likely?” Their answer tells you where to put the interfaces.
  2. Nouns → entities, verbs → behaviors (5 min). Read the requirements twice: once circling nouns (candidate classes), once circling verbs (candidate methods). Say which nouns are real entities and which are just attributes.
  3. Relationships + a quick class diagram (5 min). Has-a vs is-a, cardinalities (one Building has many Floors has many Slots). Prefer has-a; inheritance must earn its place.
  4. Allocate behavior and name each pattern WITH its justification (5–10 min). “Pricing varies by vehicle type and will grow — Strategy.” The pattern name without the reason scores nothing.
  5. Code interfaces first, then concretes (15–20 min). A working thin slice beats a complete skeleton. Narrate decisions, not syntax.
  6. The extension test (5–10 min). They WILL say “now add X”. If step 4 was right, your answer is a new class implementing an existing interface — Open/Closed demonstrated live. This moment is the round’s real grade.
  7. Edges and tests if time. Concurrency mention, invalid inputs, capacity limits.
What’s actually graded

Scope discipline (step 1), abstraction placement (step 4), the extension answer (step 6), and whether the code runs. Beautiful UML with broken code loses to ugly diagrams with a working slice.

§★ · A REQUIREMENT WALKS IN

The signal router — requirement phrase → design move

1. “We might add more X later” (payment methods, pricing rules, notification channels) → Strategy — the behavior becomes a pluggable interface
2. Creating objects is conditional or complicated → Factory (which one) · Builder (how, step by step)
3. When one thing changes, many others must react → Observer — publishers don’t know their subscribers
4. Features stack in layers (logging + retry + compression…) → Decorator — wrap, don’t subclass-explode
5. Behavior depends on what MODE the object is in → State — one class per mode, transitions explicit
6. Undo / redo / queue / schedule operations → Command — reify the action as an object
7. Incompatible interface, or a subsystem too messy to expose → Adapter · Facade
8. “Exactly one of these, shared everywhere” → Singleton — then immediately name its sins (hidden coupling, testing pain)

§L0.1 · SET L0 — OBJECTS FROM ZERO

What an object even is — and why anyone bothered

Start with the problem, not the feature. Every program is two things: data (numbers, text, lists) and functions that change the data. In a small script they can live loose — a few variables here, a few functions there. As a program grows, a disease sets in: many functions touch the same data, and nothing stops any of them from breaking its rules. One day balance is −500 and forty functions could have done it.

OOP is one move against that disease: bundle a piece of data together with the only functions allowed to touch it, into a single unit — and give the unit a name. The design of such a unit is a class. Each unit actually built from the design is an object.

Now the real-life picture. A bank designs ONE model of ATM: the screen, the keypad, the cash tray, and — most importantly — the rules of operation (you cannot withdraw more than the tray holds). Then it builds thousands of physical machines from that one design. Every machine behaves the same way, but each holds its own cash. Pressing “withdraw” in Delhi does not move a rupee in Mumbai. The design = the class. Each street machine = an object.

class BankAccount the design: fields + rules __init__ · deposit · withdraw object #1 owner: 'Deep' · balance: 500 object #2 owner: 'Asha' · balance: 0
One design, many built things. Same methods on both, but each object carries its own data — touching one never touches the other.

The vocabulary, once, in plain words: a field (attribute) is something an object knows (its owner, its balance). A method is something it can do (deposit). The constructor (__init__) is its birth certificate — the code that runs once when the object is created and fills in its starting data. And self is the simplest of all: “the particular object this call is about.”

class BankAccount:
    def __init__(self, owner):      # the constructor: runs at birth
        self.owner = owner          # THIS object's own data
        self.balance = 0

    def deposit(self, amount):
        self.balance += amount

a = BankAccount("Deep")
b = BankAccount("Asha")
a.deposit(500)
print(a.balance, b.balance)         # 500 0 — two separate boxes

The one line that demystifies self forever: a.deposit(500) is just polite spelling for BankAccount.deposit(a, 500). Python passes the object in front of the dot as the first argument. That argument is called self by convention — it could be named anything, and it always means “this one”.

STEP THROUGH · THE LIFE OF AN OBJECTstep 0

    

Check yourself: after the code above runs, what does b.deposit(200); print(a.balance) show?

500 — untouched. Inside that call, self points at b’s box; a’s box is never visited. If this feels obvious, good: that instinct IS object thinking.

Going deeper — what Python actually does under the hoodoptional depth

An object in Python is essentially a small dictionary of its attributes plus a link to its class. a.balance means: look in a’s own dict first, then in the class. That is why two objects can hold different data while sharing one set of methods — the data lives per-object, the methods live once, on the class.

And a fact that will pay off later: everything in Python is an object — integers, strings, functions, even classes themselves. type(5) is a class. You have been using objects all along; now you get to design them.

§L0.2 · SET L0 — OBJECTS FROM ZERO

Two homes for data — and the bug that lives between them

Concept first. A class can hold two kinds of data, and confusing them causes one of the most common bugs in beginner OOP. Instance data belongs to one object: written as self.something, created inside __init__, different for every object. Class data belongs to the design itself: written directly in the class body, and — this is the point — shared by every object built from it.

Real life: every library membership card carries its own member name and its own list of borrowed books (instance data). But all cards share the library’s name and the borrow limit “max 3 books” (class data). If the library raises the limit to 5, it changes for every card at once — that’s exactly what shared means.

class LibraryCard:
    LIBRARY = "Indiranagar Public Library"   # class data: shared by ALL cards
    MAX_BOOKS = 3

    def __init__(self, member):
        self.member = member                 # instance data: this card only
        self.borrowed = []                   # created fresh PER OBJECT — crucial

    def borrow(self, book):
        if len(self.borrowed) >= LibraryCard.MAX_BOOKS:
            raise ValueError("borrow limit reached")
        self.borrowed.append(book)

Now the famous bug. Move borrowed = [] up into the class body, and it becomes ONE shared list — every card appends into the same object. Why? You met the reason in the DSA lab’s §F.4: assignment copies arrows, not things. A mutable class attribute is one thing with many arrows pointing at it.

Check yourself: with borrowed = [] as a CLASS attribute, card1.borrow("Gitanjali") runs. What does card2.borrowed show?

["Gitanjali"] — card2 sees card1’s book, because there is only one list in existence and both cards’ lookups find it on the class. The fix is one line: create the list in __init__ so each card gets its own. Same disease, same cure as the [[0]*n]*m grid trap.

⚠ The sibling trap — mutable default arguments

def __init__(self, borrowed=[]) has the same disease from a different door: Python builds that default list ONCE, when the function is defined, and every object that doesn’t pass its own list shares it. The idiom: default to None, then self.borrowed = borrowed if borrowed is not None else [].

The rule to keep: constants and truly-shared settings → class body, in CAPS. Anything an object owns — especially anything mutable — → created in __init__. When in doubt, it belongs in __init__.

§L0.3 · SET L0 — OBJECTS FROM ZERO

“The same object” vs “an equal object”

Concept first. There are two different questions you can ask about two objects, and beginner code goes wrong by treating them as one. Identity: are these literally the same box in memory? (Python: is.) Equality: do these two boxes hold matching contents? (Python: ==.) They are allowed to disagree — and often should.

Real life: two crisp ₹500 notes. Equal in every way that matters for buying chai — but they are not the same note; each has its own serial number, its own paper. Meanwhile your Aadhaar card and a photocopy of it: same information, and yet for identity purposes only one is “the real one”.

₹500 note serial: 2AB 114377 ₹500 note serial: 7QX 990125 equal value? YES (==) same note? NO (is) two questions, two answers — Python keeps them separate on purpose
Equality asks about contents; identity asks “is this literally the same box in memory?” Every object supports both questions, and they are allowed to disagree.

The Python surprise that teaches the whole topic: by default, == on your own classes falls back to identity. Two freshly made Money(500) objects are not equal until you teach the class what equality should mean:

class Money:
    def __init__(self, rupees):
        self.rupees = rupees

m1, m2 = Money(500), Money(500)
print(m1 == m2)          # False!  default == is identity

class Money:
    def __init__(self, rupees):
        self.rupees = rupees
    def __eq__(self, other):                 # teach equality
        return isinstance(other, Money) and self.rupees == other.rupees
    def __hash__(self):                      # the partner rule — see below
        return hash(self.rupees)

print(Money(500) == Money(500))              # True — by value now

Why __hash__ came along: the moment you define __eq__, Python disables hashing for the class — because sets and dicts file things on shelves by hash (§F.3 of the DSA lab), and the law of that library is: equal things must land on the same shelf. Define what “equal” means, and you owe the shelf-number rule too: equal objects ⇒ equal hash. Hash the same fields you compare.

The design vocabulary this unlocks: a value object is defined by its contents — Money, a date, a coordinate. Two with the same contents are interchangeable, so give them __eq__ by value. An entity is defined by its identity — a BankAccount, a User. Two accounts with identical balances are still different accounts; equality stays identity. Deciding “value or entity?” for each class you design is a real LLD interview skill, and you now own it.

Check yourself: a = [1,2]; b = [1,2] — what do a == b and a is b print, and why?

True, then False. Lists ship with contents-equality built in (Python defined __eq__ for you), but they are still two separate boxes. Exactly the two-notes picture.

§L0.4 · SET L0 — OBJECTS FROM ZERO

Encapsulation — guarding rules, not hiding fields

Concept first, because this one is usually taught backwards. Encapsulation is not the ritual of making fields private and writing getters. It is this: every object has rules about its own data that must never break — “balance is never negative”, “a booking never exceeds capacity”. Such an always-true rule is called an invariant. The ONLY reliable way to protect an invariant is to force every change through methods that check it. If any code anywhere can write the field directly, your rule is a hope, not a guarantee.

Real life: a bank vault. Customers never walk into the vault and rearrange cash — they talk to a teller, and the teller enforces the rules (“insufficient funds”). The teller window is the method; the vault is the private field. The building is designed so there is no other door.

BankAccount object _balance (the vault) deposit() withdraw() callers reaching into the vault directly — the door the design closes
Every change walks through a teller window (a method) that checks the rules. The vault itself is never handed to callers — that is all encapsulation is.
class BankAccount:
    def __init__(self, owner):
        self.owner = owner
        self._balance = 0                    # _ means: staff door, keep out

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance += amount

    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("insufficient funds")   # the invariant, guarded
        self._balance -= amount

    @property
    def balance(self):                       # read-only window into the vault
        return self._balance

Python’s honesty policy: there is no true “private”. _balance is a convention (“staff only”), and even __balance (double underscore) is only name-mangled, not locked. Python’s culture says: mark the door clearly, and trust code review to keep people out. The @property gives outsiders a clean read-only view — and, beautifully, it means you can START with a plain public attribute and upgrade to a validated property later without changing a single caller. That upgrade path is why Python doesn’t need Java’s getter/setter ritual.

⚠ The leak nobody notices

A method that returns the internal list itself hands the vault keys out the window: return self._transactions lets the caller append fake entries. Return a copy (list(self._transactions)) or a tuple. Same aliasing arrow-picture as §F.4 — encapsulation can be broken by a return statement as easily as by an assignment.

Check yourself: withdraw(5000) on a balance of 500 — should it return None quietly, return False, or raise? Argue it.

Raise. A broken rule should be LOUD at the moment it breaks — silent None travels ten function-calls away before something else crashes, and the crime scene is cold. “Fail fast” is invariant-thinking applied to time.

§L0.5 · BEFORE MOVING ON + INTERROGATION

Traps, signals & interrogation

Interrogation — answer in your journal

L0.1

Explain, using the box picture, why a.deposit(500) cannot possibly touch b — where exactly does self point during that call, and who decided?

Hint → answer sketchjournal first

Hint: Rewrite the call without the dot.

Sketch: a.deposit(500) is BankAccount.deposit(a, 500) — Python passes the object before the dot as the first argument. self is bound to a’s box for the whole call; b’s box is simply never named anywhere in it.

L0.2

Money(500) == Money(500) is False out of the box. Why? Which two methods fix it, what law connects them, and which DSA-lab picture explains that law?

Hint → answer sketchjournal first

Hint: What does == fall back to when the class says nothing?

Sketch: Default == is identity — two boxes, so False. Define __eq__ (compare contents) and __hash__ (hash those same contents). The law: equal objects must hash equal — because sets/dicts file by hash (§F.3’s shelves), and equal things must land on the same shelf to ever be found.

L0.3

Build the shared-list LibraryCard bug from memory, predict its exact wrong output for two cards, then fix it and say WHY the fix works using the arrow picture.

Hint → answer sketchjournal first

Hint: How many list objects exist in the buggy version?

Sketch: One. borrowed = [] in the class body builds a single list; every card’s lookup misses its own dict and finds the class’s — many arrows, one thing. Creating self.borrowed = [] in __init__ builds a fresh list per object: one arrow, one thing, each.

L0.4

A teammate writes acc._balance -= fee directly and the tests pass. In three sentences, argue why this is still a bug worth blocking in review.

Hint → answer sketchjournal first

Hint: What did withdraw() know that this line skips?

Sketch: The write bypasses the single choke point where the “never negative / sufficient funds” rule lives, so the invariant now depends on every caller’s memory instead of one method’s code. Tomorrow’s validation, logging, or notification added to withdraw() silently won’t apply here. Tests passing today only means today’s inputs were kind.

§L1.1 · SET L1 — THE FOUR PILLARS

The four pillars — one story before the details

With objects in hand, four ideas make a system of objects manageable. Here they are in one household story, so the coming sections have a skeleton to hang on:

Encapsulation you already own from L0: every appliance guards its own insides — you press buttons on the microwave; you never rewire it mid-cook. Abstraction is the wall socket: a simple promise (230V, this shape) hiding a power grid behind it — you design against the promise, not the machinery. Inheritance is “a new model based on an old one”: this year’s washing machine is last year’s, plus a steam mode — same everything else. Polymorphism is what makes the socket idea powerful: MANY different devices honor the same plug promise, so the wall never needs to know what arrived — one socket, a thousand behaviors.

And the working lesson this set builds to, the one that decides real interviews: when you must choose between “X is a Y” (inheritance) and “X has a Y” (composition), composition wins whenever you’re unsure — because a slot can be re-plugged at runtime, while a family tree is forever. Section L1.4 shows the failure that makes this rule stick.

In plain English — why exactly four, and are they sacred?optional depth

Nothing sacred — “four pillars” is just a teaching tradition. The honest grouping: encapsulation and abstraction are about hiding (one hides data behind rules, the other hides machinery behind promises); inheritance and polymorphism are about reuse and variation (one shares structure, the other varies behavior behind a shared face). If you remember only one sentence: depend on promises, guard your data, and prefer slots to family trees. Every principle and pattern for the rest of this lab is that sentence, elaborated.

§L1.2 · SET L1 — THE FOUR PILLARS

Abstraction — code that depends on a promise

Concept first. An interface is a written promise-list: “whatever you are, you can pay(amount)”. Code that depends only on the promise works with every keeper of the promise — including keepers that don’t exist yet. That last clause is the entire magic, so read it twice: the future-proofing is the point.

the socket standard promise: 230V, this shape lamp1962 laptop charger2020 future device2035 — still fits
The wall was built decades before the laptop existed, yet the laptop works — because both sides depend only on the promise, never on each other.

In Python, formal promises are abstract base classes; a class that inherits one MUST implement the promised methods or it cannot even be instantiated:

from abc import ABC, abstractmethod

class PaymentMethod(ABC):                    # the socket standard
    @abstractmethod
    def pay(self, amount): ...

class UPI(PaymentMethod):
    def pay(self, amount):
        print(f"UPI request for ₹{amount}")

class Card(PaymentMethod):
    def pay(self, amount):
        print(f"Charging card ₹{amount}")

def checkout(cart_total, method: PaymentMethod):
    # this function knows ONLY the promise. Nothing else.
    method.pay(cart_total)

Watch the extension test pass in week one: next month the business adds wallets. You write class Wallet(PaymentMethod) with its own pay() — and checkout is not edited, not retested for old paths, not even reopened. New behavior arrived; existing code stood still. Interviews grade exactly this moment.

Check yourself: the anti-pattern: checkout does if isinstance(method, UPI): ... elif isinstance(method, Card): ... — what breaks when Wallet arrives?

checkout itself must be edited — the promise was ignored and the machinery leaked in. Every new method means reopening tested code. An isinstance-ladder inside business logic is the #1 sign an interface is missing.

Going deeper — Python’s informal interfaces (duck typing)optional depth

Python also honors unwritten promises: if an object simply HAS a pay() method, checkout works — no ABC needed. “If it walks like a duck and quacks like a duck…” This is duck typing, and it is idiomatic Python. Use the ABC when you want the promise written down and enforced (a team, an interview whiteboard); rely on ducks for small internal code. In interviews, write the ABC — it shows you know where the contract lives.

§L1.3 · SET L1 — THE FOUR PILLARS

Inheritance and polymorphism — one call, many behaviors

Concept first. Inheritance: build a new class as “an existing class, keep most, change some”. The child is a parent — anywhere a parent is expected, a child may stand. Overriding: the child replaces a method with its own version. And polymorphism is what happens at the call site: the code says n.send(msg) once, and whichever object is actually standing there answers in its own way. Python decides at runtime, from the actual object — this is called dynamic dispatch, and it is why one loop can drive a hundred behaviors.

Real life: a company’s alert system. Policy says “notify everyone on the on-call list” — it does not say how each person prefers to be reached. One colleague gets email, one gets SMS, one gets a push notification. The POLICY is one loop; the VARIATION lives in each channel:

class Notifier:
    def send(self, user, msg):
        raise NotImplementedError

class EmailNotifier(Notifier):
    def send(self, user, msg):
        print(f"email to {user}: {msg}")

class SMSNotifier(Notifier):
    def send(self, user, msg):
        print(f"sms to {user}: {msg}")

class PushNotifier(Notifier):
    def send(self, user, msg):
        print(f"push to {user}: {msg}")

def alert(oncall, channels, msg):
    for user in oncall:
        for ch in channels:              # ONE call site…
            ch.send(user, msg)           # …many behaviors, chosen at runtime

super(), in plain words: “run the parent’s version too, then add my part.” A Manager’s __init__ calls super().__init__(name, id) so the Employee half of it is set up by the code that owns that job — never duplicate the parent’s setup by hand.

When is inheritance the right tool? Three checks, all required: the sentence “X is a Y” rings true out loud; the child keeps every promise the parent makes (no overriding a method to raise “not supported” — that lie has a name, Liskov, coming in L2); and there is genuinely shared behavior worth inheriting. Fail any one → reach for the next section’s tool.

Check yourself: channels = [EmailNotifier(), SMSNotifier()] — how many times is the WORD send written in alert(), and how many behaviors run?

Written once, behaviors two (per user). That gap — one call site, n behaviors — is polymorphism’s entire value: adding PushNotifier changes the list you pass in, not the loop.

§L1.4 · SET L1 — THE FOUR PILLARS · THE FLAGSHIP LESSON

Composition over inheritance — slots beat family trees

The failure story first, because the rule means nothing without the pain. You’re building game characters. Some walk, some fly, some swim — so you make Walker, Flyer, Swimmer subclasses. Then design asks for a character that flies AND swims. Then an armored flyer. Every combination of abilities demands its own class: n abilities → 2ⁿ classes, most of them copy-paste. Worse: a character can never change — the decision was welded into its family tree at birth.

inheritance: one class per combination Character Walker Flyer Swimmer FlyingSwimmer ArmoredFlyer… n abilities → 2ⁿ classes composition: slots + plug-ins Character move: ⟨slot⟩ · attack: ⟨slot⟩ FlyMove SwimMove n abilities → n small classes, combined freely
Freeze the decision into the family tree and every combination needs a new class. Put it in a slot and combinations are free — and swappable at runtime.

The composition move: stop asking “what IS this character?” and ask “what does it HAVE?” Give the class slots, and put behavior objects in the slots. Combinations become free; swapping becomes a one-line assignment — even mid-game:

class Engine(ABC):
    @abstractmethod
    def start(self): ...

class PetrolEngine(Engine):
    def start(self): return "vroom (petrol, 12 km/l)"

class ElectricEngine(Engine):
    def start(self): return "whirr (electric, 7 km/kWh)"

class Car:
    def __init__(self, engine: Engine):
        self.engine = engine             # the slot
    def drive(self):
        return self.engine.start()       # ask whatever is plugged in

car = Car(PetrolEngine())
car.drive()                              # vroom
car.engine = ElectricEngine()            # swap AT RUNTIME — no new class
car.drive()                              # whirr
STEP THROUGH · THE ENGINE SWAPstep 0

    

The decision rule, out loud: say the sentence. “A car IS an engine” — absurd → has-a → composition. “A manager IS an employee” — true, promises kept → is-a is fine. And when the sentence could go either way, choose composition: a slot keeps the decision changeable; a parent class does not. (Quiet spoiler: what you just built — a slot holding swappable behavior — has a formal name, Strategy. Set L3 will introduce it as an old friend.)

Check yourself: the interviewer says: “now cars can also have a sunroof, a music system, or both.” Inheritance answer vs composition answer?

Inheritance: SunroofCar, MusicCar, SunroofMusicCar… — the 2ⁿ explosion again. Composition: a features list (or named slots) on Car holding small feature objects; both = two entries in the list. New feature next month = one new class, Car untouched.

§L1.5 · SET L1 — THE FOUR PILLARS

The supporting cast — enums, statics, and just-enough UML

Enums — for fixed menus of values. Order status is exactly one of PLACED / PACKED / SHIPPED / DELIVERED. Model that with strings and a typo ("shiped") sails through every check, corrupting data quietly. An Enum makes the menu a real type: typos fail loudly at the line that wrote them, and your editor autocompletes the choices.

from enum import Enum

class OrderStatus(Enum):
    PLACED = "placed"
    PACKED = "packed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"

order.status = OrderStatus.SHIPPED        # not a string — a member of the menu
if order.status is OrderStatus.DELIVERED: # identity comparison — safe and fast
    ...

Class methods as named constructors. Money.from_paise(4999) reads better than Money(49.99) with a comment. A @classmethod receives the class itself and builds the object — you’ll meet this again as the Factory pattern’s little sibling.

Value object vs entity — now formalized (you met it in L0.3): value objects (Money, DateRange, Coordinates) compare by contents, are best made immutable, and Python’s @dataclass(frozen=True) writes the boilerplate for you. Entities (User, Order, Account) compare by identity and carry an id. In an interview, sorting your nouns into these two buckets is often the first minute of modeling.

UML-lite — the four arrows you may draw in an interview:

association — “knows about” · Member → Loan inheritance (hollow triangle) — “is a” · EBook ▷ Book composition (filled diamond at the owner) — “part of” · Library ◆ Shelf dependency (dashed) — “uses briefly” · Checkout ⤏ NotificationService
Four arrows cover 95% of interview whiteboards. Boxes get two compartments: name on top, key methods below. Timebox the drawing — five minutes, never twenty.

Boxes get two compartments — name, then the 3–4 methods that matter. Draw for five minutes maximum, then code; the diagram is a communication tool, not the deliverable. (The lab’s DESIGN sections will always show diagrams in exactly this dialect.)

Check yourself: Library ◆— Shelf vs Member —→ Book: why a filled diamond for one and a plain arrow for the other?

Shelves are PART OF the library — destroy the library and its shelves go with it (composition, filled diamond at the owner). A member merely KNOWS ABOUT the books they borrowed — books outlive membership (association, plain arrow). The arrow encodes lifetime and ownership, not just “uses”.

§L1.6 · BEFORE MOVING ON + INTERROGATION

Traps, signals & interrogation

Interrogation — answer in your journal

L1.1

Map the wall-socket story onto the payment code: what plays the socket, the devices, the 230V promise — and what code smell corresponds to “rewiring the wall for every new device”?

Hint → answer sketchjournal first

Hint: The promise is a method signature.

Sketch: Socket = PaymentMethod ABC; devices = UPI/Card/Wallet; the 230V promise = the pay(amount) signature and its meaning; rewiring the wall = isinstance-ladders inside checkout, which force edits for every new device. The socket works for 2035’s devices because both sides depend only on the promise.

L1.2

class Stack(list): show one concrete call a user can make that breaks stack-ness, then fix the design with composition and say what the fix hides.

Hint → answer sketchjournal first

Hint: What can every list do that no stack should?

Sketch: s.insert(0, x) — jumping the queue at the bottom. Fix: Stack HAS a list (self._items) and exposes only push/pop/peek. Composition lets you inherit nothing and expose exactly the promise a stack makes — the inner list becomes vault, not ancestry.

L1.3

The penguin problem: Bird has fly(); Penguin(Bird) overrides fly() to raise. Explain which pillar’s guarantee just died, and redesign the birds with composition so nobody lies.

Hint → answer sketchjournal first

Hint: What may code holding a Bird safely assume?

Sketch: Substitutability: any Bird can stand where Bird is expected — a raising fly() makes that false, so polymorphic code now needs isinstance checks, defeating the point. Redesign: Bird HAS a movement slot; FlyMove for sparrows, WaddleSwimMove for penguins. Every bird keeps every promise it actually makes.

L1.4

Design on paper (arrows only, 5 minutes): a food-court smart card system — Card, Vendor, Transaction, TopUpMachine. Which of the four arrows connects each pair, and why?

Hint → answer sketchjournal first

Hint: Ask lifetime and ownership for each pair.

Sketch: Card —→ Transaction: association (a card knows its transactions; they outlive nothing together… actually they belong to the card’s history — a ◆ is defensible if transactions die with the card; say your assumption out loud). Vendor ⤏ Card: dependency (touches it briefly to charge). TopUpMachine ⤏ Card: dependency. If you add Wallet-vs-Meal balance types: Balance as a value object inside Card (◆). The grading is the REASONS, not the arrows.

§∞ · RUN THE PROTOCOL ON EACH

Practice ladder

Protocol per design

Read only the requirements → 30-minute attempt on paper (diagram + key decisions) → open the staged model answer one fold at a time → journal (Requirements / Choices / Trade-offs / What I missed) → re-design blank on day +3 and day +10.

§∞ · REQUIREMENT → DESIGN MOVE

Signal drill — flashcards

Each card is a phrase from a real requirements conversation; answer with the design move it should trigger. The deck grows with every set; misses are saved and lead the next shuffle.

signal → move— left
signal

// This lab grows one set at a time — feed it via DESIGN-PROMPT.md and each set arrives with staged model answers, diagrams, ladder entries, flashcards, and journal drills.