Vasu Patel logoVasu Patel
← Objects First: OOP Fundamentals
All chapters in this track
  1. 01Chapter 1: Why Object Orientation Exists
  2. 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
  3. 03Chapter 3: The Four Pillars I: Encapsulation
  4. 04Chapter 4: The Four Pillars II: Abstraction
  5. 05Chapter 5: The Four Pillars III: Inheritance
  6. 06Chapter 6: The Four Pillars IV: Polymorphism
  7. 07Chapter 7: Composition vs. Inheritance
  8. 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
  9. 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
  10. 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
  11. 11Chapter 11: Basic UML for Communicating Design
  12. 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
  13. 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
  14. 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
  15. 15Chapter 15: Design Exercise: Parking Lot System
  16. 16Chapter 16: Design Exercise: Library Management System
  17. 17Chapter 17: Design Exercise: Elevator Controller
  18. 18Chapter 18: OOP in Interviews: What a Strong Answer Looks Like

Chapter 11 of 18

Chapter 11: Basic UML for Communicating Design

Why this matters

Design discussions fall apart when people use the same words to mean different relationships — one engineer's "has-a" is composition, another's is aggregation, and neither notices until a bug shows up around object lifetime. UML class diagrams give a precise, widely-recognized vocabulary for exactly this: what's inherited, what's held, and — critically — who owns what and for how long. You don't need the full UML specification; you need the handful of notations that show up constantly in design docs, whiteboards, and interviews.

The core concept

