On this page
Description
A UML class diagram is the standard sketch of a system's structure: the classes, and the relationships between them - who uses whom, who holds whom, who owns whom, and how many. In object-oriented design interviews it's how you show your model in two minutes, and in teams it's how people agree on ownership before writing code. This skill covers the relationship notation, multiplicities, and how each line on the diagram becomes a decision in code.
Plain-English descriptions of a system's structure don't pin down ownership - two developers can read the same sentence and build genuinely incompatible object models:
"A House has Rooms, and a Library has Books."
Java
// Developer A assumes composition: a Room can't exist without its House
class House {
private List<Room> rooms = new ArrayList<>();
House() { rooms.add(new Room()); } // House creates its own Rooms
}
// Developer B assumes aggregation: a Book is handed in from outside
class Library {
private List<Book> books;
Library(List<Book> existingBooks) { this.books = existingBooks; } // Books can be shared/moved
}
Python
# Developer A assumes composition: a Room can't exist without its House
class House:
def __init__(self):
self._rooms = [Room()] # House creates its own Rooms
# Developer B assumes aggregation: a Book is handed in from outside
class Library:
def __init__(self, existing_books: list[Book]):
self._books = existing_books # Books can be shared/moved
Nothing in "a House has Rooms" said whether a Room can outlive its House, or be shared between two Houses. The mismatch only surfaces later, as a bug, when one module tries to move a Room between Houses and discovers the other module's code assumed that could never happen.
We'll go from simple to more technical:
- The notation: dependency, association, aggregation, composition, inheritance and realization.
- What the diamonds really mean: who owns the part's lifecycle.
- From diagram to code: fields, multiplicities and two-way associations.
- What a diagram can't tell you, and how to keep it honest.
The notation
ReportGenerator - - -> Logger // Dependency: uses a Logger parameter, doesn't keep it
Driver "1" ------- "1" Car // Association: Driver holds a Car reference long-term
Library "0..1" o---- "0..*" Book // Aggregation: a Book is in at most one Library, and can exist in none
House "1" *------ "1..*" Room // Composition: a Room doesn't exist independently of its House
Four relationship strengths, loosest to tightest: Dependency (a dashed line with a thin open arrowhead - "uses briefly, doesn't keep") is the weakest, for a class glimpsed only as a method parameter or local variable. Association (a plain solid line) is a lasting reference held as a field. Aggregation (hollow diamond o-- on the whole's side) and Composition (filled diamond *--) are both associations that add ownership: the key question at each diamond is "if I delete the whole, does the part get deleted too?" - composition says yes, aggregation says no.
Two more relationships describe types, not instances: a solid line with a hollow triangle pointing at the more general type is inheritance ("is-a" - Car extends Vehicle); a dashed line with the same hollow triangle is realization ("implements" - PayPalProcessor realizes the PaymentProcessor interface). Same arrowhead, different line style, because inheriting an implementation and realizing an interface's contract have different practical implications - an interface has no state or method bodies to inherit.
What the diamonds mean in code
This distinction maps directly onto real ownership decisions in code: composition usually means the "whole" object creates and owns its parts internally (e.g. a House constructs its own Room objects, and nothing else holds a reference to them) - deleting the House object naturally lets its Rooms be garbage collected too, since nothing else references them. Aggregation usually means the "whole" is just handed a reference to something that was (and still could be) managed elsewhere, like a Library holding Book objects that were created independently and could be moved to a different library.
From diagram to code
Every relationship on the diagram becomes a decision in code:
Java
// Composition: the House creates its Rooms and never hands out the live objects
class House {
private final List<Room> rooms = new ArrayList<>();
House(int roomCount) { for (int i = 0; i < roomCount; i++) rooms.add(new Room()); }
List<Room> roomSnapshots() { // copies - outside code can't reach the real rooms
List<Room> out = new ArrayList<>();
for (Room r : rooms) out.add(new Room(r));
return out;
}
}
// Aggregation: the Library is handed Books that live on their own
class Library {
private final List<Book> books = new ArrayList<>();
void add(Book b) { books.add(b); } // stores the caller's object as-is
}
// Team "0..1" ------- "1..*" Player: keep both ends in sync, in one method
class Team {
private final List<Player> roster = new ArrayList<>();
void addPlayer(Player p) {
Team previous = p.getTeam();
if (previous == this) return; // already on this team
if (previous != null) previous.roster.remove(p); // "0..1": leave the old team first
roster.add(p);
p.setTeam(this);
}
void activate() { // "1..*" once the team is active
if (roster.isEmpty()) throw new IllegalStateException("a team needs at least one player");
}
}
Python
# Composition: the House creates its Rooms and never hands out the live objects
class House:
def __init__(self, room_count: int):
self._rooms = [Room() for _ in range(room_count)]
def room_snapshots(self) -> list[Room]: # copies - outside code can't reach the real rooms
return [r.copy() for r in self._rooms]
# Aggregation: the Library is handed Books that live on their own
class Library:
def __init__(self):
self._books: list[Book] = []
def add(self, b: Book) -> None:
self._books.append(b) # stores the caller's object as-is
# Team "0..1" ------- "1..*" Player: keep both ends in sync, in one method
class Team:
def __init__(self):
self._roster: list[Player] = []
def add_player(self, p: Player) -> None:
previous = p.team
if previous is self:
return # already on this team
if previous is not None:
previous._roster.remove(p) # "0..1": leave the old team first
self._roster.append(p)
p.team = self
def activate(self) -> None: # "1..*" once the team is active
if not self._roster:
raise RuntimeError("a team needs at least one player")
Multiplicities turn into types and checks: 0..1 is a nullable field (an Optional in Java, Team | None in Python), 1..* is a collection that must not be empty when it matters, and a two-way association needs one method that updates both ends - otherwise the team's roster and the player's team field drift apart.
Python has no private: a leading underscore (self._rooms) only says "internal - don't touch", and nothing stops outside code from reaching in. So in Python, composition is a discipline the class keeps - it never hands out its live parts, only copies.
Which relationship for which situation?
| In the code... | Draw | Notation |
|---|---|---|
| A class uses another only as a parameter or local variable | Dependency | dashed line, open arrowhead |
| A class keeps a reference to another in a field | Association | solid line |
| The whole holds parts that live independently (handed in, shareable) | Aggregation | hollow diamond on the whole |
| The whole creates and owns parts that die with it | Composition | filled diamond on the whole |
| A class extends another | Inheritance | solid line, hollow triangle at the parent |
| A class implements an interface | Realization | dashed line, hollow triangle at the interface |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Treating the diagram as enforcement | the code drifts from the filled diamonds | make ownership real in code (no leaked parts, copies) |
| A getter that returns a composed part's live list | outside code can modify the "owned" parts | return copies or read-only views |
| Updating only one end of a two-way association | the roster and the player's team disagree | one method updates both ends |
| Expecting multiplicities to express state rules | "zero while setting up, one or more once active" isn't drawable | enforce it in code |
| Trusting a diagram generated from field types | composition and aggregation look identical | decide ownership from how parts are created and shared |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. A diagram shows a dashed line with a thin open arrowhead from ReportGenerator to Logger. Why isn't this an association?
Show answer
Because ReportGenerator doesn't hold a Logger as a field - it only uses one briefly (typically as a method parameter), so the relationship doesn't outlive that one call. That's Dependency, the weakest relationship; Association implies a lasting, stored reference.
2. Why doesn't a UML class diagram, by itself, guarantee that the code actually matches it?
Show answer
The diagram is a communication artifact, not a compiled contract - nothing checks that a filled diamond's "owns and destroys" promise still holds after the code changes. A diagram is only as trustworthy as how recently someone verified it against the real code.
3. You need the diagram to show that a Team can have zero Players while being set up, but must have at least one once it's active. What changes?
Show answer
Nothing changes about which relationship type is used (still association/aggregation between Team and Player) - what changes is the multiplicity label on the Player end, and that label alone can't express "0 during setup, 1+ once active": that's a state-dependent rule the diagram can't encode, so it has to live in the code (e.g. a finalizeTeam() method that rejects an empty roster - not finalize(), which would override Object.finalize(), the deprecated garbage-collection hook).
Key takeaways
- Diamonds answer one question: "if the whole is deleted, does the part get deleted too?" Filled (composition) = yes; hollow (aggregation) = no.
- Dependency (dashed, thin arrowhead) is weaker than Association (solid line) - it's "used briefly", not "held as a field".
- Inheritance (solid, hollow triangle) and Realization (dashed, hollow triangle) both point at a more general type, but one inherits an implementation and the other only implements a contract.
- A diagram is a snapshot the team agreed on, not a checked invariant - it drifts from the real code unless someone deliberately keeps it current.
- Multiplicity notation (
1,0..*,1..*) describes cardinality, not state-dependent rules - those still have to be enforced in code.