InterviewKata
Low-level design · Module 05 of 6 · Level 1: Foundations

Class and sequence diagrams

You'll draw your design before you code it. This module covers just enough UML for a whiteboard: the six relationships in a class diagram, and sequence diagrams that show one use case step by step.

1. The idea

A map of a city and a set of directions are different drawings of the same place. The map (a class diagram) shows what exists and how it's connected; the directions (a sequence diagram) show one trip through it, step by step. You need both, and neither needs every street.

Class diagrams

A box per class: name, the important fields, the important methods. Lines say how classes relate:

RelationshipNotationMeansExample
AssociationA —— BA refers to B; both live independentlyOrder → Customer
AggregationA ◇—— BA groups Bs that can outlive itTeam ◇ Player
CompositionA ◆—— BA owns Bs; they die with itOrder ◆ LineItem
InheritanceB ——▷ AB is an ACardPayment ▷ Payment
RealisationB - - -▷ AB implements interface AEmailChannel ▷ Channel
DependencyA - - -> BA uses B briefly (a parameter)Order ⇢ TaxCalculator

Add multiplicities where they matter (1, 0..1, 1..*, 0..*). In an interview, fields and methods can be partial: draw what the discussion needs.

Sequence diagrams

Objects across the top, time running down, one arrow per call, dashed arrows for returns. Draw one for the main use case; it's where missing methods and wrong responsibilities show up.

2. How it works

The checkout from the code below as a sequence diagram: each step is one message between objects.

3. Try it yourself

Name the relationship

For each pair, choose the relationship you'd draw.

4. In code

Each class is commented with the relationship it has in the diagram: association, composition, inheritance and dependency.

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class Customer:                       # Customer "1" --places--> "0..*" Order   (association)
    name: str


@dataclass(frozen=True)
class LineItem:                       # Order "1" <>-- "1..*" LineItem   (composition: items die with the order)
    sku: str
    price: int
    qty: int


class Payment(ABC):                   # Payment <|-- CardPayment   (inheritance / realisation)
    @abstractmethod
    def describe(self) -> str: ...


class CardPayment(Payment):
    def __init__(self, last4: str) -> None:
        self.last4 = last4

    def describe(self) -> str:
        return f"card ending {self.last4}"


class TaxCalculator:                  # Order ..> TaxCalculator   (dependency: used, not stored)
    def tax(self, amount: int) -> int:
        return amount * 8 // 100


class Order:
    def __init__(self, customer: Customer) -> None:
        self.customer = customer      # association: a reference to an object that lives on its own
        self.items: list[LineItem] = []   # composition: created and owned here
        self.payment: Payment | None = None

    def add_item(self, sku: str, price: int, qty: int) -> None:
        self.items.append(LineItem(sku, price, qty))

    def total(self, tax: TaxCalculator) -> int:   # dependency: passed in for this call only
        subtotal = sum(i.price * i.qty for i in self.items)
        return subtotal + tax.tax(subtotal)


if __name__ == "__main__":
    ana = Customer("Ana")
    order = Order(ana)
    order.add_item("keyboard", 5000, 1)
    order.add_item("cable", 500, 2)
    order.payment = CardPayment("4242")
    print(f"{order.customer.name}: {len(order.items)} line items, total {order.total(TaxCalculator())}, paid by {order.payment.describe()}")
class Customer { constructor(readonly name: string) {} } // Customer "1" --places--> "0..*" Order   (association)

interface LineItem { sku: string; price: number; qty: number; } // Order "1" <>-- "1..*" LineItem   (composition)

abstract class Payment { abstract describe(): string; } // Payment <|-- CardPayment   (inheritance)

class CardPayment extends Payment {
  constructor(private last4: string) { super(); }
  describe(): string { return `card ending ${this.last4}`; }
}

class TaxCalculator { // Order ..> TaxCalculator   (dependency: used, not stored)
  tax(amount: number): number { return Math.floor((amount * 8) / 100); }
}

class Order {
  readonly items: LineItem[] = []; // composition: created and owned here
  payment: Payment | null = null;
  constructor(readonly customer: Customer) {} // association: a reference to an object that lives on its own

  addItem(sku: string, price: number, qty: number): void { this.items.push({ sku, price, qty }); }

  total(tax: TaxCalculator): number { // dependency: passed in for this call only
    const subtotal = this.items.reduce((s, i) => s + i.price * i.qty, 0);
    return subtotal + tax.tax(subtotal);
  }
}

