Skip to main content

Class Diagrams — Structure of Your System

Editorial

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:

  1. 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.

  2. Middle compartment — Attributes: The data the class holds. Each attribute follows the format:
    visibility name : type

  3. 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):

SymbolMeaningPython EquivalentJava Equivalent
+PublicNo prefixpublic
-Private__ prefix (name mangling)private
#Protected_ prefix (convention)protected
~Package/InternalModule-levelDefault (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

SymbolMeaning
1Exactly one
0..1Zero or one (optional)
*Zero or more
1..*One or more
2..5Between 2 and 5
A reference card for UML class diagram notation, using the lesson's Library examples. Six relationships, each with its meaning and notation. Association, two classes know about each other: a solid line (an arrow is optional), Member 1 to * Loan. Aggregation, has-a where the part can exist independently: a hollow diamond at the whole, Library to Member. Composition, has-a where the part cannot exist without the whole: a filled diamond at the whole, Loan to DueDate. Inheritance or generalization, is-a: a solid line with a hollow triangle at the parent, Librarian to Member. Interface implementation, a class implements an interface: a dashed line with a hollow triangle at the interface, EmailNotifier to «interface» Notifier. Dependency, one class uses another temporarily: a dashed line with an open arrow at the used class, LoanService to FineCalculator. Below, the class box with its three compartments, drawn for Book: the class name (bold, centered; italic if abstract; «interface» above it for an interface); attributes in the form visibility name : type (- title: String, - isbn: String, - status: BookStatus); methods in the form visibility name(parameters) : returnType (+ getTitle(): String, + isAvailable(): boolean, + markAsBorrowed(): void). Visibility: + public, - private, # protected, ~ package or internal. Multiplicity: 1 exactly one, 0..1 zero or one (optional), * zero or more, 1..* one or more, 2..5 between 2 and 5.

Visualization

Pick one of the six UML relationships and watch the correct connector render between two Library class boxes - with the example, its meaning, the notation, and multiplicity. The line style (solid vs dashed) and end shape (diamond, triangle, arrow) fully identify the relationship.

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:

  • Librarian inherits from Member (solid line, hollow triangle) — a librarian is a member with extra permissions
  • Member has Loan records (association, 1 to *) — one member can have many loans
  • Loan references exactly one Book (association, 1 to 1) — each loan is for one specific book
  • Loan composes a DueDate (filled diamond) — the due date has no meaning outside the loan
  • Book depends on BookStatus enum (dashed arrow) — uses it but doesn't own it
  • Loan depends on FineCalculator (dashed arrow) — uses it temporarily to compute fines
Complete UML class diagram for Library Management System showing Book, Member, Librarian, Loan, Fine, Reservation classes with all attributes, methods, and relationships including inheritance, composition, association, and dependency

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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