Design Class Diagrams in LLD Interviews

Updated 2026-03-11

TL;DR

Class diagrams are visual blueprints showing classes, their attributes, methods, and relationships (inheritance, composition, association). In interviews, you’ll draw these to communicate your design before writing code. Mastering class diagrams helps you think through object relationships and apply design patterns effectively.

Prerequisites: Understanding of classes and objects, basic OOP concepts (encapsulation, inheritance, polymorphism), familiarity with relationships between objects (has-a, is-a). Knowledge of at least one OOP language (Python, Java, or C++).

After this topic: Design and draw class diagrams that clearly show class structure and relationships. Translate verbal requirements into visual class designs. Identify and apply common design patterns in diagram form. Communicate design decisions effectively in technical interviews.

Core Concept

What is a Class Diagram?

A class diagram is a structural diagram from UML (Unified Modeling Language) that shows the static structure of a system. It displays classes as boxes divided into three sections: class name, attributes (fields), and methods (operations).

Class Representation

Each class box contains:

  • Top section: Class name (capitalized, bold)
  • Middle section: Attributes with visibility (+public, -private, #protected) and type
  • Bottom section: Methods with visibility, parameters, and return types

Relationships Between Classes

Inheritance (Is-A): Shown with a solid line and hollow triangle pointing to parent. A Dog is-a Animal.

Composition (Strong Has-A): Solid line with filled diamond at container end. A Car has-a Engine — the engine cannot exist without the car.

Aggregation (Weak Has-A): Solid line with hollow diamond. A Department has Employees — employees can exist independently.

Association: Simple solid line showing a relationship. A Teacher teaches Students.

Dependency: Dashed arrow showing one class uses another temporarily. A Controller depends on a Service.

Multiplicity

Numbers or ranges near relationship lines show how many instances participate:

  • 1 = exactly one
  • 0..1 = zero or one
  • * or 0..* = zero or more
  • 1..* = one or more
  • 3..5 = between three and five

Why Class Diagrams Matter in Interviews

Interviewers use class diagrams to assess your ability to:

  1. Translate requirements into object-oriented design
  2. Choose appropriate relationships between classes
  3. Apply SOLID principles and design patterns
  4. Communicate complex designs quickly and clearly

Drawing a diagram before coding shows structured thinking and prevents costly design mistakes.

Visual Guide

Basic Class Structure

classDiagram
    class Vehicle {
        -string licensePlate
        -int year
        #double mileage
        +start() void
        +stop() void
        +getMileage() double
    }
    note for Vehicle "- private\n# protected\n+ public"

A class box shows name, attributes with visibility modifiers, and methods with return types

Inheritance Relationship

classDiagram
    Animal <|-- Dog
    Animal <|-- Cat
    class Animal {
        -string name
        -int age
        +eat() void
        +sleep() void
    }
    class Dog {
        -string breed
        +bark() void
        +fetch() void
    }
    class Cat {
        -bool indoor
        +meow() void
        +scratch() void
    }

Hollow triangle arrow points from child to parent. Dog and Cat inherit from Animal.

Composition vs Aggregation

classDiagram
    Car *-- Engine : composition
    Car o-- Wheel : aggregation
    University o-- Student : aggregation
    class Car {
        -string model
        +drive() void
    }
    class Engine {
        -int horsepower
        +start() void
    }
    class Wheel {
        -int size
        +rotate() void
    }
    class University {
        -string name
    }
    class Student {
        -string studentId
    }

Filled diamond = composition (strong ownership). Hollow diamond = aggregation (weak ownership).

Association with Multiplicity

classDiagram
    Teacher "1" -- "*" Student : teaches
    Student "*" -- "*" Course : enrolls in
    class Teacher {
        -string employeeId
        +teach() void
    }
    class Student {
        -string studentId
        +study() void
    }
    class Course {
        -string courseCode
        +addStudent() void
    }

Numbers show cardinality: one teacher teaches many students, students enroll in many courses (many-to-many)

Design Pattern Example: Strategy Pattern

classDiagram
    Context o-- Strategy
    Strategy <|.. ConcreteStrategyA
    Strategy <|.. ConcreteStrategyB
    class Context {
        -Strategy strategy
        +setStrategy(Strategy) void
        +executeStrategy() void
    }
    class Strategy {
        <<interface>>
        +execute() void
    }
    class ConcreteStrategyA {
        +execute() void
    }
    class ConcreteStrategyB {
        +execute() void
    }

Dashed line with hollow triangle shows interface implementation. Context uses Strategy interface.

Examples

Example 1: E-Commerce Order System

Requirement: Design a system where customers place orders containing multiple products. Each order has a payment method.

Analysis:

  • Customer has many Orders (aggregation — orders exist independently)
  • Order has many Products (association with multiplicity)
  • Order has one Payment (composition — payment is part of order lifecycle)
from datetime import datetime
from typing import List
from enum import Enum

class PaymentStatus(Enum):
    PENDING = "pending"
    COMPLETED = "completed"
    FAILED = "failed"

class Payment:
    """Composition: Payment cannot exist without Order"""
    def __init__(self, amount: float, method: str):
        self.amount = amount
        self.method = method  # "credit_card", "paypal", etc.
        self.status = PaymentStatus.PENDING
    
    def process(self) -> bool:
        # Payment processing logic
        self.status = PaymentStatus.COMPLETED
        return True

class Product:
    """Independent entity"""
    def __init__(self, product_id: str, name: str, price: float):
        self.product_id = product_id
        self.name = name
        self.price = price

class Order:
    """Aggregates products, composes payment"""
    def __init__(self, order_id: str, customer):
        self.order_id = order_id
        self.customer = customer  # Association
        self.products: List[Product] = []  # Aggregation
        self.payment: Payment = None  # Composition
        self.created_at = datetime.now()
    
    def add_product(self, product: Product):
        self.products.append(product)
    
    def calculate_total(self) -> float:
        return sum(p.price for p in self.products)
    
    def checkout(self, payment_method: str) -> bool:
        total = self.calculate_total()
        self.payment = Payment(total, payment_method)
        return self.payment.process()

class Customer:
    """Has many orders (aggregation)"""
    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] = []
    
    def place_order(self) -> Order:
        order = Order(f"ORD-{len(self.orders) + 1}", self)
        self.orders.append(order)
        return order

