Last updated: 2026-09-18
Class Diagrams: Design Before Code
Through the early 1990s, three separate notations for describing object-oriented systems were competing for the same job: Grady Booch's, James Rumbaugh's OMT, and Ivar Jacobson's Objectory. When the three joined forces at Rational Software, they merged their notations into one1, and the Object Management Group adopted the result as the Unified Modeling Language in 19972. A class diagram is the part of UML most programmers actually use day to day: a small, precise vocabulary for drawing the static structure of a system — which classes exist, what each one knows and does, and how they relate to each other — before any of it is code.
That last point is the one worth taking seriously. A class diagram drawn before implementation is a design tool: a cheap place to argue about whether an Order should own its LineItems outright or merely reference them, before that decision is baked into constructors and destructors. A class diagram drawn after implementation, generated from the code, is documentation — useful, but a different kind of artefact, and one that goes stale the moment the code changes underneath it.
Anatomy of a Class Box
Every class in a UML class diagram is a rectangle divided into up to three compartments: the class name, its attributes, and its operations (methods). Visibility is marked with a prefix symbol rather than a keyword — + for public, - for private, # for protected, ~ for package-visible — and two typographic conventions carry extra meaning: an italicised name marks an abstract class or method, and an underlined name marks something static (shared by the class rather than by each instance).
Nothing in that box says anything about how withdraw checks for sufficient funds, or what happens on failure — a class diagram deliberately stops at the interface. The behaviour inside a method belongs to a different UML diagram (a sequence or activity diagram) or, more often in practice, to the code itself.
Relationships Between Classes
The value of a class diagram is less in any single box and more in the lines between boxes — UML gives each kind of relationship its own arrowhead, and mixing them up changes the meaning of the diagram, not just its appearance.
| Relationship | Notation | Meaning |
|---|---|---|
| Association | Plain line | "Knows about" — one class holds a reference to another |
| Aggregation | Line with open (hollow) diamond at the whole | "Has-a", but the part can outlive the whole |
| Composition | Line with filled diamond at the whole | "Owns-a" — the part's lifetime is bound to the whole's |
| Generalization | Line with hollow triangle arrowhead | "Is-a" — inheritance between classes |
| Realization | Dashed line with hollow triangle arrowhead | A class implements an interface's contract |
| Dependency | Dashed line with open arrowhead | "Uses" — a weaker, often temporary relationship (a parameter, a local variable) |
Aggregation versus composition is the distinction students draw wrong most often, because both read as "has-a" in casual English3. The test that actually separates them is a lifecycle question, not an ownership one: if the whole object were deleted right now, would the part still make sense to exist on its own? A Library and its Books are aggregation — delete the Library object and the books still exist, physically and logically, and could belong to a different library. An Order and its LineItems are composition — a line item with no order to belong to is meaningless, and deleting the order should delete its line items with it.
Everything past the boxes in that diagram is a decision. Choosing aggregation over composition for Library–Book is a claim that a book's data can be shared or moved between libraries independently; choosing composition for Order–LineItem is a claim that a line item is never meaningful detached from its order. Get the choice right on the diagram and the constructors, copy semantics, and cleanup code that follow are consequences of a decision already made — get it wrong, and the code ends up fighting the model. A relational database models the same Library–Book domain with a different vocabulary entirely — see Relational Modelling for how cardinality and foreign keys answer the same "how do these things relate" question without any notion of object ownership at all.
Multiplicity Notation
The numbers at each end of a relationship line — its multiplicity — state how many instances of one class can be associated with a single instance of the other at any moment.
| Notation | Meaning |
|---|---|
1 |
Exactly one |
0..1 |
Zero or one (optional) |
0..* (or *) |
Zero or more |
1..* |
One or more (at least one) |
3..5 |
Between three and five, inclusive |
Multiplicity is where a class diagram earns its keep as a design check rather than just a picture: Order "1" *-- "1..*" LineItem is a claim that an order is never valid with zero line items, and that claim should show up later as a validation rule or a constructor precondition — if it doesn't, the diagram and the code have quietly drifted apart.
From Diagram to Code
Translating the relationships above into a concrete language is mechanical once the diamond, triangle, and dashed-versus-solid distinctions are fixed. Composition — the strong, lifetime-bound relationship — is usually the one that most changes shape depending on the target language, because "the part's lifetime is bound to the whole's" means something different in a garbage-collected language than it does in one with deterministic destruction.
class LineItem:
def __init__(self, quantity: int, unit_price: float):
self.quantity = quantity
self.unit_price = unit_price
class Order:
def __init__(self, order_id: str):
self.order_id = order_id
self._items: list[LineItem] = [] # created and owned here
def add_item(self, quantity: int, unit_price: float) -> None:
# Order constructs its own LineItems — they have no
# existence independent of this Order.
self._items.append(LineItem(quantity, unit_price))
#include <vector>
class LineItem {
public:
LineItem(int qty, double price) : quantity(qty), unitPrice(price) {}
int quantity;
double unitPrice;
};
class Order {
std::string orderId;
std::vector<LineItem> items; // owned by value: destroyed with the Order
public:
explicit Order(std::string id) : orderId(std::move(id)) {}
void addItem(int qty, double price) {
items.emplace_back(qty, price);
}
};
import java.util.ArrayList;
import java.util.List;
public final class LineItem {
private final int quantity;
private final double unitPrice;
public LineItem(int quantity, double unitPrice) {
this.quantity = quantity;
this.unitPrice = unitPrice;
}
}
public final class Order {
private final String orderId;
private final List<LineItem> items = new ArrayList<>(); // Order-owned
public Order(String orderId) {
this.orderId = orderId;
}
public void addItem(int quantity, double unitPrice) {
items.add(new LineItem(quantity, unitPrice));
}
}
using System.Collections.Generic;
public sealed class LineItem {
public int Quantity { get; }
public double UnitPrice { get; }
public LineItem(int quantity, double unitPrice) {
Quantity = quantity;
UnitPrice = unitPrice;
}
}
public sealed class Order {
private readonly string _orderId;
private readonly List<LineItem> _items = new(); // owned by the Order
public Order(string orderId) => _orderId = orderId;
public void AddItem(int quantity, double unitPrice) =>
_items.Add(new LineItem(quantity, unitPrice));
}
LineItem = Struct.new(:quantity, :unit_price)
class Order
def initialize(order_id)
@order_id = order_id
@items = [] # created and owned here
end
def add_item(quantity, unit_price)
@items << LineItem.new(quantity, unit_price)
end
end
Notice what all five versions have in common, regardless of syntax: LineItem objects are only ever constructed inside Order, never handed in from outside and never exposed for a caller to hold onto independently. That is composition, expressed as a constructor discipline rather than a language keyword — C++ happens to make the lifetime binding automatic through value semantics, but Java, C#, Python and Ruby all achieve the same design intent through convention: nothing outside Order is ever given a way to construct or outlive a LineItem.
When a Diagram Earns Its Keep
A class diagram is cheapest and most useful at exactly the moment most students skip it: before the first class is written, on a whiteboard or a scrap of paper, as a way of arguing about a design with someone else (or with a future version of yourself) before the cost of changing it goes up. Three or four boxes and the relationships between them can expose a wrong assumption — a supposed one-to-one relationship that is really one-to-many, a composition that should have been an aggregation — in minutes, rather than after a refactor.
The diagram is much less useful, though still not worthless, drawn after the code exists, generated automatically as documentation. It can still orient a new reader quickly, but it inherits every structural problem already in the code — a diagram of a tangled design is a tangled diagram — and it starts going stale the moment anyone edits a class without redrawing it. Treat a reverse-engineered diagram as a snapshot, not a source of truth, and be suspicious of any diagram nobody has updated since the code it describes last changed.
The relationships covered here — especially generalization and realization — are the same vocabulary used to describe the structure behind common design patterns: a Strategy pattern is a class diagram with one interface and several realizing classes; a Composite pattern is a composition relationship between a component and a collection of itself. Reading a pattern's diagram fluently, and being able to draw your own before writing the classes it describes, is the same skill practised on both a small and a large scale.
References
Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The Unified Modeling Language User Guide (2nd ed.). Addison-Wesley. Held by the University of Reading Library. ↩
Object Management Group. (2017). Unified Modeling Language, Version 2.5.1. https://www.omg.org/spec/UML/2.5.1/About-UML ↩
Fowler, M., & Scott, K. (1999). UML Distilled: A Brief Guide to the Standard Object Modeling Language (2nd ed.). Addison-Wesley. Held by the University of Reading Library. ↩