Low-level designCore8 min

UML class diagrams

A class diagram shows a design's classes, what each holds and does, and how they relate: owns, refers to, groups, is a kind of, implements, or uses. Six kinds of line cover almost every design you'll draw.

Watch it evolve1 / 9
The problem

A café's orders, drawn from the code

Three classes: an Order, its lines, and the menu items they sell.

80
Start
Stage 1
Stage 2
Final

1 · The class box

One box per class, in three parts

A class diagram is a map of a design's structure: its classes, what each one holds and can do, and how they're connected. It shows what exists, not what happens over time; the order of calls belongs in a sequence diagram. This page follows a café's ordering code, and every diagram on it is drawn from that code.

The first version has three classes. An Order has lines, and each OrderLine is a quantity of one MenuItem:

Orders, lines and menu itemsJava
class MenuItem {
    private final String name;
    private final int cents;

    MenuItem(String name, int cents) {
        this.name = name;
        this.cents = cents;
    }

    String name() {
        return name;
    }

    int priceCents() {
        return cents;
    }
}

class OrderLine {
    private final MenuItem item;
    private final int quantity;

    OrderLine(MenuItem item, int quantity) {
        this.item = item;
        this.quantity = quantity;
    }

    int quantity() {
        return quantity;
    }

    int totalCents() {
        return item.priceCents() * quantity;
    }
}

class Order {
    private static int nextNumber = 1;

    private final int number;
    private final List<OrderLine> lines = new ArrayList<>();

    Order() {
        number = nextNumber++;
    }

    int number() {
        return number;
    }

    void add(MenuItem item, int quantity) {
        lines.add(new OrderLine(item, quantity));
    }

    int itemCount() {
        return lines.stream().mapToInt(OrderLine::quantity).sum();
    }

    int totalCents() {
        return lines.stream().mapToInt(OrderLine::totalCents).sum();
    }
}
*MenuItem− name: String− cents: int~ name(): String~ priceCents(): intOrderLine− item: MenuItem− quantity: int~ quantity(): int~ totalCents(): intOrder− nextNumber: int− number: int− lines: List<OrderLine>~ number(): int~ add(MenuItem, int)~ itemCount(): int~ totalCents(): int

Each box has a name on top, then the attributes (fields), then the operations (methods).

Each line in a box follows one pattern. An attribute is visibility name: Type, such as - number: int. An operation is visibility name(parameter types): ReturnType, such as ~ add(MenuItem, int); with no return type, it returns nothing. A few more marks carry meaning:

  • an underlined member is static: nextNumber belongs to the class, not to each order;
  • an italic name is abstract: it has no body here, and a subclass provides one;
  • «interface» or «enum» above a name says what kind of type it is.

Mark

Visibility

Who can use it

+

Public

Any code

-

Private

Only the class itself

#

Protected

The class and its subclasses

~

Package

Code in the same package

The diagrams here are drawn from the Java version, where a method with no modifier is visible within its package, so most methods show ~. The other languages map differently: C++ has public, protected and private; JavaScript has public and #private; Python has no enforced visibility, and a leading underscore says "internal" by convention.

2 · The six lines

Every line answers: how are these two related?

Lines between boxes are the important part of a class diagram. Each kind says something different about how two classes depend on each other, and each has a typical shape in code:

Relationship

Drawn as

Read it as

In the café

Association

Solid line, open arrowhead

refers to

OrderLine → MenuItem

Composition

Solid line, filled diamond at the whole

is made of, and owns

Order ◆ OrderLine

Aggregation

Solid line, hollow diamond at the whole

groups, without owning

PastryBox ◇ Pastry

Generalization

Solid line, hollow triangle at the parent

is a kind of

Drink ▷ MenuItem

Realization

Dashed line, hollow triangle at the interface

implements

HappyHour ▷ Discount

Dependency

Dashed line, open arrowhead

uses, briefly

Order ⇢ Discount

An arrowhead points from the class that knows to the class it knows about: OrderLine holds a MenuItem, and the menu item has no idea which lines point to it. The rest of the page builds the café up one kind of line at a time.

3 · Owning and referring

Composition and association

An order creates its lines and keeps them. A line never moves to another order, and when the order is gone, so are its lines. That's composition: a whole made of parts that belong to it alone, drawn with a filled diamond at the whole.