[BEGINNER] A UML class box has three sections: class name, fields (with types), and methods (with signatures and visibility markers + public, - private, # protected).

+-----------------------------+
|           Account           |
+-----------------------------+
| - balanceCents: number      |
| + id: string                |
+-----------------------------+
| + deposit(amount: number)   |
| + withdraw(amount: number)  |
| + getBalanceCents(): number |
+-----------------------------+

Relationships, precisely

[SENIOR INSIGHT] This is where UML earns its keep — the three relationships that look similar in casual conversation but mean very different things for object lifetime and coupling:

RelationshipArrowMeaningLifetimeCode shape
Inheritance ("is-a")▷——— (hollow triangle, solid line)Subclass extends superclassN/A — a type relationshipclass Savings extends Account
Composition ("owns-a", part-of)◆——— (filled diamond, solid line)The whole owns the part; the part cannot exist independentlyPart is destroyed when the whole isAn Order and its OrderLine items — line items have no meaning without the order
Aggregation ("has-a", loosely)◇——— (hollow diamond, solid line)The whole has the part, but the part can exist independentlyPart outlives the wholeA Team and its Players — a player still exists if the team is disbanded
Association (uses/references)——— (plain line)One class refers to or uses another, no ownership impliedIndependentA Driver associated with a Car they're currently driving

The distinction between composition and aggregation is the one people conflate most, and it's exactly the "has-a" concept from Chapter 7 needing one more level of precision: does the contained object's lifetime depend on the container?

Order ◆──── OrderLine        (composition: line items die with the order)
Team  ◇──── Player           (aggregation: players survive if the team disbands)
Driver ──── Car              (association: no ownership either way)

In code, this distinction usually shows up as: composition means the container creates and destroys the contained objects internally (they're never passed in from outside, never shared with another owner); aggregation means the contained objects are created independently and merely referenced.

// Composition — OrderLine instances are created and owned entirely by Order.
class OrderLine {
  constructor(public sku: string, public quantity: number) {}
}
class Order {
  private lines: OrderLine[] = [];
  addItem(sku: string, quantity: number): void {
    this.lines.push(new OrderLine(sku, quantity)); // created internally, owned exclusively
  }
}

// Aggregation — Player instances exist independently and are merely referenced.
class Player {
  constructor(public name: string) {}
}
class Team {
  private players: Player[] = [];
  addPlayer(player: Player): void {
    this.players.push(player); // passed in from outside — Team doesn't own its lifetime
  }
}
class OrderLine:
    def __init__(self, sku: str, quantity: int) -> None:
        self.sku = sku
        self.quantity = quantity

class Order:
    def __init__(self) -> None:
        self._lines: list[OrderLine] = []

    def add_item(self, sku: str, quantity: int) -> None:
        self._lines.append(OrderLine(sku, quantity))  # composition: created internally

class Team:
    def __init__(self) -> None:
        self._players: list["Player"] = []

    def add_player(self, player: "Player") -> None:
        self._players.append(player)  # aggregation: passed in, independently owned

A worked example diagram

[IN PRACTICE] Here's how these notations combine to describe a small system — a library (the domain used again in Chapter 16's full design exercise) — precisely enough that a reader could redraw it without seeing the original image:

+----------------+          +----------------+
|    Library     |  ◆────── |  LibraryBranch  |   (composition: a branch belongs to exactly
+----------------+  1    *  +----------------+    one Library and is destroyed with it)
                                    |
                                    | ◇  1    *
                                    v
                              +----------------+
                              |      Book       |   (aggregation: a book exists independently
                              +----------------+    of any one branch — it can transfer)
                                    ^
                                    | ▷ (inheritance)
                        +-----------+-----------+
                        |                       |
                +----------------+     +----------------+
                | PhysicalBook   |     |   EBook        |
                +----------------+     +----------------+

+----------------+          +----------------+
|    Member      |  ———────  |     Loan       |   (association: a member is linked to their
+----------------+          +----------------+    active loans, no ownership implied)

Multiplicities (1, *) on either end of a relationship line specify how many of each side can participate — Library 1 ◆── * LibraryBranch reads as "one Library owns zero-or-more Branches."

Common mistakes

  1. Using composition and aggregation interchangeably in a design doc. This isn't pedantry — it directly affects how deletion, serialization, and testing should behave. Deleting a Library should cascade-delete its LibraryBranch records (composition); it must never cascade-delete Book records that other branches still reference (aggregation).
  2. Drawing every relationship as a plain association because it's the easiest to draw, losing the lifetime information that actually mattered for the design decision at hand.
  3. Treating a UML diagram as a deliverable in itself rather than a communication tool. A diagram nobody checks against the actual code drifts out of date and becomes actively misleading — worse than no diagram.
  4. Skipping multiplicities. "A Team has Players" is ambiguous about whether a team can have zero players, exactly one, or unlimited — the multiplicity markers remove that ambiguity.

Checklist: using UML effectively

  1. Use inheritance arrows only for genuine is-a, substitutable relationships — same substitutability test as Chapters 5–6, just drawn instead of coded.
  2. Distinguish composition from aggregation deliberately by asking "does the contained object's lifetime depend on the container?" — this single question resolves the ambiguity every time.
  3. Include multiplicities on association ends when they matter to the design decision being communicated (e.g., "can a Book have zero branches" is a real question with a real answer).
  4. Keep diagrams at the level of the current discussion — a high-level design conversation doesn't need every private field shown; a detailed LLD review might.
  5. Treat a diagram as disposable documentation of a decision at a point in time, not a contract that must be kept eternally in sync with code — redraw it when it's needed for the next discussion, rather than maintaining it as living documentation unless the team has committed to that.

Discussion / interview questions

  1. "What's the difference between composition and aggregation, and why does the distinction matter in practice?" Strong answer: Composition: the contained object's lifetime is owned by the container (destroyed together); aggregation: the contained object exists independently. Matters for deletion cascades, serialization boundaries, and understanding what "owns" a piece of data.

  2. "Draw (or describe) a class diagram for an Order containing OrderLines, where OrderLines cannot exist without their Order." Strong answer: Uses a filled-diamond composition relationship from Order to OrderLine, correctly reasoning that OrderLines are created and destroyed by Order.

  3. "How would you represent inheritance versus interface implementation differently in UML?" Strong answer: Both use a hollow-triangle arrow toward the parent/interface; interface implementation is conventionally drawn with a dashed line (- - -▷) while class inheritance uses a solid line (——▷), and the target is labeled <<interface>> for an interface.

  4. "Why might a team decide not to keep UML diagrams continuously up to date with the codebase?" Strong answer: Diagrams are a communication tool for a specific design discussion, not always a maintained artifact; an out-of-date diagram that nobody checks against the code can actively mislead, so many teams treat them as disposable, generated fresh when needed rather than committed to staying in sync.

  5. "What does a multiplicity of 1 to * mean on a relationship between Library and LibraryBranch?" Strong answer: One Library can have zero or more LibraryBranches (reading the * end), while each LibraryBranch belongs to exactly one Library (reading the 1 end) — correctly reads both ends of the relationship, not just one.

Chapter summary / cheat sheet

  • A UML class box: name, fields (with visibility markers), methods.
  • Inheritance (▷, hollow triangle): is-a, substitutable. Composition (◆, filled diamond): owns-a, dependent lifetime. Aggregation (◇, hollow diamond): has-a, independent lifetime. Association (plain line): uses/references, no ownership.
  • The composition-vs-aggregation question that resolves ambiguity: "does the contained object's lifetime depend on the container?"
  • Multiplicities (1, *, etc.) on relationship ends remove ambiguity about how many participants are allowed on each side.
  • Treat diagrams as communication tools for a specific discussion, not necessarily as artifacts requiring permanent synchronization with the code.