Every case study in this course draws one, and every later pattern lesson does too. Here is how to read the six arrows, what the small numbers mean, and the five-minute version you can draw on a whiteboard.
You are given a library to model. Someone borrows a book, but the library owns three copies of it, a member can have two out at a time, and a late return costs money. Before you write any code, the interviewer says "draw me the classes". What goes on the board?
A class diagram is a map of the classes in a design and the relationships between them. It is a floor plan of the code, not a photograph: it shows which rooms exist and which doors connect them, and leaves the furniture out. The notation is a small slice of UML (the Unified Modeling Language). You need about six kinds of line, a handful of numbers, and the discipline to stop drawing.
This lesson starts from a working library in four languages, shows the diagram most people draw first and why nobody can read it, then goes through the six relationships one at a time, each pinned to the line of code that creates it. It ends with the five-minute subset for interviews and the mistakes that cost marks. The parking lot, the library and every other case study draw their classes the way this lesson teaches.
A class diagram is a floor plan of the code: classes are the rooms, relationships are the doors, and fields and getters are the furniture left out.
The first version: the code, and a diagram nobody can read#
Here is the library in code. Read it for what each class holds and how it comes to hold it, because that is exactly what a diagram's lines record. A book creates its copies, a loan is handed a copy and a member, a fine policy is passed in for one call, and one error extends another:
# library.py: the library loan, with every relationship the diagram will draw
from abc import ABC, abstractmethod
LOAN_DAYS = 14
class LoanError(Exception): # inheritance: NoCopyAvailable extends this
pass
class NoCopyAvailable(LoanError):
pass
class FinePolicy(ABC): # an interface: the contract a fine rule meets
@abstractmethod
def fine(self, days_late: int) -> int: ...
class PerDayFine(FinePolicy): # realisation: PerDayFine implements FinePolicy
def __init__(self, rupees_per_day: int) -> None:
self.rupees_per_day = rupees_per_day
def fine(self, days_late: int) -> int:
return self.rupees_per_day * days_late
class BookCopy:
def __init__(self, barcode: str) -> None:
self.barcode = barcode
class Book:
def __init__(self, title: str, barcodes: list) -> None:
self.title = title
self.copies = [BookCopy(b) for b in barcodes] # composition: the book creates and owns its copies
class Member:
def __init__(self, name: str) -> None:
self.name = name
class Loan:
def __init__(self, copy: BookCopy, member: Member, borrowed_day: int) -> None:
self.copy = copy # association: a reference to a copy that lives on without the loan
self.member = member # association: the same for the member
self.borrowed_day = borrowed_day
def fine(self, policy: FinePolicy, returned_day: int) -> int: # dependency: the policy is only a parameter
days_late = max(0, returned_day - (self.borrowed_day + LOAN_DAYS))
return policy.fine(days_late)
class Library:
def __init__(self) -> None:
self.books = {} # composition: the library creates its catalogue...
self.members = [] # aggregation: ...but members are created elsewhere and only joined
self.loans = [] # composition: the library creates its loans
def add_book(self, title: str, barcodes: list) -> None:
self.books[title] = Book(title, barcodes)
def join(self, member: Member) -> None:
self.members.append(member)
def lend(self, title: str, member: Member, day: int) -> Loan:
for copy in self.books[title].copies:
if not any(loan.copy is copy for loan in self.loans):
loan = Loan(copy, member, day)
self.loans.append(loan)
return loan
raise NoCopyAvailable(f"no free copy of {title}")
library = Library()
library.add_book("Godan", ["G1", "G2"])
asha, ravi, tara = Member("Asha"), Member("Ravi"), Member("Tara")
for member in [asha, ravi, tara]:
library.join(member)
print(f"Godan has {len(library.books['Godan'].copies)} copies")
first = library.lend("Godan", asha, 1)
print(f"{first.member.name} borrows {first.copy.barcode} on day 1")
second = library.lend("Godan", ravi, 2)
print(f"{second.member.name} borrows {second.copy.barcode} on day 2")
try:
library.lend("Godan", tara, 3)
except LoanError as e: # also catches NoCopyAvailable
print("rejected:", e)
print(f"Asha returns on day 20: fine {first.fine(PerDayFine(2), 20)}")// LibraryDemo.java: the library loan, with every relationship the diagram will draw
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
class LibraryDemo {
static final int LOAN_DAYS = 14;
static class LoanError extends RuntimeException { // inheritance: NoCopyAvailable extends this
LoanError(String message) { super(message); }
}
static class NoCopyAvailable extends LoanError {
NoCopyAvailable(String message) { super(message); }
}
interface FinePolicy { // an interface: the contract a fine rule meets
int fine(int daysLate);
}
static final class PerDayFine implements FinePolicy { // realisation: PerDayFine implements FinePolicy
private final int rupeesPerDay;
PerDayFine(int rupeesPerDay) { this.rupeesPerDay = rupeesPerDay; }
public int fine(int daysLate) { return rupeesPerDay * daysLate; }
}
static final class BookCopy {
final String barcode;
BookCopy(String barcode) { this.barcode = barcode; }
}
static final class Book {
final String title;
final List<BookCopy> copies = new ArrayList<>();
Book(String title, List<String> barcodes) {
this.title = title;
for (String b : barcodes) copies.add(new BookCopy(b)); // composition: the book creates and owns its copies
}
}
static final class Member {
final String name;
Member(String name) { this.name = name; }
}
static final class Loan {
final BookCopy copy; // association: a reference to a copy that lives on without the loan
final Member member; // association: the same for the member
final int borrowedDay;
Loan(BookCopy copy, Member member, int borrowedDay) { this.copy = copy; this.member = member; this.borrowedDay = borrowedDay; }
int fine(FinePolicy policy, int returnedDay) { // dependency: the policy is only a parameter
int daysLate = Math.max(0, returnedDay - (borrowedDay + LOAN_DAYS));
return policy.fine(daysLate);
}
}
static final class Library {
final Map<String, Book> books = new LinkedHashMap<>(); // composition: the library creates its catalogue...
final List<Member> members = new ArrayList<>(); // aggregation: ...but members are created elsewhere and only joined
final List<Loan> loans = new ArrayList<>(); // composition: the library creates its loans
void addBook(String title, List<String> barcodes) { books.put(title, new Book(title, barcodes)); }
void join(Member member) { members.add(member); }
Loan lend(String title, Member member, int day) {
for (BookCopy copy : books.get(title).copies) {
boolean out = loans.stream().anyMatch(l -> l.copy == copy);
if (!out) {
Loan loan = new Loan(copy, member, day);
loans.add(loan);
return loan;
}
}
throw new NoCopyAvailable("no free copy of " + title);
}
}
public static void main(String[] args) {
Library library = new Library();
library.addBook("Godan", List.of("G1", "G2"));
Member asha = new Member("Asha"), ravi = new Member("Ravi"), tara = new Member("Tara");
for (Member member : List.of(asha, ravi, tara)) library.join(member);
System.out.println("Godan has " + library.books.get("Godan").copies.size() + " copies");
Loan first = library.lend("Godan", asha, 1);
System.out.println(first.member.name + " borrows " + first.copy.barcode + " on day 1");
Loan second = library.lend("Godan", ravi, 2);
System.out.println(second.member.name + " borrows " + second.copy.barcode + " on day 2");
try {
library.lend("Godan", tara, 3);
} catch (LoanError e) { // also catches NoCopyAvailable
System.out.println("rejected: " + e.getMessage());
}
System.out.println("Asha returns on day 20: fine " + first.fine(new PerDayFine(2), 20));
}
}// library.cpp: the library loan, with every relationship the diagram will draw
#include <algorithm>
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
const int LOAN_DAYS = 14;
class LoanError : public std::runtime_error { // inheritance: NoCopyAvailable extends this
public:
using std::runtime_error::runtime_error;
};
class NoCopyAvailable : public LoanError {
public:
using LoanError::LoanError;
};
class FinePolicy { // an interface: the contract a fine rule meets
public:
virtual ~FinePolicy() = default;
virtual int fine(int daysLate) const = 0;
};
class PerDayFine : public FinePolicy { // realisation: PerDayFine implements FinePolicy
int rupeesPerDay_;
public:
explicit PerDayFine(int rupeesPerDay) : rupeesPerDay_(rupeesPerDay) {}
int fine(int daysLate) const override { return rupeesPerDay_ * daysLate; }
};
struct BookCopy {
std::string barcode;
};
struct Book {
std::string title;
std::vector<BookCopy> copies; // composition: the copies live inside the book, by value
Book(std::string t, const std::vector<std::string>& barcodes) : title(std::move(t)) {
for (const auto& b : barcodes) copies.push_back(BookCopy{b});
}
};
struct Member {
std::string name;
};
struct Loan {
const BookCopy* copy; // association: a pointer to a copy that lives on without the loan
const Member* member; // association: the same for the member
int borrowedDay;
int fine(const FinePolicy& policy, int returnedDay) const { // dependency: the policy is only a parameter
int daysLate = std::max(0, returnedDay - (borrowedDay + LOAN_DAYS));
return policy.fine(daysLate);
}
};
class Library {
public:
std::map<std::string, Book> books; // composition: the library creates its catalogue...
std::vector<const Member*> members; // aggregation: ...but members are created elsewhere and only joined
std::vector<Loan> loans; // composition: the library creates its loans
void addBook(const std::string& title, const std::vector<std::string>& barcodes) {
books.emplace(title, Book(title, barcodes));
}
void join(const Member& member) { members.push_back(&member); }
Loan lend(const std::string& title, const Member& member, int day) {
for (const BookCopy& copy : books.at(title).copies) {
bool out = std::any_of(loans.begin(), loans.end(), [&](const Loan& l) { return l.copy == © });
if (!out) {
loans.push_back(Loan{©, &member, day});
return loans.back();
}
}
throw NoCopyAvailable("no free copy of " + title);
}
};
int main() {
Library library;
library.addBook("Godan", {"G1", "G2"});
Member asha{"Asha"}, ravi{"Ravi"}, tara{"Tara"};
for (const Member* member : {&asha, &ravi, &tara}) library.join(*member);
std::cout << "Godan has " << library.books.at("Godan").copies.size() << " copies\n";
Loan first = library.lend("Godan", asha, 1);
std::cout << first.member->name << " borrows " << first.copy->barcode << " on day 1\n";
Loan second = library.lend("Godan", ravi, 2);
std::cout << second.member->name << " borrows " << second.copy->barcode << " on day 2\n";
try {
library.lend("Godan", tara, 3);
} catch (const LoanError& e) { // also catches NoCopyAvailable
std::cout << "rejected: " << e.what() << "\n";
}
std::cout << "Asha returns on day 20: fine " << first.fine(PerDayFine(2), 20) << "\n";
}// library.js: the library loan, with every relationship the diagram will draw
const LOAN_DAYS = 14;
class LoanError extends Error {} // inheritance: NoCopyAvailable extends this
class NoCopyAvailable extends LoanError {}
// FinePolicy is an interface: any object with fine(daysLate) meets the contract.
// JavaScript has no interfaces, so the contract is the method name.
class PerDayFine { // realisation: PerDayFine meets the FinePolicy contract
constructor(rupeesPerDay) {
this.rupeesPerDay = rupeesPerDay;
}
fine(daysLate) {
return this.rupeesPerDay * daysLate;
}
}
class BookCopy {
constructor(barcode) {
this.barcode = barcode;
}
}
class Book {
constructor(title, barcodes) {
this.title = title;
this.copies = barcodes.map((b) => new BookCopy(b)); // composition: the book creates and owns its copies
}
}
class Member {
constructor(name) {
this.name = name;
}
}
class Loan {
constructor(copy, member, borrowedDay) {
this.copy = copy; // association: a reference to a copy that lives on without the loan
this.member = member; // association: the same for the member
this.borrowedDay = borrowedDay;
}
fine(policy, returnedDay) { // dependency: the policy is only a parameter
const daysLate = Math.max(0, returnedDay - (this.borrowedDay + LOAN_DAYS));
return policy.fine(daysLate);
}
}
class Library {
constructor() {
this.books = new Map(); // composition: the library creates its catalogue...
this.members = []; // aggregation: ...but members are created elsewhere and only joined
this.loans = []; // composition: the library creates its loans
}
addBook(title, barcodes) {
this.books.set(title, new Book(title, barcodes));
}
join(member) {
this.members.push(member);
}
lend(title, member, day) {
for (const copy of this.books.get(title).copies) {
if (!this.loans.some((loan) => loan.copy === copy)) {
const loan = new Loan(copy, member, day);
this.loans.push(loan);
return loan;
}
}
throw new NoCopyAvailable(`no free copy of ${title}`);
}
}
const library = new Library();
library.addBook("Godan", ["G1", "G2"]);
const [asha, ravi, tara] = [new Member("Asha"), new Member("Ravi"), new Member("Tara")];
for (const member of [asha, ravi, tara]) library.join(member);
console.log(`Godan has ${library.books.get("Godan").copies.length} copies`);
const first = library.lend("Godan", asha, 1);
console.log(`${first.member.name} borrows ${first.copy.barcode} on day 1`);
const second = library.lend("Godan", ravi, 2);
console.log(`${second.member.name} borrows ${second.copy.barcode} on day 2`);
try {
library.lend("Godan", tara, 3);
} catch (e) {
if (!(e instanceof LoanError)) throw e; // also matches NoCopyAvailable
console.log("rejected:", e.message);
}
console.log(`Asha returns on day 20: fine ${first.fine(new PerDayFine(2), 20)}`);Godan has 2 copies
Asha borrows G1 on day 1
Ravi borrows G2 on day 2
rejected: no free copy of Godan
Asha returns on day 20: fine 10The fine, so the numbers make sense: a loan lasts 14 days, so Asha's loan from day 1 is due on day 15; she returns on day 20, which is 5 days late, and 5 × 2 rupees is 10. (This sketch does not model returns: a loan stays in the list. That keeps the code to what the diagram needs.)
Now draw it, the way most people do at first: a box per class with every field, and a plain line wherever two classes seem related:
%% title: The first diagram: boxes and unlabelled lines
classDiagram
class Library {
+books
+members
+loans
}
class Book {
+title
+copies
}
class BookCopy {
+barcode
}
class Member {
+name
}
class Loan {
+copy
+member
+borrowed_day
}
Library -- Book
Library -- Member
Library -- Loan
Book -- BookCopy
Loan -- BookCopy
Loan -- MemberIt looks tidy and says almost nothing:
- Every line means the same thing. Does
Library -- Membermean the library creates members, or only knows them? A reader cannot tell. - No numbers. Can a book have one copy or many? Can a loan have no member? The diagram is silent, and these are the questions interviewers ask.
- No direction. Does a
Loanknow itsBookCopy, or does the copy know its loan? Plain lines hide who holds the reference. - Fields repeat the code. The boxes list
copyandmember, which are exactly what the lines should be showing. A diagram that only restates the fields is a worse version of the code. - It cannot be checked against the design. Nothing in it could be wrong, so nothing in it is useful.
What can you read off the first diagram?
Look at the diagram of plain lines. Can a book have more than one copy? Can a loan exist without a member? Which class holds the reference between Loan and BookCopy?
One strong answer
None of the three can be answered. A plain line carries no multiplicity and no direction, so the diagram does not say. The code does: a Book holds a list of copies (many), a Loan is built from a copy and a member (so it always has both), and the Loan holds the references. The six relationships and the numbers are the notation that puts those answers on the page.
The six relationships#
Each relationship in UML is a statement you can verify in the code. The quickest way to choose the right line is to ask what the code does, not what the sentence "sounds like":
| Relationship | Mermaid | In the code | Ask yourself |
|---|---|---|---|
| Dependency | A ..> B |
B is a parameter, a local variable or a return type; no field |
Does A only use B for the length of one call? |
| Association | A --> B |
A has a field that refers to a B created elsewhere |
Does A know a B, which lives on without it? |
| Aggregation | A o-- B |
An association to a collection of parts, passed in from outside | Is A a whole of parts that exist on their own? |
| Composition | A *-- B |
A owns its Bs outright (usually it creates them): each B belongs to one A and goes when A goes |
Does a B make no sense without its A? |
| Inheritance | A <|-- B |
class B extends A |
Is B a kind of A? |
| Realisation | A <|.. B |
class B implements A (an interface) |
Does B promise what the interface A says? |
Here is the whole library, drawn with all six:
%% title: The library loan: all six relationships
classDiagram
class Library {
+add_book(title, barcodes)
+join(member)
+lend(title, member, day) Loan
}
class Book {
+title str
}
class BookCopy {
+barcode str
}
class Member {
+name str
}
class Loan {
+borrowed_day int
+fine(policy, returned_day) int
}
class FinePolicy {
<<interface>>
+fine(days_late) int
}
class PerDayFine {
+rupees_per_day int
}
class LoanError
class NoCopyAvailable
Library "1" *-- "*" Book : catalogue
Book "1" *-- "1..*" BookCopy : copies
Library "1" *-- "*" Loan : loans
Library "*" o-- "*" Member : members
Loan "*" --> "1" BookCopy : of
Loan "*" --> "1" Member : by
Loan ..> FinePolicy : prices with
Library ..> NoCopyAvailable : raises
FinePolicy <|.. PerDayFine
LoanError <|-- NoCopyAvailableRead the lines against the code, one by one:
- Composition (filled diamond, on the whole's end).
Librarycreates itsBooks inadd_bookand itsLoans inlend;Bookcreates itsBookCopyobjects in its constructor. In C++ the copies are owned by value, in a vector inside the book. Remove the library and these go with it. - Aggregation (hollow diamond).
LibraryholdsMembers, but it never creates them: they are made elsewhere and passed tojoin. A member is a person who can leave, or belong to another library at the same time, so the library is a whole of parts that exist on their own. That is why the number on the library's end is*: an aggregated part may be shared by several wholes, while a composed part belongs to exactly one (every composition above has1on the whole's end). - Association (a plain arrow). A
Loanholds a reference to aBookCopyand to aMember, handed to it when it is built. The arrow points from the one that holds to the one it refers to. Neither the copy nor the member disappears when the loan does. - Dependency (dashed arrow).
Loan.finetakes aFinePolicyas a parameter and never stores it. It needs the policy only for the length of the call, so there is no field and the line is dashed.Librarydepends onNoCopyAvailablethe same way: it raises one, and keeps none. - Realisation (dashed line, hollow triangle).
PerDayFinepromises what theFinePolicyinterface describes. The triangle points at what is promised. - Inheritance (solid line, hollow triangle).
NoCopyAvailableis aLoanError, which is why the oneexcept LoanErrorin the demo caught it. Same triangle, solid line.
Two honest notes about these lines. First, aggregation is the weakest of the six: UML itself is vague about what it adds to an association, and many teams draw a plain association and skip it. The course's own diagrams use it for "holds parts passed in" (a Group of Users, a LogBus of Sinks), but you will rarely lose marks for drawing an association instead. Second, composition is about who owns, not about who is important. Loan is a central class, and it is still a part of Library.
Choose each line by what the code does: creates and owns, holds in a field, or is only passed in for one call.
Multiplicity: the small numbers#
A number at the end of a line says how many objects of that class the line connects to. It sits next to the class it counts:
| Written | Means | Example |
|---|---|---|
1 |
exactly one | each BookCopy belongs to exactly 1 Book |
0..1 |
optional: none or one | a copy has 0 or 1 active loan |
* |
zero or more (the same as 0..*) |
a Library has any number of Books |
1..* |
one or more | a Book has at least 1 copy |
0..2, 3 |
a range, or an exact count | a member has 0 to 2 copies out |
The course writes "many" where UML writes *; read it the same way. The one thing people get backwards is which end the number goes on. In Book "1" *-- "1..*" BookCopy, the 1 beside Book says "each copy has exactly one book" and the 1..* beside BookCopy says "each book has one or more copies". You read a number as "how many of this for one of the class on the other end".
A multiplicity counts the class it sits beside: each copy has one book, and each book has one or more copies.
Multiplicity is not decoration. It is a rule your code has to enforce, and an interviewer who asks "what happens with zero copies?" is asking where the 1..* became a check:
# multiplicity.py: "1..*" and "0..2" on a diagram are rules the code must enforce
class BookCopy:
def __init__(self, barcode: str) -> None:
self.barcode = barcode
class Book:
def __init__(self, title: str, barcodes: list) -> None:
if len(barcodes) < 1: # 1..*: a book has at least one copy
raise ValueError("a book needs at least one copy")
self.title = title
self.copies = [BookCopy(b) for b in barcodes]
class Member:
MAX_LOANS = 2 # 0..2: a member has at most two copies out at a time
def __init__(self, name: str) -> None:
self.name = name
self.on_loan = []
def borrow(self, copy: BookCopy) -> None:
if len(self.on_loan) >= Member.MAX_LOANS:
raise ValueError(f"{self.name} already has {Member.MAX_LOANS} loans (limit {Member.MAX_LOANS})")
self.on_loan.append(copy)
try:
Book("Empty", [])
except ValueError as e:
print("rejected:", e)
godan = Book("Godan", ["G1", "G2", "G3"])
asha = Member("Asha")
asha.borrow(godan.copies[0])
asha.borrow(godan.copies[1])
print(f"Asha has {len(asha.on_loan)} loans")
try:
asha.borrow(godan.copies[2])
except ValueError as e:
print("rejected:", e)// Multiplicity.java: "1..*" and "0..2" on a diagram are rules the code must enforce
import java.util.ArrayList;
import java.util.List;
class Multiplicity {
static final class BookCopy {
final String barcode;
BookCopy(String barcode) { this.barcode = barcode; }
}
static final class Book {
final String title;
final List<BookCopy> copies = new ArrayList<>();
Book(String title, List<String> barcodes) {
if (barcodes.size() < 1) throw new IllegalArgumentException("a book needs at least one copy"); // 1..*
this.title = title;
for (String b : barcodes) copies.add(new BookCopy(b));
}
}
static final class Member {
static final int MAX_LOANS = 2; // 0..2: a member has at most two copies out at a time
final String name;
final List<BookCopy> onLoan = new ArrayList<>();
Member(String name) { this.name = name; }
void borrow(BookCopy copy) {
if (onLoan.size() >= MAX_LOANS) {
throw new IllegalArgumentException(name + " already has " + MAX_LOANS + " loans (limit " + MAX_LOANS + ")");
}
onLoan.add(copy);
}
}
public static void main(String[] args) {
try {
new Book("Empty", List.of());
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
Book godan = new Book("Godan", List.of("G1", "G2", "G3"));
Member asha = new Member("Asha");
asha.borrow(godan.copies.get(0));
asha.borrow(godan.copies.get(1));
System.out.println("Asha has " + asha.onLoan.size() + " loans");
try {
asha.borrow(godan.copies.get(2));
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
}
}// multiplicity.cpp: "1..*" and "0..2" on a diagram are rules the code must enforce
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct BookCopy {
std::string barcode;
};
struct Book {
std::string title;
std::vector<BookCopy> copies;
Book(std::string t, const std::vector<std::string>& barcodes) : title(std::move(t)) {
if (barcodes.size() < 1) throw std::invalid_argument("a book needs at least one copy"); // 1..*
for (const auto& b : barcodes) copies.push_back(BookCopy{b});
}
};
class Member {
public:
static const int MAX_LOANS = 2; // 0..2: a member has at most two copies out at a time
std::string name;
std::vector<const BookCopy*> onLoan;
explicit Member(std::string n) : name(std::move(n)) {}
void borrow(const BookCopy& copy) {
if (static_cast<int>(onLoan.size()) >= MAX_LOANS) {
throw std::invalid_argument(name + " already has " + std::to_string(MAX_LOANS) + " loans (limit " + std::to_string(MAX_LOANS) + ")");
}
onLoan.push_back(©);
}
};
int main() {
try {
Book("Empty", {});
} catch (const std::invalid_argument& e) {
std::cout << "rejected: " << e.what() << "\n";
}
Book godan("Godan", {"G1", "G2", "G3"});
Member asha("Asha");
asha.borrow(godan.copies[0]);
asha.borrow(godan.copies[1]);
std::cout << "Asha has " << asha.onLoan.size() << " loans\n";
try {
asha.borrow(godan.copies[2]);
} catch (const std::invalid_argument& e) {
std::cout << "rejected: " << e.what() << "\n";
}
}// multiplicity.js: "1..*" and "0..2" on a diagram are rules the code must enforce
class BookCopy {
constructor(barcode) {
this.barcode = barcode;
}
}
class Book {
constructor(title, barcodes) {
if (barcodes.length < 1) throw new Error("a book needs at least one copy"); // 1..*
this.title = title;
this.copies = barcodes.map((b) => new BookCopy(b));
}
}
class Member {
static MAX_LOANS = 2; // 0..2: a member has at most two copies out at a time
constructor(name) {
this.name = name;
this.onLoan = [];
}
borrow(copy) {
if (this.onLoan.length >= Member.MAX_LOANS) {
throw new Error(`${this.name} already has ${Member.MAX_LOANS} loans (limit ${Member.MAX_LOANS})`);
}
this.onLoan.push(copy);
}
}
try {
new Book("Empty", []);
} catch (e) {
console.log("rejected:", e.message);
}
const godan = new Book("Godan", ["G1", "G2", "G3"]);
const asha = new Member("Asha");
asha.borrow(godan.copies[0]);
asha.borrow(godan.copies[1]);
console.log(`Asha has ${asha.onLoan.length} loans`);
try {
asha.borrow(godan.copies[2]);
} catch (e) {
console.log("rejected:", e.message);
}rejected: a book needs at least one copy
Asha has 2 loans
rejected: Asha already has 2 loans (limit 2)Both numbers show up as a check in code: the 1..* as a constructor that refuses an empty list, and the 0..2 as a limit that borrow enforces. They are drawn like this, and the line has a number at the other end too: 0..1 beside Member, because a copy is out with at most one member, and a copy on the shelf is out with none:
%% title: Multiplicity: the numbers become rules
classDiagram
class Book {
+title str
}
class BookCopy {
+barcode str
}
class Member {
+name str
}
Book "1" *-- "1..*" BookCopy : copies
Member "0..1" --> "0..2" BookCopy : has outWhere does the number go?
A hotel room holds from zero to four guests, and a guest is in exactly one room at a time. Fill in the numbers: Room "?" --> "?" Guest.
One strong answer
Room "1" --> "0..4" Guest. The number beside Guest counts guests for one room: 0..4. The number beside Room counts rooms for one guest: exactly 1. The usual slip is to swap them, which would say that a guest can be in up to four rooms and a room holds exactly one guest.
Interfaces, abstract classes and the rest of a box#
A class box has three parts, and you rarely need the last two: the name, its attributes and its operations. Mermaid writes them as +name type and +name(args) type, with the leading symbol saying who can see them:
| Mark | Meaning | Mermaid |
|---|---|---|
+ |
public: callers can use it | +fine(days_late) int |
- |
private: only the class itself | -balance int |
# |
protected: the class and its subclasses | #rate int |
~ |
package (Java's default, no keyword): classes in the same package | ~cache list |
<<interface>> |
a contract with no code of its own | inside the box, first line |
<<abstract>> |
a class that cannot be created and may hold shared code | the same |
<<enumeration>> |
a fixed set of named values | the same |
Python's ABC with only @abstractmethod members is an interface; one that also holds shared code is an abstract class. Java's interface and C++'s class of pure virtual functions are interfaces. JavaScript has neither, so the contract is the method name, and the diagram still draws <<interface>> because the design has one.
The five-minute whiteboard version#
A class diagram in an interview is for the interviewer and for you: it shows you have found the pieces, and it is the plan you code from. It is not a deliverable. Here is the subset that earns the marks, as a target for the order and the time, not a rule from any exam:
- 0:00–1:00 — nouns from the problem become boxes, names only:
Library,Book,BookCopy,Member,Loan - 1:00–2:00 — add the two or three members per class that carry a rule (
lend,fine), not getters - 2:00–3:30 — draw the lines that matter: inheritance and realisation, composition, and associations with their numbers
- 3:30–4:30 — mark interfaces, and say each line aloud ("a book has one or more copies; a loan refers to a copy")
- 4:30–5:00 — walk one use case through the boxes: "Asha borrows the last copy"
A five-minute whiteboard class diagram: boxes first, then the members that carry rules, then the lines that matter with their numbers, then one use case walked through.
What to leave off, on purpose:
- Getters, setters and private helpers. They fill the box and hide the rules.
- Every dependency. Draw one when the interviewer will ask about it (the policy passed to
fine), and skip the rest. - A type on every field. Types help when they carry meaning (
rupees_per_day int), and clutter when they do not. - Aggregation, unless you are asked: a plain association says the same thing for this purpose.
- Utility classes with no state and no relationships.
The diagram and the code must agree. If you change the design while coding (a new class, a flipped arrow), redraw that one line. A diagram that no longer matches is worse than none, because the interviewer will read the diagram and test you on it.
When not to use it#
A class diagram is the right tool for structure: which classes exist and what holds what. It is the wrong tool for several things people try to draw with it.
- You need to show a flow over time. A request moving between services is a sequence diagram. A class diagram has no time.
- You need to show a lifecycle. An order moving from placed to delivered is a state diagram (the vending machine draws one). Do not encode it as classes and arrows.
- You are about to draw every class in the system. If a diagram needs more than about ten boxes, it is not helping a reader; draw the part under discussion.
- The design is still changing every few minutes. A diagram you keep rewriting costs time you need for code. Draw the stable core.
- The boxes would only repeat the code. A diagram of every field is a slower way to read the same file.
The test is the same one used for the whiteboard version: would the interviewer, or a teammate, learn something from a line they could not see in the code at a glance? A line that records who creates whom or how many passes that test. A line that restates a field does not.
Where it shows up in the case studies#
All 22 of the course's low-level design lessons draw one class diagram each, in exactly this notation. A few show the relationships on purpose:
| Case study | A line to look at | What it shows |
|---|---|---|
| Parking lot | ParkingLot "1" *-- "many" Floor, Floor "1" *-- "many" ParkingSpot |
Composition: floors and spots are created and owned by the lot, and go with it |
| Parking lot | SpotAssignmentStrategy and NearestSmallestFit |
Realisation: an interface and the class that implements it |
| Library | Book "1" *-- "many" BookCopy, Loan --> BookCopy |
One title, many copies; a loan refers to one copy, not the title |
| Chess | Piece, Slider and Rook |
Inheritance, a shallow family of three levels; and Board "1" o-- "many" Piece for aggregation |
| Splitwise | SplitStrategy ..> Split |
Dependency: the strategy produces splits but stores none |
(The course writes "many" where this lesson writes *.) When you meet a new case study, read its diagram first with the table above in your head: you will know in a minute which classes own which, and where the interfaces are.
In the interview#
Narrate the diagram as you draw it, in one pass: "A library has books and members; a book has one or more copies, and a loan points at one copy and one member. The fine rule is an interface so a new tariff is a new class." Each clause is one line on the board, and the multiplicities are said, not left for the interviewer to guess.
Then expect the follow-ups:
- "Composition or aggregation?" Ask who creates the part and whether it can outlive the whole. A copy is created by its book and has no life without it: composition. A member is created elsewhere and can leave: aggregation, or a plain association. Say that aggregation is weakly defined and you would not lose a mark either way.
- "Association or dependency?" Is there a field? A field that holds the other object is an association; a parameter or local used within one call is a dependency.
- "Why does
Loanpoint atBookCopyand not atBook?" Because a member takes home one physical object with a barcode and returns that object. The title cannot be returned. (The library lesson builds exactly this.) - "What does
1..*become in code?" A check where the object is built: here, aBookconstructor that refuses an empty list. - "Do I have to use UML?" No. Boxes and labelled lines are enough, as long as each line says what it means and the numbers are there. The notation is a shared shorthand, not an exam.
Common mistakes#
- Plain lines with no meaning: every relationship drawn the same, so nothing can be read from the diagram.
- The diamond on the wrong end. It sits on the whole (
Book *-- BookCopy), not on the part. - The number on the wrong end.
1..*goes beside the class that is counted (BookCopy), and "each copy has exactly one book" is the1besideBook. - Composition everywhere, because it feels strong. Use it only when the part cannot exist without the whole.
- Dashed and solid confused. Realisation (implements an interface) is dashed, inheritance (extends a class) is solid; both end in a hollow triangle at the parent.
- Drawing every field, getter and dependency, so the lines are lost in the boxes.
- A diagram that does not match the code a minute later.
UML class diagrams
Q1/4 A Book creates its BookCopy objects, and a copy never exists without its book. Which line joins them?
Answer & explanation
Answer: C
Composition means the whole creates and owns the part, which goes when the whole goes. The filled diamond sits on the whole (Book). Aggregation would be for parts created elsewhere and passed in; a dependency has no field at all.
Q2/4 Loan.fine(policy, day) takes a FinePolicy as a parameter, uses it once and never stores it. Which relationship is that?
Answer & explanation
Answer: C
With no field there is no lasting reference, only a use for the length of one call. That is a dependency. Realisation is the line from a class to the interface it implements, here PerDayFine to FinePolicy.
Q3/4 In Book "1" -- "1.." BookCopy, what does the 1..* beside BookCopy say?
Answer & explanation
Answer: C
A number counts the class it sits beside, for one object of the class at the other end. So 1..* beside BookCopy says one book has one or more copies, and the 1 beside Book says each copy has exactly one book.
Q4/4 Which of these belongs on a five-minute whiteboard class diagram?
Answer & explanation
Answer: C
The diagram is for showing the pieces and how they relate. Getters, every dependency and every type bury the lines that carry the design. Draw what the interviewer will ask about, and say each line aloud.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