# Usage example
customer = Customer("C001", "Alice Smith", "alice@example.com")
order = customer.place_order()

product1 = Product("P001", "Laptop", 999.99)
product2 = Product("P002", "Mouse", 29.99)

order.add_product(product1)
order.add_product(product2)

print(f"Order total: ${order.calculate_total():.2f}")  # Output: Order total: $1029.98
order.checkout("credit_card")
print(f"Payment status: {order.payment.status.value}")  # Output: Payment status: completed

Class Diagram Representation:

Customer "1" o-- "*" Order : places
Order "1" *-- "1" Payment : has
Order "*" -- "*" Product : contains

Try it yourself: Add a ShippingAddress class with composition to Order. Should it be composition or aggregation? Why?


Example 2: Document Editor with Strategy Pattern

Requirement: Design a document editor that can export documents in different formats (PDF, HTML, Markdown). The export strategy should be changeable at runtime.

Analysis:

  • Use Strategy pattern for export behavior
  • Document uses ExportStrategy (dependency/association)
  • Concrete strategies implement ExportStrategy interface
from abc import ABC, abstractmethod

class ExportStrategy(ABC):
    """Strategy interface"""
    @abstractmethod
    def export(self, content: str) -> str:
        pass

class PDFExportStrategy(ExportStrategy):
    """Concrete strategy for PDF export"""
    def export(self, content: str) -> str:
        # Simplified PDF conversion
        return f"<PDF>\n{content}\n</PDF>"

class HTMLExportStrategy(ExportStrategy):
    """Concrete strategy for HTML export"""
    def export(self, content: str) -> str:
        return f"<html>\n<body>\n{content}\n</body>\n</html>"

