Class Diagrams — Structure of Your System
Introduction
If you could learn only one UML diagram for the rest of your career, it should be the class diagram.
A class diagram is the structural backbone of your design. It shows every class in your system, what data each class holds (attributes), what each class can do (methods), and how classes relate to each other (relationships). It's the single most used diagram in LLD interviews, design reviews, and documentation.
Think of a class diagram like the floor plan of a building. A floor plan doesn't tell you how people move through the building (that's a sequence diagram) or what activities happen in each room (that's an activity diagram). It tells you: these rooms exist, they're this size, and they connect through these doors and hallways.
In this tutorial, you'll learn to read and draw class diagrams using our Library Management System. By the end, you'll know every notation symbol, every relationship arrow, and every visibility modifier — the full toolkit for representing your system's structure.
This builds directly on the OOP relationships you learned in S4 — association, aggregation, composition, dependency, and inheritance. A class diagram is those concepts made visible.
Anatomy of a Class Box
Every class in a UML class diagram is drawn as a rectangle divided into three compartments:
-
Top compartment — Class Name: The name of the class, centered and bold. If the class is abstract, the name is italicized. If it's an interface, the stereotype
<<interface>>appears above the name. -
Middle compartment — Attributes: The data the class holds. Each attribute follows the format:
visibility name : type -
Bottom compartment — Methods: The operations the class can perform. Each method follows the format:
visibility name(parameters) : returnType
Visibility Modifiers
UML uses symbols to indicate access levels — these map directly to the access modifiers you learned in S3 (Encapsulation):
| Symbol | Meaning | Python Equivalent | Java Equivalent |
|---|---|---|---|
+ | Public | No prefix | public |
- | Private | __ prefix (name mangling) | private |
# | Protected | _ prefix (convention) | protected |
~ | Package/Internal | Module-level | Default (package-private) |
Example — a Book class:
- Class name:
Book - Attributes:
- title: String,- isbn: String,- status: BookStatus - Methods:
+ getTitle(): String,+ isAvailable(): boolean,+ markAsBorrowed(): void
The - before title means it's private — external code can't access it directly. The + before getTitle() means it's public — this is the controlled interface. This is encapsulation in visual form.
Notation Guide — Relationships
Relationships between classes are shown as lines connecting class boxes, with different arrowheads and line styles for each type. This is where most beginners struggle — so let's be precise.
1. Association (solid line, optional arrow)
Two classes know about each other. A Member is associated with many Loan records.
- Notation: Solid line between classes
- Multiplicity: Numbers at each end show how many instances participate (
1,0..1,*,1..*) - Example:
Member 1 ———— * Loan(one member has many loans)
2. Aggregation (open diamond)
"Has-a" relationship where the part can exist independently. A Library has Members, but members exist even if the library closes.
- Notation: Solid line with an open (hollow) diamond at the whole's end
- Example:
Library ◇———— Member
3. Composition (filled diamond)
"Has-a" relationship where the part cannot exist without the whole. A Loan contains a DueDate — if the loan is deleted, the due date is meaningless.
- Notation: Solid line with a filled (solid) diamond at the whole's end
- Example:
Loan ◆———— DueDate
4. Inheritance / Generalization (solid line, hollow triangle)
An "is-a" relationship. Librarian is a Member with extra privileges.
- Notation: Solid line with a hollow triangle arrowhead pointing at the parent class
- Example:
Librarian ——▷ Member
5. Interface Implementation (dashed line, hollow triangle)
A class implements an interface. EmailNotifier implements Notifier.
- Notation: Dashed line with a hollow triangle arrowhead pointing at the interface
- Example:
EmailNotifier - - -▷ <<interface>> Notifier
6. Dependency (dashed line, open arrow)
One class uses another temporarily (e.g., as a method parameter). LoanService depends on FineCalculator.
- Notation: Dashed line with an open arrowhead pointing at the used class
- Example:
LoanService - - -→ FineCalculator
Multiplicity Notation
| Symbol | Meaning |
|---|---|
1 | Exactly one |
0..1 | Zero or one (optional) |
* | Zero or more |
1..* | One or more |
2..5 | Between 2 and 5 |

Visualization

Complete Example — Library Management System Class Diagram
Here is the completed class diagram for our Library Management System. Study the relationships carefully — each arrow type, diamond, and multiplicity indicator was chosen deliberately:
Librarianinherits fromMember(solid line, hollow triangle) — a librarian is a member with extra permissionsMemberhasLoanrecords (association,1to*) — one member can have many loansLoanreferences exactly oneBook(association,1to1) — each loan is for one specific bookLoancomposes aDueDate(filled diamond) — the due date has no meaning outside the loanBookdepends onBookStatusenum (dashed arrow) — uses it but doesn't own itLoandepends onFineCalculator(dashed arrow) — uses it temporarily to compute fines
Translating a Class Diagram to Code
A well-drawn class diagram maps directly to code. Let's translate the core classes from our Library Management System diagram into Python and Java, paying attention to how each UML concept (visibility, relationships, multiplicity) appears in code.
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import date, timedelta
from enum import Enum
from typing import List, Optional
# UML Enum → Python Enum
class BookStatus(Enum):
AVAILABLE = "available"
BORROWED = "borrowed"
RESERVED = "reserved"
LOST = "lost"
# UML <<interface>> → Python ABC
class Searchable(ABC):
@abstractmethod
def search(self, query: str) -> list:
pass
class Book(Searchable):
"""UML: Book implements Searchable, depends on BookStatus."""
def __init__(self, title: str, isbn: str, author: str):
self._title = title # UML: - title : String (private)
self._isbn = isbn # UML: - isbn : String
self._author = author # UML: - author : String
self._status = BookStatus.AVAILABLE # UML: - status : BookStatus
def get_title(self) -> str: # UML: + getTitle() : String
return self._title
def is_available(self) -> bool: # UML: + isAvailable() : boolean
return self._status == BookStatus.AVAILABLE
def mark_as_borrowed(self) -> None: # UML: + markAsBorrowed() : void
self._status = BookStatus.BORROWED
def mark_as_returned(self) -> None:
self._status = BookStatus.AVAILABLE
def search(self, query: str) -> list:
query_lower = query.lower()
if query_lower in self._title.lower() or query_lower in self._author.lower():
return [self]
return []
class Fine:
"""UML: Fine — created by Loan (dependency)."""
def __init__(self, amount: float):
self._amount = amount
self._paid = False
self._issue_date = date.today()
def pay(self) -> None:
self._paid = True
def get_amount(self) -> float:
return self._amount
class Loan:
"""UML: Loan has composition with DueDate, association with Book.
Multiplicity: each Loan references exactly 1 Book."""
DAILY_FINE_RATE = 0.50
def __init__(self, book: Book, borrow_date: date, loan_days: int = 14):
self._book = book # Association: 1-to-1
self._borrow_date = borrow_date
self._due_date = borrow_date + timedelta(days=loan_days) # Composition
self._return_date: Optional[date] = None
self._returned = False
def is_overdue(self) -> bool:
if self._returned:
return False
return date.today() > self._due_date
def calculate_fine(self) -> float:
"""Dependency: Loan uses Fine temporarily."""
if not self.is_overdue():
return 0.0
check_date = self._return_date if self._returned else date.today()
overdue_days = (check_date - self._due_date).days
return overdue_days * self.DAILY_FINE_RATE
def mark_returned(self) -> None:
self._returned = True
self._return_date = date.today()
self._book.mark_as_returned()
class Member:
"""UML: Member has * Loans (one-to-many association)."""
def __init__(self, name: str, member_id: str, email: str):
self._name = name
self._member_id = member_id
self._email = email
self._active_loans: List[Loan] = [] # Multiplicity: *
def borrow_book(self, book: Book) -> Loan:
if not book.is_available():
raise ValueError(f"Book '{book.get_title()}' is not available")
book.mark_as_borrowed()
loan = Loan(book, date.today())
self._active_loans.append(loan)
return loan
def return_book(self, loan: Loan) -> Optional[Fine]:
loan.mark_returned()
self._active_loans.remove(loan)
fine_amount = loan.calculate_fine()
if fine_amount > 0:
return Fine(fine_amount)
return None
def get_active_loans(self) -> List[Loan]:
return list(self._active_loans)
class Librarian(Member):
"""UML: Librarian inherits from Member (generalization)."""
def __init__(self, name: str, member_id: str, email: str, employee_id: str):
super().__init__(name, member_id, email)
self._employee_id = employee_id
def add_book(self, book: Book) -> None:
print(f"Book '{book.get_title()}' added to catalog")
def register_member(self, member: Member) -> None:
print(f"Member '{member._name}' registered")
def collect_fine(self, fine: Fine) -> None:
fine.pay()
print(f"Fine of ${fine.get_amount():.2f} collected")
# --- Usage ---
book = Book("Clean Code", "978-0132350884", "Robert C. Martin")
member = Member("Alice", "M-001", "alice@example.com")
librarian = Librarian("Bob", "M-002", "bob@example.com", "E-101")
librarian.add_book(book)
loan = member.borrow_book(book)
print(f"Borrowed: {book.get_title()}, Available: {book.is_available()}")
print(f"Active loans: {len(member.get_active_loans())}")import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
// UML Enum → Java Enum
enum BookStatus {
AVAILABLE, BORROWED, RESERVED, LOST
}
// UML <<interface>> → Java Interface
interface Searchable {
List<Book> search(String query);
}
class Book implements Searchable {
private String title; // UML: - title : String
private String isbn; // UML: - isbn : String
private String author; // UML: - author : String
private BookStatus status; // UML: - status : BookStatus
public Book(String title, String isbn, String author) {
this.title = title;
this.isbn = isbn;
this.author = author;
this.status = BookStatus.AVAILABLE;
}
public String getTitle() { return title; } // UML: + getTitle() : String
public boolean isAvailable() { // UML: + isAvailable() : boolean
return status == BookStatus.AVAILABLE;
}
public void markAsBorrowed() { status = BookStatus.BORROWED; }
public void markAsReturned() { status = BookStatus.AVAILABLE; }
@Override
public List<Book> search(String query) {
List<Book> results = new ArrayList<>();
if (title.toLowerCase().contains(query.toLowerCase())
|| author.toLowerCase().contains(query.toLowerCase())) {
results.add(this);
}
return results;
}
}
class Fine {
private double amount;
private boolean paid;
private LocalDate issueDate;
public Fine(double amount) {
this.amount = amount;
this.paid = false;
this.issueDate = LocalDate.now();
}
public void pay() { this.paid = true; }
public double getAmount() { return amount; }
}
class Loan {
private static final double DAILY_FINE_RATE = 0.50;
private Book book; // Association: 1-to-1
private LocalDate borrowDate;
private LocalDate dueDate; // Composition: part of Loan
private LocalDate returnDate;
private boolean returned;
public Loan(Book book, LocalDate borrowDate, int loanDays) {
this.book = book;
this.borrowDate = borrowDate;
this.dueDate = borrowDate.plusDays(loanDays);
this.returned = false;
}
public boolean isOverdue() {
if (returned) return false;
return LocalDate.now().isAfter(dueDate);
}
public double calculateFine() {
if (!isOverdue()) return 0.0;
LocalDate checkDate = returned ? returnDate : LocalDate.now();
long overdueDays = ChronoUnit.DAYS.between(dueDate, checkDate);
return overdueDays * DAILY_FINE_RATE;
}
public void markReturned() {
this.returned = true;
this.returnDate = LocalDate.now();
this.book.markAsReturned();
}
}
class Member {
private String name;
private String memberId;
private String email;
private List<Loan> activeLoans; // Multiplicity: *
public Member(String name, String memberId, String email) {
this.name = name;
this.memberId = memberId;
this.email = email;
this.activeLoans = new ArrayList<>();
}
public Loan borrowBook(Book book) {
if (!book.isAvailable()) {
throw new IllegalStateException("Book '" + book.getTitle() + "' is not available");
}
book.markAsBorrowed();
Loan loan = new Loan(book, LocalDate.now(), 14);
activeLoans.add(loan);
return loan;
}
public Fine returnBook(Loan loan) {
loan.markReturned();
activeLoans.remove(loan);
double fineAmount = loan.calculateFine();
if (fineAmount > 0) {
return new Fine(fineAmount);
}
return null;
}
public List<Loan> getActiveLoans() {
return new ArrayList<>(activeLoans);
}
protected String getName() { return name; }
}
class Librarian extends Member { // Inheritance: Librarian IS-A Member
private String employeeId;
public Librarian(String name, String memberId, String email, String employeeId) {
super(name, memberId, email);
this.employeeId = employeeId;
}
public void addBook(Book book) {
System.out.println("Book '" + book.getTitle() + "' added to catalog");
}
public void registerMember(Member member) {
System.out.println("Member '" + member.getName() + "' registered");
}
public void collectFine(Fine fine) {
fine.pay();
System.out.printf("Fine of $%.2f collected%n", fine.getAmount());
}
}Common Mistakes When Drawing Class Diagrams
Mistake 1: Wrong Relationship Arrow
❌ Wrong: Using a filled diamond (composition) when you mean association. If Member has a list of Loan objects, that's association — loans are tracked, but they aren't owned in the composition sense (a loan references a book too).
✅ Right: Think about lifecycle dependency. Does the part die when the whole dies? If yes → composition (filled diamond). If no → aggregation (open diamond) or plain association.
Mistake 2: Showing Every Field and Method
❌ Wrong: Listing all 23 methods of a class, including trivial getters, setters, toString(), and equals().
✅ Right: Show only the key attributes and methods — the ones that communicate the class's responsibility. In interviews, 3-5 methods per class is the sweet spot. The diagram should communicate design intent, not serve as a code generator.
Mistake 3: Missing Multiplicity
❌ Wrong: Drawing an association line from Member to Loan without numbers at the ends. This hides crucial information — can a member have only 1 loan or 100?
✅ Right: Always add multiplicity: 1 to *, 0..1, 1..*. Multiplicity constraints drive the data structure choice — 1 means a single reference, * means a collection.
Mistake 4: Confusing Inheritance with Interface Implementation
❌ Wrong: Using a solid line with hollow triangle for both "extends" and "implements".
✅ Right: Inheritance (extends) = solid line + hollow triangle. Interface implementation = dashed line + hollow triangle. The dashed line signals "this is a contract, not a reuse of implementation."
Mistake 5: Putting Implementation Details in the Diagram
❌ Wrong: Adding HashMap<String, Book> bookCache as an attribute, or private void validateISBN(String isbn) as a method.
✅ Right: Class diagrams show design-level abstractions. Implementation details like cache data structures and internal helper methods belong in the code, not the diagram.
Practice Tips
-
Start with nouns. Read the requirements and underline every noun — those are your candidate classes. For our library system: Book, Member, Librarian, Loan, Reservation, Fine, Catalog.
-
Add relationships second. Once you have class boxes, ask: "Does class A know about class B? Does A own B? Is A a kind of B?" Each answer determines the arrow type.
-
Ask the lifecycle question. The key test for composition vs aggregation vs association: If I delete object A, does object B lose all meaning? If yes → composition. If no → association or aggregation.
-
Keep it to 5-8 classes. A class diagram with 15+ classes is overwhelming and unreadable. If your system is larger, split into focused diagrams: one for the core domain, one for the notification subsystem, etc.
-
Use the "squint test." Step back and squint at your diagram. Can you see the major clusters and relationships? If everything is a tangled web of crossing lines, reorganize the layout. Place related classes near each other.
-
Draw it by hand first. In interviews, you'll be on a whiteboard. Practice drawing class boxes and arrows freehand. Speed matters — you should be able to sketch a 6-class diagram in 5-7 minutes.
You'll continue building on this class diagram notation throughout S5. The use case, sequence, activity, and state machine diagrams all complement it — different views of the same Library Management System.
Key Takeaways
- A class diagram is the most important UML diagram — it shows classes, attributes, methods, and relationships as a static structural blueprint
- Classes are drawn as rectangles with three compartments: name, attributes, methods — with visibility modifiers (
+public,-private,#protected) - Six relationship types, each with distinct arrow notation: association (solid line), aggregation (open diamond), composition (filled diamond), inheritance (solid line + hollow triangle), implementation (dashed line + hollow triangle), dependency (dashed arrow)
- Multiplicity labels (
1,*,0..1,1..*) at line ends specify how many instances participate in a relationship — this drives data structure choices in code - In LLD interviews, class diagrams are the most scrutinized diagram — practice drawing 5-8 class diagrams with correct notation until it's second nature
- A class diagram maps directly to code — every box becomes a class, every arrow becomes a field or parameter, every visibility symbol becomes an access modifier