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.
§◇ · YOUR DESK
Today
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.
| Set | Topic | The skill it builds | Anchor questions | Done |
|---|---|---|---|---|
| L0 | Objects from zero | classes, objects, self, identity vs equality, encapsulation as rule-guarding | BankAccount · Money · LibraryCard | L0 ✓ |
| L1 | The four pillars | abstraction/interfaces, inheritance, polymorphism, composition-over-inheritance, UML-lite | PaymentMethod · Notifier · the engine swap | L1 ✓ |
| L2 | Good design vocabulary + SOLID | coupling/cohesion, DRY/KISS/YAGNI, each SOLID principle as violation → refactor | god-class autopsies | — |
| L3 | The everyday six | Strategy · Factory · Builder · Singleton (and sins) · Observer · Decorator | pricing engines, notifiers, config builders | — |
| L4 | The situational eight | State · Command · Adapter · Facade · Proxy · Chain · Template · Composite | vending states, undo stacks, middleware | — |
| L5 | The method + first classics | scope freeze → nouns/verbs → patterns justified → skeleton → extension test | Tic-Tac-Toe · Parking Lot · Vending Machine | — |
| L6 | Core classics | one dominant pattern each — recognition training | Elevator · Snake & Ladder · Library / Hotel booking | — |
| L7 | Offer-deciders | multi-pattern composition under time pressure | Splitwise · BookMyShow · Logger · Rate Limiter · Chess | — |
| L8 | Concurrency band | threads, races, locks vs atomics, immutability, safe publication | thread-safe LRU · connection pool · producer-consumer | — |
| L9 | Bosses | end-to-end designs graded under extension pressure | Food delivery · Payment flow · In-memory DB | — |
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
- 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.
- 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.
- 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.
- 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.
- Code interfaces first, then concretes (15–20 min). A working thin slice beats a complete skeleton. Narrate decisions, not syntax.
- 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.
- Edges and tests if time. Concurrency mention, invalid inputs, capacity limits.
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
§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.
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”.
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.
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”.
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.
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.
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
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.
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.
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.
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.
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.
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
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:
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
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.
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.
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.
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
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.
// 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.