UML & Class Design Basics · UML Basics

UML Class Diagram Basics

Telling association, aggregation, and composition apart - and why the difference matters for who owns an object's lifetime.

  • 8 min read
  • 17 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The notation
  3. What the diamonds mean in code
  4. From diagram to code
  5. Which relationship for which situation?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

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:

  1. The notation: dependency, association, aggregation, composition, inheritance and realization.
  2. What the diamonds really mean: who owns the part's lifecycle.
  3. From diagram to code: fields, multiplicities and two-way associations.
  4. 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...DrawNotation
A class uses another only as a parameter or local variableDependencydashed line, open arrowhead
A class keeps a reference to another in a fieldAssociationsolid line
The whole holds parts that live independently (handed in, shareable)Aggregationhollow diamond on the whole
The whole creates and owns parts that die with itCompositionfilled diamond on the whole
A class extends anotherInheritancesolid line, hollow triangle at the parent
A class implements an interfaceRealizationdashed line, hollow triangle at the interface

Common mistakes

MistakeWhat happensDo this instead
Treating the diagram as enforcementthe code drifts from the filled diamondsmake ownership real in code (no leaked parts, copies)
A getter that returns a composed part's live listoutside code can modify the "owned" partsreturn copies or read-only views
Updating only one end of a two-way associationthe roster and the player's team disagreeone method updates both ends
Expecting multiplicities to express state rules"zero while setting up, one or more once active" isn't drawableenforce it in code
Trusting a diagram generated from field typescomposition and aggregation look identicaldecide 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.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

In the code...Draw
Used only as a parameter or localDependency (dashed, open arrow)
Kept in a fieldAssociation (solid line)
Parts handed in, outlive the wholeAggregation (hollow diamond)
Parts created and owned, die with the wholeComposition (filled diamond)
extendsInheritance (solid, hollow triangle)
implementsRealization (dashed, hollow triangle)
Data about the link itselfan association class (Membership)

Snippets worth knowing by heart

Java

void addPlayer(Player p) {                       // one method keeps both ends in sync
    Team previous = p.getTeam();
    if (previous == this) return;
    if (previous != null) previous.roster.remove(p);
    roster.add(p);
    p.setTeam(this);
}

Python

def add_player(self, p: Player) -> None:         # one method keeps both ends in sync
    previous = p.team
    if previous is self:
        return
    if previous is not None:
        previous._roster.remove(p)
    self._roster.append(p)
    p.team = self

The traps

  • The diagram isn't enforcement - code must keep the promise.
  • Leaking a composed part through a getter.
  • Updating one end of a two-way association.
  • State rules in multiplicities - enforce them in code.
  • Ownership read from generated diagrams.

Staff-level points, one line each

  • Keep diagrams as code (Mermaid, PlantUML) next to the code, reviewed in PRs.
  • Generated diagrams can't distinguish composition from aggregation.
  • C4: context, containers, components, code - class diagrams are the last zoom level.
  • Composition = ON DELETE CASCADE (+ ORM orphan removal); aggregation = SET NULL/RESTRICT.
  • SQLite ignores foreign keys without PRAGMA foreign_keys = ON - the rooms survived their house.
  • {ordered} = List, {unique} = Set; association classes = join entities; qualifiers = maps; /derived = methods.
  • Prefer one-way associations; two-way ones need a sync method.
  • Interview sketch: nouns, ownership, multiplicity, interfaces at the variation points.

Say it in an interview

  • "A ticket can't exist without its booking, so it's composition - and ON DELETE CASCADE in the schema."
  • "The join date belongs to the membership, not to the player or the team."
  • "I'll keep this association one-way; nothing needs to navigate back."
  • "Payment methods vary, so PaymentMethod is an interface on the diagram."

Practice

This skill comes with 17 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice UML Class Diagram Basics free

Aggregation vs. Composition, in Code

MediumProCoding problemJava · Python

Two ownership relationships, two different rules for who can touch the shared object:

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Enforcing Multiplicity: Team and Player

MediumProCoding problemJava · Python

Enforce two multiplicity rules from the lesson on a Team/Player association:

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice UML Class Diagram Basics free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime