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.
A café's orders, drawn from the code
Three classes: an Order, its lines, and the menu items they sell.
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:
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();
}
}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:
nextNumberbelongs 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 |
|
Composition | Solid line, filled diamond at the whole | is made of, and owns |
|
Aggregation | Solid line, hollow diamond at the whole | groups, without owning |
|
Generalization | Solid line, hollow triangle at the parent | is a kind of |
|
Realization | Dashed line, hollow triangle at the interface | implements |
|
Dependency | Dashed line, open arrowhead | uses, briefly |
|
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.
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.
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;
}
}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.
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.
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());
}
}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..1says 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» |
| A class with only pure virtual functions |
|
Abstract class |
| A class with a pure virtual function |
|
Composition | A field created and kept by the owner | A member held by value | An attribute created in |
Static member |
|
| 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
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.