class MarkdownExportStrategy(ExportStrategy):
    """Concrete strategy for Markdown export"""
    def export(self, content: str) -> str:
        # Convert to markdown format
        return f"# Document\n\n{content}"

class Document:
    """Context class that uses strategy"""
    def __init__(self, title: str, content: str):
        self.title = title
        self.content = content
        self._export_strategy: ExportStrategy = None
    
    def set_export_strategy(self, strategy: ExportStrategy):
        """Change strategy at runtime"""
        self._export_strategy = strategy
    
    def export(self) -> str:
        if not self._export_strategy:
            raise ValueError("Export strategy not set")
        return self._export_strategy.export(self.content)

# Usage example
doc = Document("My Report", "This is the document content.")

# Export as PDF
doc.set_export_strategy(PDFExportStrategy())
print(doc.export())
# Output:
# <PDF>
# This is the document content.
# </PDF>

# Change strategy to HTML
doc.set_export_strategy(HTMLExportStrategy())
print(doc.export())
# Output:
# <html>
# <body>
# This is the document content.
# </body>
# </html>

# Change strategy to Markdown
doc.set_export_strategy(MarkdownExportStrategy())
print(doc.export())
# Output:
# # Document
#
# This is the document content.

Class Diagram Representation:

Document o-- ExportStrategy : uses
ExportStrategy <|.. PDFExportStrategy : implements
ExportStrategy <|.. HTMLExportStrategy : implements
ExportStrategy <|.. MarkdownExportStrategy : implements

Java/C++ Note: In Java, use interface keyword for ExportStrategy. In C++, use pure virtual functions (virtual void export() = 0;).

Try it yourself: Add a JSONExportStrategy that exports the document as JSON with title and content fields. Draw the updated class diagram.


Example 3: Library Management System

Requirement: Design a library system where members can borrow books. Track which books are borrowed and by whom.

from datetime import datetime, timedelta
from typing import List, Optional

class Book:
    """Independent entity"""
    def __init__(self, isbn: str, title: str, author: str):
        self.isbn = isbn
        self.title = title
        self.author = author
        self.is_available = True

class Loan:
    """Association class between Member and Book"""
    def __init__(self, book: Book, member: 'Member', due_days: int = 14):
        self.book = book
        self.member = member
        self.borrowed_date = datetime.now()
        self.due_date = self.borrowed_date + timedelta(days=due_days)
        self.returned_date: Optional[datetime] = None
    
    def is_overdue(self) -> bool:
        if self.returned_date:
            return False
        return datetime.now() > self.due_date
    
    def return_book(self):
        self.returned_date = datetime.now()
        self.book.is_available = True