const order = new Order(new Customer("Ana"));
order.addItem("keyboard", 5000, 1);
order.addItem("cable", 500, 2);
order.payment = new CardPayment("4242");
console.log(`${order.customer.name}: ${order.items.length} line items, total ${order.total(new TaxCalculator())}, paid by ${order.payment.describe()}`);
import java.util.ArrayList;
import java.util.List;

record Customer(String name) {} // Customer "1" --places--> "0..*" Order   (association)

record LineItem(String sku, int price, int qty) {} // Order "1" <>-- "1..*" LineItem   (composition)

abstract class Payment { // Payment <|-- CardPayment   (inheritance)
    abstract String describe();
}

class CardPayment extends Payment {
    private final String last4;

    CardPayment(String last4) {
        this.last4 = last4;
    }

    String describe() {
        return "card ending " + last4;
    }
}

class TaxCalculator { // Order ..> TaxCalculator   (dependency: used, not stored)
    int tax(int amount) {
        return amount * 8 / 100;
    }
}

class Order {
    final Customer customer; // association: a reference to an object that lives on its own
    final List<LineItem> items = new ArrayList<>(); // composition: created and owned here
    Payment payment;

    Order(Customer customer) {
        this.customer = customer;
    }

    void addItem(String sku, int price, int qty) {
        items.add(new LineItem(sku, price, qty));
    }

    int total(TaxCalculator tax) { // dependency: passed in for this call only
        int subtotal = items.stream().mapToInt(i -> i.price() * i.qty()).sum();
        return subtotal + tax.tax(subtotal);
    }
}

public class Main {
    public static void main(String[] args) {
        Order order = new Order(new Customer("Ana"));
        order.addItem("keyboard", 5000, 1);
        order.addItem("cable", 500, 2);
        order.payment = new CardPayment("4242");
        System.out.println(order.customer.name() + ": " + order.items.size() + " line items, total " + order.total(new TaxCalculator())
                + ", paid by " + order.payment.describe());
    }
}

Output (the same in all three languages)

Ana: 2 line items, total 6480, paid by card ending 4242

5. In the interview

6. Remember it

Memory hook: "The map shows what is; the directions show one trip"Class diagrams show structure; sequence diagrams show one use case over time.
How this hook works: what “The map shows what is; the directions show one trip” means

What the slogan means, phrase by phrase

“The map shows what is”
Class diagram: classes, their parts and relationships, true at all times.
“the directions show one trip”
Sequence diagram: who calls whom, in what order, for one scenario.

Why this picture fits: A city map and turn-by-turn directions describe the same place, for different questions.

The story → the concept

The cityThe design
Streets and buildingsClasses and relationships
Owned vs nearby buildingsComposition vs association
Turn-by-turn directionsA sequence diagram
Not every streetOnly what the discussion needs

Watch it on an example

  1. Map: Order ◆ LineItem, Order → Customer, CardPayment ▷ Payment.
  2. Directions: Customer → Order.addItem → Order.total → TaxCalculator.tax → Payment.describe.
  3. A missing arrow (who computes tax?) shows a missing dependency.

The trap it saves you from

Spending ten minutes on perfect UML while the design stays wrong.

It still works when…

The interviewer prefers code. A two-minute class sketch still prevents most wrong turns.

7. Common mistakes

Every field and getterClutters the diagram. Show what matters for the design.
Composition vs association confusedAsk: does the part die with the whole?
No interfaces shownThe variation points are the most important part of the drawing.
Only a class diagramStructure looks fine until you trace a call. Draw one sequence.

8. Practice exercise (25 minutes)

  1. Draw a class diagram (10 min). For the library problem (P09), from memory.
  2. Draw a sequence (10 min). Checkout of a book, including the waitlist check.
  3. Compare (5 min). Open P09 and compare. What did you miss?

9. Self-check quiz

An order's line items are deleted with the order. Which relationship?

The parts die with the whole.

A method receives a TaxCalculator as a parameter and doesn't keep it. Which relationship?

Used briefly, not stored.

What does a sequence diagram show?

Objects across the top, time down.

Which part of a class diagram matters most in an interview?

That's where the design decisions are.

10. Go deeper

ResourceTimeWhy
Class diagram (Wikipedia)ReferenceThe full notation, including the relationship arrows.
Diagramming and documenting (Software architecture)30 minC4 and other views one level up.
Module 06: ConcurrencyNextThe follow-up every LLD interview asks.