Foundation 1 of 6 · 25 min

From Requirements to Classes

Every LLD answer starts the same way: read the requirements, pull out the nouns and verbs, and decide who owns which responsibility.

✓ Turn a prompt into entities, methods and relationships✓ Choose composition over inheritance and say why✓ Spot a class that owns too much
Say it in 30 seconds
  • 4 scoresRequirements, model, change, a working core
  • 5 to 8Classes that usually suffice for a classic question
  • 1 sentenceWhat a class must protect, or merge it away
Show what to say

“I start an LLD answer by reading the requirements twice: nouns become classes, verbs become methods, and I ask of each method who owns it. Rules live next to the data they read, I prefer composition to inheritance, and I stop adding classes when a new one has nothing to protect.”

Trade-offs

  • Composition vs inheritance: a part you swap, and a hybrid that is a new part, against a tree that multiplies with every new trait.
  • Composition vs dependency: composition creates and owns the part, so a test cannot swap it; a dependency is handed in, so a fake fits.
  • Few classes vs a class per noun: five to eight classes with a reason each beat Manager and Factory suffixes that protect nothing.

Bring it up before they ask

Say which single class you would edit if a rule changed (students get 7 days instead of 14), because a rule written in two places is how a god class bites.

0 of 0 sections ticked
30-second answer

Nouns become classes, verbs become methods, and every method gets one question: who owns this? Keep each rule next to the data it reads, prefer parts you swap over family trees, and stop adding classes when a new one protects nothing.

What an LLD round scores

The interviewer is watching how you decide, not whether your code compiles in their head. Four things earn points:

  • Requirements: you ask about limits and scope before you draw.
  • The model: sensible nouns, honest relationships.
  • Change: you name the requirement most likely to change and show the one place it lands.
  • A working core: one scenario, end to end, with edge cases said out loud.

This lesson follows a lending library, from one class that does everything to classes that each own one thing.

Nouns and verbs from a prompt

Highlight the nouns (classes) and the verbs (methods).

A library lends books to members. A member may hold three books at a time, each for 14 days. Returning late costs 0.50 per day.

In the promptBecomesWhy
libraryLibraryThe entry point
books, membersBook, MemberEach has data and rules
lends, returnsborrow, giveBackVerbs are methods
holds a book for 14 daysLoanA link with its own data is a noun

A noun with no data to protect and no behaviour is just a field. A day or a fee is a number.

Who owns the behaviour

In god-library-before.js one class does everything. Then students get 7 days instead of 14. The 14 lives in two places, so the email and the fee disagree. That is a god class: many reasons to change.

The fix is moving each rule next to the data it reads. In library-after.js a Loan knows its due day, a Member knows their limit, and Library only coordinates. Students are one new Member.

Rule of thumb: a method that mostly reads another class's fields belongs on that class.

// One class that knows stock, members, loan rules, fees and email.
export class Library {
  constructor() {
    this.books = new Map(); // isbn -> { title, copies }
    this.members = new Map(); // id -> { name, loans: [{ isbn, day }] }
    this.outbox = [];
  }
  addBook(isbn, title, copies) { this.books.set(isbn, { title, copies }); }
  join(id, name) { this.members.set(id, { name, loans: [] }); }

  borrow(memberId, isbn, day) {
    const member = this.members.get(memberId);
    const book = this.books.get(isbn);
    if (!member || !book || book.copies === 0) return false;
    if (member.loans.length >= 3) return false; // limit: hard-wired here
    book.copies -= 1;
    member.loans.push({ isbn, day });
    this.outbox.push(`${member.name}: ${book.title} is due on day ${day + 14}`); // 14 days, once
    return true;
  }

  giveBack(memberId, isbn, day) {
    const member = this.members.get(memberId);
    const i = member ? member.loans.findIndex((l) => l.isbn === isbn) : -1;
    if (i < 0) return -1;
    const [loan] = member.loans.splice(i, 1);
    this.books.get(isbn).copies += 1;
    return Math.max(0, day - (loan.day + 14)) * 0.5; // 14 days again, plus the fee rule
  }
}

Composition over inheritance

Think of a toy car with a swappable battery pack. The car has a battery. That is composition. Inheritance says "is a", and breaks fast: an ElectricCar is a Car until fillUp calls refuel() on it.

In inheritance-before.js every new trait multiplies the tree: electric, hybrid, truck, electric truck. In composition-after.js a vehicle has an energy source, and a hybrid is just a new part.

Inherit only when every subclass works anywhere the parent does (Liskov substitution).

// "An electric car is a car" reads fine until someone calls refuel().
export class Vehicle {
  constructor(plate) { this.plate = plate; }
}

export class Car extends Vehicle {
  refuel() { return `${this.plate}: petrol in the tank`; }
}

export class ElectricCar extends Car {
  refuel() { throw new Error('electric cars have no tank'); } // breaks every caller of Car
  charge() { return `${this.plate}: plugged in`; }
}

export const fillUp = (vehicles) => vehicles.map((v) => v.refuel());

The relationships you draw

A class diagram is a thinking tool, so draw it in the order you think. Step through the four stages: each one pulls out a noun, a relationship that carries data, or a collaborator.

On a whiteboard, write the one class you cannot avoid, add the nouns, then ask which relationships hold data of their own.