class Member:
    """Library 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) -> Optional[Loan]:
        if not book.is_available:
            return None
        
        book.is_available = False
        loan = Loan(book, self)
        self.active_loans.append(loan)
        return loan
    
    def return_book(self, book: Book):
        for loan in self.active_loans:
            if loan.book == book and not loan.returned_date:
                loan.return_book()
                self.active_loans.remove(loan)
                break

class Library:
    """Manages books and members"""
    def __init__(self, name: str):
        self.name = name
        self.books: List[Book] = []  # Aggregation
        self.members: List[Member] = []  # Aggregation
        self.loan_history: List[Loan] = []  # Composition
    
    def add_book(self, book: Book):
        self.books.append(book)
    
    def register_member(self, member: Member):
        self.members.append(member)
    
    def process_loan(self, member: Member, book: Book) -> bool:
        loan = member.borrow_book(book)
        if loan:
            self.loan_history.append(loan)
            return True
        return False

# Usage example
library = Library("City Library")

book1 = Book("978-0134685991", "Effective Java", "Joshua Bloch")
book2 = Book("978-0135957059", "The Pragmatic Programmer", "Hunt & Thomas")

library.add_book(book1)
library.add_book(book2)

member = Member("M001", "Bob Johnson")
library.register_member(member)

# Member borrows a book
success = library.process_loan(member, book1)
print(f"Loan successful: {success}")  # Output: Loan successful: True
print(f"Book available: {book1.is_available}")  # Output: Book available: False

# Try to borrow same book again
success = library.process_loan(member, book1)
print(f"Second loan successful: {success}")  # Output: Second loan successful: False

# Return the book
member.return_book(book1)
print(f"Book available after return: {book1.is_available}")  # Output: Book available after return: True

Class Diagram Key Relationships:

Library o-- Book : manages
Library o-- Member : has
Library *-- Loan : tracks
Member "*" -- "*" Book : borrows (through Loan)
Loan "*" -- "1" Book : references
Loan "*" -- "1" Member : references

Try it yourself: Add a Reservation class that allows members to reserve books that are currently borrowed. What relationship should it have with Book and Member?

Common Mistakes

1. Confusing Composition and Aggregation

Mistake: Using composition (filled diamond) when aggregation (hollow diamond) is appropriate, or vice versa.

Why it’s wrong: Composition implies lifecycle dependency — when the container is destroyed, the contained object is destroyed. Aggregation means the contained object can exist independently.

Example: Drawing University *-- Student (composition) is wrong because students exist independently of the university. Use University o-- Student (aggregation) instead.

Fix: Ask yourself: “If I delete the container, should the contained object be deleted too?” If yes, use composition. If no, use aggregation.


2. Overusing Inheritance

Mistake: Creating deep inheritance hierarchies when composition would be better. Drawing Manager --|> Employee --|> Person --|> LivingBeing when you only need Manager and Employee.

Why it’s wrong: Violates “favor composition over inheritance” principle. Makes code rigid and hard to change. Deep hierarchies are difficult to understand and maintain.

Example: Instead of Car --|> Engine --|> Vehicle, use Car *-- Engine and Car --|> Vehicle.

Fix: Use inheritance only for true “is-a” relationships. Prefer composition for “has-a” relationships. Keep inheritance hierarchies shallow (2-3 levels max).


3. Missing Multiplicity

Mistake: Drawing relationship lines without indicating how many instances participate (e.g., Teacher -- Student without 1..*).

Why it’s wrong: Leaves ambiguity about the relationship. Does one teacher teach one student? Many students? This affects implementation significantly.

Example: Order -- Product doesn’t tell you if an order can have multiple products or just one.

Fix: Always add multiplicity to both ends of relationships. Think through: “How many X can one Y have?” and “How many Y can one X have?“


4. Mixing Implementation Details with Design

Mistake: Including getter/setter methods, private helper methods, or implementation-specific details in class diagrams during initial design.

Why it’s wrong: Class diagrams should show the essential structure and public interface, not every implementation detail. Clutters the diagram and obscures the design.

Example: Adding +getFirstName(), +setFirstName(), +getLastName(), +setLastName() when you could just show -firstName: string and -lastName: string.

Fix: In design phase, focus on key attributes and important public methods. Save detailed method signatures for implementation documentation.


5. Incorrect Relationship Direction

Mistake: Drawing arrows pointing the wrong way, especially for inheritance and dependency.

Why it’s wrong: Inheritance arrows should point from child to parent (child knows about parent). Dependency arrows point from dependent to dependency (A depends on B means arrow goes from A to B).

Example: Drawing Animal --|> Dog instead of Dog --|> Animal. The arrow should point toward the parent class.

Fix: Remember: inheritance arrow points to parent (“Dog is-a Animal”), dependency arrow points to what you depend on (“Controller depends on Service”).

Interview Tips

Start with High-Level Classes

When given a design problem, don’t jump into details immediately. Start by identifying the main nouns in the requirements — these become your classes. Write them down as boxes with just the class name. Then identify relationships. This shows structured thinking.

Example approach: “Let me start by identifying the main entities: User, Post, Comment. Now let me think about their relationships…”


Verbalize Your Reasoning

As you draw, explain your choices out loud. Say things like: “I’m using composition here because a Room cannot exist without a House” or “This is aggregation because Students can exist independently of a Course.”

Why it matters: Interviewers want to understand your thought process, not just see the final diagram. Explaining shows you understand the nuances.


Use Standard Notation Correctly

Interviewers expect you to know UML basics. If you’re unsure, ask: “Should I use standard UML notation?” Most will say yes. Know these cold:

  • Solid line + hollow triangle = inheritance
  • Solid line + filled diamond = composition
  • Solid line + hollow diamond = aggregation
  • Dashed line + hollow triangle = interface implementation
  • Solid line = association
  • Dashed arrow = dependency

Pro tip: If you forget a symbol, describe it in words: “This is a composition relationship — the Engine is owned by the Car.”


Identify Design Patterns

If the problem fits a known pattern, mention it. “This looks like a Strategy pattern — I’ll have a context class that uses an interface, with multiple concrete implementations.” Then draw it.

Common interview patterns:

  • Strategy: Interchangeable algorithms (payment methods, sorting strategies)
  • Observer: Notification systems (event listeners, pub-sub)
  • Factory: Object creation (different user types, product variants)
  • Decorator: Adding behavior dynamically (stream wrappers, UI components)
  • Singleton: Single instance (database connection, configuration)

Handle Ambiguity by Asking Questions

Requirements are often vague. Ask clarifying questions: “Can a User have multiple Addresses, or just one?” “Should the Comment be deleted when the Post is deleted?” This shows you think about edge cases.

Template questions:

  • “What’s the cardinality of this relationship?”
  • “Does X own Y, or can Y exist independently?”
  • “Are there any constraints on this relationship?”

Draw Iteratively

Don’t try to create the perfect diagram on the first try. Start simple, then refine. “Let me start with the basic classes, then I’ll add relationships and attributes.”

Progression:

  1. Draw class boxes with names only
  2. Add key relationships
  3. Add important attributes
  4. Add critical methods
  5. Refine based on discussion

Connect to SOLID Principles

If appropriate, mention how your design follows SOLID principles. “I’m using an interface here to follow the Dependency Inversion Principle” or “Each class has a single responsibility.”

Quick SOLID checklist:

  • Single Responsibility: Each class does one thing
  • Open/Closed: Use interfaces for extensibility
  • Liskov Substitution: Subclasses can replace parents
  • Interface Segregation: Small, focused interfaces
  • Dependency Inversion: Depend on abstractions, not concretions

Practice Common Scenarios

Interviewers often ask you to design:

  • Parking lot system: Vehicle hierarchy, parking spots, payment
  • Library system: Books, members, loans, reservations
  • E-commerce: Products, orders, payments, shipping
  • Social media: Users, posts, comments, likes, follows
  • Elevator system: Elevators, floors, requests, scheduling

Practice drawing these systems. Time yourself — aim for a reasonable diagram in 10-15 minutes.


Know When to Stop

Don’t over-engineer. If the interviewer says “that’s enough detail,” stop drawing. They’re testing if you know when a design is “good enough” versus perfect.

Red flag: Spending 30 minutes on a class diagram when the interviewer wanted a quick sketch in 5 minutes.

Green flag: Drawing a clear, simple diagram that captures the essential relationships, then asking “Should I add more detail to any part?”

Key Takeaways

  • Class diagrams visualize structure: They show classes, attributes, methods, and relationships before you write code. In interviews, draw diagrams to communicate design decisions clearly.

  • Master the four key relationships: Inheritance (is-a, hollow triangle), Composition (strong has-a, filled diamond), Aggregation (weak has-a, hollow diamond), and Association (simple relationship, solid line). Know when to use each.

  • Multiplicity matters: Always indicate how many instances participate in relationships (1, , 0..1, 1..). This affects your implementation significantly.

  • Design patterns have standard diagrams: Strategy, Observer, Factory, and other patterns have recognizable class diagram structures. Learn to identify and draw them quickly.

  • Think before you draw: Identify main classes first, then relationships, then details. Verbalize your reasoning as you go. Ask clarifying questions about ambiguous requirements. Start simple and refine iteratively.