Class Diagrams in UML: Notation & Examples
TL;DR
Class diagrams are UML’s most widely-used diagram type, showing the static structure of a system through classes, their attributes, methods, and relationships. They serve as blueprints for implementation and communication tools between developers, making them essential for design discussions and technical interviews.
Core Concept
What is a Class Diagram?
A class diagram is a structural UML diagram that visualizes the classes in a system and how they relate to each other. Think of it as an architectural blueprint — it shows what exists in your system, not how it behaves over time.
Each class is represented as a box divided into three compartments:
- Class name (top): The identifier for the class
- Attributes (middle): The data/fields the class holds
- Methods (bottom): The operations/functions the class can perform
Why Class Diagrams Matter
Class diagrams bridge the gap between design and implementation. Before writing a single line of code, you can:
- Visualize the entire system structure
- Identify missing classes or redundant relationships
- Communicate design decisions to team members
- Spot design flaws early (tight coupling, circular dependencies)
In interviews, you’ll often be asked to “design a parking lot” or “design a library system.” Interviewers expect you to sketch a class diagram first, showing you can think before coding.
Core Components
Visibility modifiers indicate access levels:
+public: accessible everywhere-private: accessible only within the class#protected: accessible in class and subclasses~package/default: accessible within the same package
Attributes are shown as: visibility name: type = defaultValue
Example: - balance: float = 0.0
Methods are shown as: visibility name(parameters): returnType
Example: + deposit(amount: float): bool
Static members are underlined. Abstract classes/methods are italicized or marked with {abstract}.
Visual Guide
Basic Class Structure
classDiagram
class BankAccount {
-accountNumber: string
-balance: float
-owner: string
+deposit(amount: float) bool
+withdraw(amount: float) bool
+getBalance() float
}
note for BankAccount "Top: Class name\nMiddle: Attributes (data)\nBottom: Methods (behavior)"
A class box has three compartments: name, attributes, and methods. The minus sign (-) indicates private members, while plus (+) indicates public members.
Inheritance Relationship
classDiagram
Animal <|-- Dog
Animal <|-- Cat
class Animal {
#name: string
#age: int
+makeSound() void
+eat() void
}
class Dog {
-breed: string
+makeSound() void
+fetch() void
}
class Cat {
-indoor: bool
+makeSound() void
+scratch() void
}
Inheritance is shown with a solid line and hollow triangle pointing to the parent class. Dog and Cat inherit from Animal, gaining access to protected (#) and public (+) members.
Association vs Aggregation vs Composition
classDiagram
Professor --> Course : teaches
Department o-- Professor : has
University *-- Department : contains
class Professor {
-name: string
+teach() void
}
class Course {
-courseCode: string
}
class Department {
-name: string
}
class University {
-name: string
}
Association (—>): Professor teaches Course (weak relationship). Aggregation (o—): Department has Professors (shared ownership). Composition (—): University contains Departments (strong ownership, Department can’t exist without University).*
Dependency Relationship
classDiagram
OrderProcessor ..> PaymentGateway : uses
OrderProcessor ..> EmailService : uses
class OrderProcessor {
+processOrder(order: Order) bool
}
class PaymentGateway {
+charge(amount: float) bool
}
class EmailService {
+sendConfirmation(email: string) void
}
Dependency (..>): OrderProcessor uses PaymentGateway and EmailService temporarily (as method parameters or local variables), but doesn’t store them as attributes.
Multiplicity in Relationships
classDiagram
Customer "1" --> "0..*" Order : places
Order "1" *-- "1..*" OrderItem : contains
OrderItem "*" --> "1" Product : references
class Customer {
-customerId: string
-name: string
}
class Order {
-orderId: string
-date: datetime
}
class OrderItem {
-quantity: int
-price: float
}
class Product {
-productId: string
-name: string
}
Multiplicity shows how many instances participate in a relationship. 1 = exactly one, 0.. = zero or more, 1..* = one or more, * = many. One Customer places zero or more Orders; one Order contains one or more OrderItems.*
Examples
Example 1: Simple Library System
Let’s design a basic library system with books, members, and loans.
Class Diagram (text representation):
┌─────────────────┐
│ Book │
├─────────────────┤
│ - isbn: string │
│ - title: string │
│ - author: string│
├─────────────────┤
│ + getDetails() │
└─────────────────┘
△
│ (composition)
│ 1..*
┌─────────────────┐
│ Library │
├─────────────────┤
│ - name: string │
│ - books: list │
├─────────────────┤
│ + addBook() │
│ + findBook() │
└─────────────────┘
│
│ (association)
│ 0..*
┌─────────────────┐
│ Loan │
├─────────────────┤
│ - loanDate: date│
│ - dueDate: date │
├─────────────────┤
│ + isOverdue() │
└─────────────────┘
│
│ (association)
│ 1
┌─────────────────┐
│ Member │
├─────────────────┤
│ - memberId: str │
│ - name: string │
├─────────────────┤
│ + borrowBook() │
└─────────────────┘
Python Implementation:
from datetime import datetime, timedelta
from typing import List
class Book:
def __init__(self, isbn: str, title: str, author: str):
self.__isbn = isbn # Private attribute
self.__title = title
self.__author = author
def get_details(self) -> str:
return f"{self.__title} by {self.__author} (ISBN: {self.__isbn})"
class Member:
def __init__(self, member_id: str, name: str):
self.__member_id = member_id
self.__name = name
self.__active_loans: List['Loan'] = []
def borrow_book(self, book: Book) -> 'Loan':
loan = Loan(self, book)
self.__active_loans.append(loan)
return loan
def get_name(self) -> str:
return self.__name
class Loan:
def __init__(self, member: Member, book: Book):
self.__member = member # Association: Loan knows about Member
self.__book = book # Association: Loan knows about Book
self.__loan_date = datetime.now()
self.__due_date = self.__loan_date + timedelta(days=14)
def is_overdue(self) -> bool:
return datetime.now() > self.__due_date
def get_details(self) -> str:
status = "OVERDUE" if self.is_overdue() else "Active"
return f"{self.__member.get_name()} borrowed {self.__book.get_details()} - {status}"
class Library:
def __init__(self, name: str):
self.__name = name
self.__books: List[Book] = [] # Composition: Library owns Books
def add_book(self, book: Book) -> None:
self.__books.append(book)
def find_book(self, isbn: str) -> Book:
for book in self.__books:
if book._Book__isbn == isbn: # Name mangling to access private
return book
return None
# Usage
library = Library("City Library")
book1 = Book("978-0132350884", "Clean Code", "Robert Martin")
library.add_book(book1)
member = Member("M001", "Alice Johnson")
loan = member.borrow_book(book1)
print(loan.get_details())
# Output: Alice Johnson borrowed Clean Code by Robert Martin (ISBN: 978-0132350884) - Active
Key Relationships:
- Composition (Library *— Book): Library owns books. If library is destroyed, books in the system are too.
- Association (Member — Loan — Book): Loan connects Member and Book, but they can exist independently.
Try it yourself: Add a return_book() method to Member that removes a loan from active_loans.
Example 2: E-commerce Order System with Inheritance
Class Diagram Relationships:
Payment (abstract)
△
│ (inheritance)
┌─────┴─────┐
│ │
CreditCard PayPal
Customer
│ 1
│ (association)
│ 0..*
Order
│ 1
│ (composition)
│ 1..*
OrderItem
│ *
│ (association)
│ 1
Product
Python Implementation:
from abc import ABC, abstractmethod
from typing import List
class Product:
def __init__(self, product_id: str, name: str, price: float):
self.product_id = product_id
self.name = name
self.price = price
class OrderItem:
def __init__(self, product: Product, quantity: int):
self.__product = product # Association
self.__quantity = quantity
def get_subtotal(self) -> float:
return self.__product.price * self.__quantity
def get_details(self) -> str:
return f"{self.__product.name} x{self.__quantity} = ${self.get_subtotal():.2f}"
class Order:
def __init__(self, order_id: str):
self.order_id = order_id
self.__items: List[OrderItem] = [] # Composition: Order owns OrderItems
def add_item(self, item: OrderItem) -> None:
self.__items.append(item)
def get_total(self) -> float:
return sum(item.get_subtotal() for item in self.__items)
def get_summary(self) -> str:
summary = f"Order {self.order_id}:\n"
for item in self.__items:
summary += f" {item.get_details()}\n"
summary += f"Total: ${self.get_total():.2f}"
return summary
class Customer:
def __init__(self, customer_id: str, name: str, email: str):
self.customer_id = customer_id
self.name = name
self.email = email
self.__orders: List[Order] = [] # Association: Customer has Orders
def place_order(self, order: Order) -> None:
self.__orders.append(order)
def get_order_history(self) -> List[Order]:
return self.__orders.copy()
# Abstract base class for payment methods
class Payment(ABC):
def __init__(self, amount: float):
self._amount = amount # Protected attribute
@abstractmethod
def process_payment(self) -> bool:
"""Process the payment. Returns True if successful."""
pass
@abstractmethod
def get_receipt(self) -> str:
"""Generate payment receipt."""
pass
class CreditCardPayment(Payment):
def __init__(self, amount: float, card_number: str, cvv: str):
super().__init__(amount)
self.__card_number = card_number[-4:] # Store only last 4 digits
self.__cvv = cvv
def process_payment(self) -> bool:
# Simulate payment processing
print(f"Processing credit card payment of ${self._amount:.2f}...")
return True
def get_receipt(self) -> str:
return f"Credit Card (****{self.__card_number}): ${self._amount:.2f}"
class PayPalPayment(Payment):
def __init__(self, amount: float, email: str):
super().__init__(amount)
self.__email = email
def process_payment(self) -> bool:
print(f"Processing PayPal payment of ${self._amount:.2f}...")
return True
def get_receipt(self) -> str:
return f"PayPal ({self.__email}): ${self._amount:.2f}"
# Usage
product1 = Product("P001", "Laptop", 999.99)
product2 = Product("P002", "Mouse", 29.99)
order = Order("ORD-12345")
order.add_item(OrderItem(product1, 1))
order.add_item(OrderItem(product2, 2))
customer = Customer("C001", "Bob Smith", "bob@example.com")
customer.place_order(order)
print(order.get_summary())
# Output:
# Order ORD-12345:
# Laptop x1 = $999.99
# Mouse x2 = $59.98
# Total: $1059.97
payment = CreditCardPayment(order.get_total(), "1234567890123456", "123")
if payment.process_payment():
print(payment.get_receipt())
# Output:
# Processing credit card payment of $1059.97...
# Credit Card (****3456): $1059.97
Key Design Decisions:
- Inheritance: Payment is abstract, forcing CreditCardPayment and PayPalPayment to implement process_payment() and get_receipt()
- Composition: Order owns OrderItems (if order is deleted, items are too)
- Association: OrderItem references Product (product exists independently)
Java Note: In Java, you’d use abstract class Payment or interface Payment. Python uses ABC (Abstract Base Class) module.
Try it yourself: Create a DebitCardPayment class that inherits from Payment and adds a PIN verification method.
Example 3: Reading a Class Diagram and Implementing It
You’re given this class diagram in an interview:
┌──────────────────┐
│ Vehicle │ (abstract)
├──────────────────┤
│ # make: string │
│ # model: string │
│ # year: int │
├──────────────────┤
│ + start(): void │ (abstract)
│ + stop(): void │
│ + getInfo(): str │
└──────────────────┘
△
│
┌────┴────┐
│ │
┌──────┐ ┌──────┐
│ Car │ │ Truck│
├──────┤ ├──────┤
│-doors│ │-cargo│
│ :int │ │ :int │
├──────┤ ├──────┤
│start │ │start │
│load │ │load │
└──────┘ └──────┘
Fleet
│ 1
│ (aggregation)
│ 0..*
Vehicle
Implementation:
from abc import ABC, abstractmethod
from typing import List
class Vehicle(ABC):
def __init__(self, make: str, model: str, year: int):
self._make = make # Protected (# in UML)
self._model = model
self._year = year
@abstractmethod
def start(self) -> None:
pass
def stop(self) -> None:
print(f"{self._make} {self._model} stopped.")
def get_info(self) -> str:
return f"{self._year} {self._make} {self._model}"
class Car(Vehicle):
def __init__(self, make: str, model: str, year: int, doors: int):
super().__init__(make, model, year)
self.__doors = doors # Private (- in UML)
def start(self) -> None:
print(f"Car {self._make} {self._model} started with key.")
def load_passengers(self, count: int) -> None:
max_passengers = self.__doors * 2 # Rough estimate
if count <= max_passengers:
print(f"Loaded {count} passengers.")
else:
print(f"Cannot load {count} passengers. Max: {max_passengers}")
class Truck(Vehicle):
def __init__(self, make: str, model: str, year: int, cargo_capacity: int):
super().__init__(make, model, year)
self.__cargo_capacity = cargo_capacity
def start(self) -> None:
print(f"Truck {self._make} {self._model} started with ignition.")
def load_cargo(self, weight: int) -> bool:
if weight <= self.__cargo_capacity:
print(f"Loaded {weight}kg cargo.")
return True
else:
print(f"Cannot load {weight}kg. Capacity: {self.__cargo_capacity}kg")
return False
class Fleet:
def __init__(self, name: str):
self.__name = name
self.__vehicles: List[Vehicle] = [] # Aggregation: Fleet has Vehicles
def add_vehicle(self, vehicle: Vehicle) -> None:
self.__vehicles.append(vehicle)
print(f"Added {vehicle.get_info()} to {self.__name}")
def start_all(self) -> None:
print(f"Starting all vehicles in {self.__name}:")
for vehicle in self.__vehicles:
vehicle.start()
# Usage
fleet = Fleet("Company Fleet")
car = Car("Toyota", "Camry", 2023, 4)
truck = Truck("Ford", "F-150", 2022, 1000)
fleet.add_vehicle(car)
fleet.add_vehicle(truck)
fleet.start_all()
# Output:
# Added 2023 Toyota Camry to Company Fleet
# Added 2022 Ford F-150 to Company Fleet
# Starting all vehicles in Company Fleet:
# Car Toyota Camry started with key.
# Truck Ford F-150 started with ignition.
car.load_passengers(6)
truck.load_cargo(800)
# Output:
# Loaded 6 passengers.
# Loaded 800kg cargo.
Try it yourself: Add a Motorcycle class that inherits from Vehicle and has a has_sidecar: bool attribute.
Common Mistakes
1. Confusing Association, Aggregation, and Composition
Mistake: Using association (plain line) for everything, or not understanding the ownership difference.
Why it matters: These relationships have different lifecycle implications:
- Association: Independent lifecycles (Student — Course)
- Aggregation: Shared ownership (Department has Professors, but professors can exist without department)
- Composition: Strong ownership (House has Rooms, rooms can’t exist without house)
How to avoid: Ask yourself: “If I delete object A, must object B also be deleted?” If yes, it’s composition. If no, it’s association or aggregation.
Example:
# WRONG: Using simple reference for composition
class House:
def __init__(self):
self.rooms = [] # Should create rooms, not just reference them
# RIGHT: House creates and owns rooms
class House:
def __init__(self, num_rooms: int):
self.__rooms = [Room(f"Room {i}") for i in range(num_rooms)]
# Rooms are created with house and destroyed with house
2. Overcomplicating Diagrams with Too Much Detail
Mistake: Including every getter/setter, private helper method, or implementation detail in the diagram.
Why it matters: Class diagrams are for communication and design, not documentation. Too much detail obscures the important relationships and makes diagrams unreadable.
How to avoid: Include only public interfaces and key private attributes. Omit obvious getters/setters unless they have special logic. Focus on relationships between classes.
Example:
# TOO DETAILED:
┌─────────────────────────┐
│ BankAccount │
├─────────────────────────┤
│ - accountNumber: string │
│ - balance: float │
│ - createdDate: datetime │
│ - lastModified: datetime│
│ - isActive: bool │
├─────────────────────────┤
│ + getAccountNumber() │
│ + setAccountNumber() │
│ + getBalance() │
│ + setBalance() │
│ + getCreatedDate() │
│ + getLastModified() │
│ + setLastModified() │
│ + isActive() │
│ + setActive() │
│ + deposit(amount) │
│ + withdraw(amount) │
│ - validateAmount() │
│ - updateTimestamp() │
│ - logTransaction() │
└─────────────────────────┘
# BETTER:
┌─────────────────────────┐
│ BankAccount │
├─────────────────────────┤
│ - accountNumber: string │
│ - balance: float │
├─────────────────────────┤
│ + deposit(amount): bool │
│ + withdraw(amount): bool│
│ + getBalance(): float │
└─────────────────────────┘
3. Misusing Inheritance Instead of Composition
Mistake: Creating inheritance hierarchies when composition would be clearer (violating “favor composition over inheritance”).
Why it matters: Inheritance creates tight coupling. If you inherit just to reuse code (not because of an “is-a” relationship), you’ll create brittle designs.
How to avoid: Use the “is-a” test. A Car is a Vehicle (inheritance OK). A Car has an Engine (use composition, not inheritance).
Example:
# WRONG: Car inheriting from Engine
class Engine:
def start(self):
print("Engine started")
class Car(Engine): # Car is NOT an Engine!
def drive(self):
self.start()
print("Driving")
# RIGHT: Car has an Engine (composition)
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.__engine = Engine() # Composition
def drive(self):
self.__engine.start()
print("Driving")
4. Incorrect Multiplicity Notation
Mistake: Placing multiplicity on the wrong end of the relationship or using incorrect notation.
Why it matters: Multiplicity tells you how many instances participate in a relationship. Getting it backwards changes the meaning entirely.
How to avoid: Read the relationship as a sentence. “One Customer places zero or more Orders” means Customer has “1” on its side and Order has “0..*” on its side.
Example:
# WRONG:
Customer "0..*" --> "1" Order
(This reads: "Many customers place one order" - incorrect!)
# RIGHT:
Customer "1" --> "0..*" Order
(This reads: "One customer places many orders" - correct!)
Multiplicity cheat sheet:
1= exactly one0..1= zero or one (optional)*or0..*= zero or more1..*= one or moren..m= between n and m
5. Forgetting to Show Abstract Classes and Interfaces Clearly
Mistake: Not distinguishing abstract classes or interfaces from concrete classes in the diagram.
Why it matters: Abstract classes can’t be instantiated. If someone tries to implement your design without knowing a class is abstract, they’ll write incorrect code.
How to avoid: Use italics for abstract class names and methods, or add {abstract} stereotype. For interfaces, use <<interface>> stereotype.
Example:
# UNCLEAR:
┌─────────────┐
│ Shape │ # Looks like a regular class
├─────────────┤
│ + draw() │
└─────────────┘
# CLEAR:
┌─────────────────┐
│ Shape │ # Italicized or marked
│ {abstract} │
├─────────────────┤
│ + draw() {abstract}
└─────────────────┘
# OR for interface:
┌─────────────────┐
│ <<interface>> │
│ Drawable │
├─────────────────┤
│ + draw() │
└─────────────────┘
Interview Tips
1. Start with Nouns and Verbs from Requirements
When asked to “design a system,” extract classes from nouns and methods from verbs in the problem statement.
Example: “Design a parking lot where vehicles can park in spots. The lot has multiple levels.”
- Nouns → Classes: ParkingLot, Vehicle, ParkingSpot, Level
- Verbs → Methods: park(), leave(), findAvailableSpot()
Pro tip: Say this out loud: “I’ll start by identifying the key entities…” This shows structured thinking.
2. Draw the Diagram Before Writing Code
Interviewers want to see you design before implementing. Even in a coding interview, sketch a quick class diagram.
What to say: “Let me sketch the class structure first to make sure we agree on the design before I start coding.”
Time allocation: Spend 20-30% of your time on the diagram. It prevents costly rewrites later.
3. Explain Relationship Choices Out Loud
Don’t just draw lines — explain why you chose composition over association, or inheritance over interfaces.
Example dialogue:
- “I’m using composition here because a Car owns its Engine — the engine doesn’t make sense without the car.”
- “I’m using inheritance here because both Car and Truck are types of Vehicle, so they share common behavior.”
- “I’m using an interface for Payment because we want to support multiple payment methods without tight coupling.”
Why this matters: It shows you understand the tradeoffs, not just the syntax.
4. Know How to Extend Your Design
Interviewers often ask: “How would you add feature X?” or “What if requirements change to Y?”
Prepare to discuss:
- Where would you add a new class?
- Which relationships would change?
- Would you need to refactor existing classes?
Example: “If we need to add a Motorcycle class, I’d extend Vehicle and add specific attributes like hasSidecar. The existing Fleet class wouldn’t need changes because it works with the Vehicle abstraction.”
5. Practice Common Design Problems
These problems appear frequently in interviews. Practice drawing class diagrams for:
- Parking Lot System: Vehicles, spots, levels, payment
- Library Management: Books, members, loans, fines
- E-commerce Platform: Products, orders, customers, payments
- Hotel Booking System: Rooms, reservations, guests, payments
- ATM System: Accounts, transactions, cards, cash dispensers
Practice drill: Set a timer for 10 minutes. Draw a class diagram for one of these. Then implement the core classes in code.
6. Discuss Design Patterns When Relevant
If your class diagram uses a design pattern, mention it. This shows advanced knowledge.
Examples:
- “I’m using the Strategy pattern here for different payment methods.”
- “This is a Factory pattern — the VehicleFactory creates different vehicle types.”
- “I’m applying the Observer pattern so the UI can react to order status changes.”
Warning: Don’t force patterns where they don’t fit. Only mention them if they naturally solve the problem.
7. Handle Ambiguity Confidently
Real-world requirements are vague. Interviewers test how you handle ambiguity.
When unclear, ask:
- “Should a customer be able to have multiple addresses, or just one?”
- “Can a product belong to multiple categories?”
- “Do we need to track order history, or just current orders?”
Then state your assumption: “I’ll assume one address per customer for now, but we can easily extend this to a one-to-many relationship later.”
8. Watch for These Red Flags in Your Design
Interviewers look for these common issues:
- God classes: One class doing everything (e.g., a System class with 20 methods)
- Circular dependencies: ClassA depends on ClassB, which depends on ClassA
- Missing abstractions: Concrete classes everywhere, no interfaces or abstract classes
- Over-engineering: 10 classes for a simple problem
Self-check question: “Is each class doing one thing well, or is it doing too much?“
9. Be Ready to Code from Your Diagram
After drawing the diagram, the interviewer will likely say: “Now implement the core classes.”
Transition smoothly: “Based on this design, I’ll start with the Vehicle abstract class since Car and Truck depend on it.”
Implementation order:
- Base classes / interfaces first
- Concrete implementations next
- Classes with dependencies last
10. Know the Difference Between Class and Object Diagrams
Interviewers sometimes ask: “Can you show me an object diagram for this scenario?”
- Class diagram: Shows the structure (classes and relationships)
- Object diagram: Shows specific instances at runtime
Example: Class diagram shows Customer and Order classes. Object diagram shows “Alice (Customer) placed Order #123 containing 2 items.”
Key Takeaways
- Class diagrams show static structure: They visualize classes, attributes, methods, and relationships, serving as blueprints before coding.
- Master the four key relationships: Association (knows-about), Aggregation (has-a, shared), Composition (owns, strong), Inheritance (is-a). Each has different lifecycle and coupling implications.
- Use visibility modifiers correctly:
+public,-private,#protected. This communicates encapsulation decisions to other developers. - Multiplicity matters: Always specify how many instances participate in relationships (1, 0.., 1..). Read relationships as sentences to verify correctness.
- Design before coding: In interviews, sketch the class diagram first. It shows structured thinking and prevents costly rewrites. Explain your relationship choices out loud to demonstrate understanding of design tradeoffs.