The order creates and keeps its linesJava
class Order {
    private static int nextNumber = 1;

    private final int number;
    private final List<OrderLine> lines = new ArrayList<>();

    Order() {
        number = nextNumber++;
    }

    int number() {
        return number;
    }

    void add(MenuItem item, int quantity) {
        lines.add(new OrderLine(item, quantity));
    }

    int itemCount() {
        return lines.stream().mapToInt(OrderLine::quantity).sum();
    }

    int totalCents() {
        return lines.stream().mapToInt(OrderLine::totalCents).sum();
    }
}

A line refers to a menu item, but doesn't own it: the croissant on the menu exists before any order and is shared by every line that sells one. That's an association, a plain solid line with an arrowhead toward the class being referred to.

The * next to OrderLine is a multiplicity: how many objects can be at that end. An order has any number of lines. The common values are 1 (exactly one), 0..1 (optional), * (any number) and 1..* (at least one). These diagrams show the number at the far end; a fuller diagram also marks the near end, such as 1 next to Order, since each line belongs to exactly one order.

In C++ the difference shows in the types: Order holds its OrderLine objects by value, in a std::vector<OrderLine>, while each line holds a std::shared_ptr to a menu item that others share.

4 · Kinds of

Generalization: a drink is a kind of menu item

The café's items differ: a drink costs more when large, a pastry has a fixed price, and a pastry box costs 10% less than its pastries bought one by one. MenuItem becomes abstract, with an abstract priceCents(), and each kind extends it.

One abstract parent, three kindsJava
abstract class MenuItem {
    private final String name;

    protected MenuItem(String name) {
        this.name = name;
    }

    String name() {
        return name;
    }

    abstract int priceCents();
}

class Drink extends MenuItem {
    private final int cents;
    private final boolean large;

    Drink(String name, int cents, boolean large) {
        super(name);
        this.cents = cents;
        this.large = large;
    }

    int priceCents() {
        return large ? cents + 80 : cents;
    }
}

class PastryBox extends MenuItem {
    private final List<Pastry> pastries;

    PastryBox(String name, List<Pastry> pastries) {
        super(name);
        this.pastries = List.copyOf(pastries);
    }

    // 10% off the pastries bought one by one.
    int priceCents() {
        return pastries.stream().mapToInt(Pastry::priceCents).sum() * 90 / 100;
    }
}
**«abstract»MenuItem− name: String~ name(): String~ priceCents(): intDrink− cents: int− large: boolean~ priceCents(): intPastry− cents: int~ priceCents(): intPastryBox− pastries: List<Pastry>~ priceCents(): intOrderLine− item: MenuItem− quantity: int~ quantity(): int~ totalCents(): intOrder− nextNumber: int− number: int− lines: List<OrderLine>~ number(): int~ add(MenuItem, int)~ itemCount(): int~ totalCents(): int

Generalization: each kind points to MenuItem with a hollow triangle. The abstract class and its abstract method are in italics.

Nothing about OrderLine changed. It still refers to a MenuItem, and any kind can stand in for one. The diagram shows that at a glance: the line to MenuItem is the only line OrderLine has.

5 · Grouping

Aggregation: a box of pastries it doesn't own

A pastry box holds three pastries, but those same Pastry objects are on the menu and sold on their own too. The box groups them without owning them. That's aggregation, drawn with a hollow diamond at the group.

The box keeps references to pastries made elsewhereJava
class PastryBox extends MenuItem {
    private final List<Pastry> pastries;

    PastryBox(String name, List<Pastry> pastries) {
        super(name);
        this.pastries = List.copyOf(pastries);
    }

    // 10% off the pastries bought one by one.
    int priceCents() {
        return pastries.stream().mapToInt(Pastry::priceCents).sum() * 90 / 100;
    }
}

Aggregation is the vaguest of the six. The UML standard itself leaves its exact meaning to whoever draws it, so readers may take a hollow diamond in different ways. A safe rule: draw composition when the parts live and die with the whole, an association when one object merely refers to another, and aggregation only when "a group of things that exist on their own" is the point you want to make.

6 · Contracts and uses

Realization and dependency

Happy hour takes 20% off. Discounts may multiply later, so the order works with an interface Discount, and HappyHour implements it. The order doesn't keep a discount: it's handed one when it computes a discounted total, uses it, and lets it go.