Stage 1 of 4

One class does everything

The naive version: stock, members, loan rules, fees and email all live in Library. It works, and every new rule means editing the same class.

A library design, grown in four stagesStage one is a single class that does everything. Each later stage pulls out a noun, then a relationship that carries data, then a collaborator the library is handed. Library composes Book, aggregates Member, Member composes Loan, Loan refers to a Book, and Library depends on a Notifier only while it sends.Librarybooks: Map<isbn, Book>members: Map<id, Member>borrow(memberId, isbn, day)giveBack(memberId, isbn, day)

Stage one is a single class that does everything. Each later stage pulls out a noun, then a relationship that carries data, then a collaborator the library is handed. Library composes Book, aggregates Member, Member composes Loan, Loan refers to a Book, and Library depends on a Notifier only while it sends.

  • ◆ composition (owns)
  • ◇ aggregation
  • △ inheritance / implements (dashed)
  • → association · ⇢ dependency

What each line means in code

Each line is a question about lifetime.

RelationshipAskIn the code
AssociationDoes A know B?A field or argument holding B
AggregationA holds B, but B outlives AA field filled from outside
CompositionA creates and owns Bnew B inside A
DependencyDoes A use B for one call?A parameter or a local

relationships.js has one of each.

In an interview, composition against dependency is the one that matters. It decides who creates an object and how a test replaces it.

// The four lines you draw, as they look in code.
export class Notifier {
  send(to, text) { return `to ${to}: ${text}`; }
}
export class Book {
  constructor(isbn, title) { this.isbn = isbn; this.title = title; }
}
export class Member {
  constructor(id, name) { this.id = id; this.name = name; }
}
export class Loan {
  constructor(book, member, day) { this.book = book; this.member = member; this.day = day; } // association: knows both, owns neither
}

export class Library {
  constructor() { this.books = []; this.members = []; this.loans = []; }

  addBook(isbn, title) { // composition: created here, lives and dies with this library
    const book = new Book(isbn, title);
    this.books.push(book);
    return book;
  }
  join(member) { this.members.push(member); } // aggregation: made elsewhere, outlives this library

  lend(member, book, day, notifier) { // dependency: the notifier is only used during this call
    const loan = new Loan(book, member, day);
    this.loans.push(loan);
    return notifier.send(member.name, `borrowed ${book.title}`);
  }
}

Encapsulation and tell, don't ask

In library-after.js, member.loans.push(loan) reaches into a member and decides for it. That is asking, and the limit rule now lives in whoever calls.

reach-in-before.js shows the cost: the desk checks the limit, the kiosk forgets, and one member holds two books on a limit of one. In tell-after.js the member hides its list and exposes borrow, so no caller can skip the rule.

Rule of thumb: if two callers need the same if on an object's fields, move the if into the object.

// The rule lives in the callers, so every caller has to remember it.
export class Member {
  constructor(limit = 3) { this.limit = limit; this.loans = []; }
}

export function borrowAtDesk(member, isbn) {
  if (member.loans.length >= member.limit) return false;
  member.loans.push(isbn);
  return true;
}

export function borrowAtKiosk(member, isbn) {
  member.loans.push(isbn); // someone forgot the limit check, and nothing stops them
  return true;
}

The too-many-classes trap

A class earns its place when it has state to protect, behaviour that would be duplicated, or a requirement that varies there. Otherwise it is a function or a field.

Nothing there protects anything. Compare Loan.feeAt(day): one method, on the one class that knows the due day. Five to eight classes usually suffice, as in the parking lot question.

LoanFeeCalculatorFactory -> LoanFeeCalculator -> FeeResult
BookManager, MemberManager, LoanManager, LibraryManager

Try it

Change Ana's limit, make Sam a 14-day member, or move a return day, and run it. Each change touches one number in one place.

try-it.js
// Change a limit, a loan period or a day, then press Run.
const lib = new Library();
lib.addBook('b1', 'Dune', 1);
lib.addBook('b2', 'Emma', 2);
lib.join(new Member('ana', 'Ana', 1));          // limit 1; try 2
lib.join(new Member('sam', 'Sam', 3, 7));       // a student: 7 days

console.log(lib.borrow('ana', 'b1', 0) !== null);  // true
console.log(lib.borrow('ana', 'b2', 0) !== null);  // false: Ana is at her limit
console.log(lib.borrow('sam', 'b2', 0).dueDay);    // 7
console.log(lib.giveBack('ana', 'b1', 20));        // 3: six days late at 0.5
console.log(lib.giveBack('sam', 'b2', 10));        // 1.5: three days late

Follow-up questions

How do you decide what becomes a class?

It has data to protect or behaviour that would be duplicated. A link with its own data, like a loan, counts. The rest are fields.

When is inheritance the right choice?

When every subclass can stand in for the parent and the tree stays shallow. If you override a method to throw, use a part instead.

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. A prompt says: a member borrows a book for 14 days, and late returns cost 0.50 a day. Which phrase most deserves its own class?

Code it yourself

Write the class in the browser editor, in JavaScript or Python. Each exercise runs against hidden tests, and the step-by-step solution unlocks after a wrong submission.

Next: Good classes are easy to extend. SOLID names the five ways they stop being easy.