An interface, its implementation, and a method that uses itJava
interface Discount {
    int apply(int cents);
}

class HappyHour implements Discount {
    // 20% off.
    public int apply(int cents) {
        return cents * 80 / 100;
    }
}

class Order {
    private static int nextNumber = 1;

    private final int number;
    private final List<OrderLine> lines = new ArrayList<>();

    Order() {
        number = nextNumber++;
    }

    int number() {
        return number;
    }

    void add(MenuItem item, int quantity) {
        lines.add(new OrderLine(item, quantity));
    }

    int itemCount() {
        return lines.stream().mapToInt(OrderLine::quantity).sum();
    }

    int totalCents() {
        return lines.stream().mapToInt(OrderLine::totalCents).sum();
    }

    int discountedCents(Discount discount) {
        return discount.apply(totalCents());
    }
}
**«interface»Discount+ apply(int): intHappyHour+ apply(int): int«abstract»MenuItem− name: String~ name(): String~ priceCents(): intDrink− cents: int− large: boolean~ priceCents(): intPastry− cents: int~ priceCents(): intPastryBox− pastries: List<Pastry>~ priceCents(): intOrderLine− item: MenuItem− quantity: int~ quantity(): int~ totalCents(): intOrder− nextNumber: int− number: int− lines: List<OrderLine>~ number(): int~ add(MenuItem, int)~ itemCount(): int~ totalCents(): int~ discountedCents(Discount): int

Realization: HappyHour points to Discount with a dashed line and a hollow triangle. Dependency: Order uses Discount (and MenuItem) only as parameters, drawn as dashed arrows.

A dependency is the weakest link: one class uses another for the length of a call, as a parameter, a local variable or a return type, without holding on to it. It still matters, because a change to Discount can break Order. In Python the realization isn't written at all: HappyHour never names Discount, and a type checker accepts it because it has the right apply() method.

Order 1: 4 items, $19.16
With happy hour: $15.32
Order 2: 1 item, $2.60

7 · Drawing well

A diagram is for a reader

  • Show what the reader needs. A diagram for a design review needs the classes, their key operations and the relationships; it doesn't need every getter.
  • Read every line as a sentence before you draw it: "an order is made of lines", "a box groups pastries". If the sentence is false, so is the line.
  • Use the weakest relationship that's true. Every line is a dependency someone must keep working; a dependency is lighter than an association, and an association lighter than composition.

Two habits keep a diagram honest:

  • Add multiplicities where they carry a rule: 0..1 says optional, 1..* says never empty.
  • Keep it in step with the code. A hand-drawn diagram goes stale as soon as the code changes. These are drawn from the code, so they can't drift.

In the diagram

Java

C++

Python

«interface»

interface

A class with only pure virtual functions

typing.Protocol or an abstract base class

Abstract class

abstract class

A class with a pure virtual function

abc.ABC with @abstractmethod

Composition

A field created and kept by the owner

A member held by value

An attribute created in __init__

Static member

static

static

A class attribute

JavaScript has no interfaces or abstract classes: an interface is a documented shape that objects follow, and an abstract method is one that throws until a subclass overrides it.

8 · In practice

Draw by writing code

The practice editor draws the class diagram of your code as you type, with the same lines as this page, so you can check that each relationship says what you meant. The next page, Modeling entities and relationships, is about choosing between them.

Design the parking lot and watch its diagram take shape.

Check yourself

4 questions

1. A Course keeps a list of Student objects that are created by enrollment and exist in other courses too. Which line fits best?
2. What does a dashed line with a hollow triangle mean?
3. Order.discountedCents(Discount d) uses the discount and doesn't store it. How is that drawn?
4. In a class box, - cents: int and ~ priceCents(): int mean what?

Takeaways

Remember this

  • A class box has three parts: the name, the attributes, and the operations, each line marked with its visibility.
  • Composition (filled diamond) means the whole creates and owns its parts; association (plain arrow) means one object refers to another.
  • A hollow triangle means "is a kind of": solid for extending a class, dashed for implementing an interface.
  • A dashed arrow is a dependency: a type used briefly, as a parameter, local or return type.
  • Use the weakest relationship that's true, and read every line as a sentence before you draw it.