Back to the path
Level 1 intermediate 23 min #uml#class-diagrams#communication#interview#modeling

UML Class Diagrams: Reading, Drawing, and Translating to Code

The class diagram is the lingua franca of an LLD interview. Learn the exact box notation, visibility markers, static/abstract conventions, and relationship arrows — then translate diagrams to code and back, fluently, on a whiteboard.

Why this is the most important communication skill in LLD

You can have the cleanest design in your head and still fail an LLD round if you can’t show it. The interviewer needs to see your classes, their responsibilities, and how they connect — fast, on a whiteboard or a shared doc, often in ASCII. The UML class diagram is the agreed-upon notation for exactly this.

You do not need formal, tool-perfect UML. Interviewers want practical UML: enough rigor that any engineer reads your boxes and arrows the same way you meant them. This lesson teaches the subset that actually shows up: the class box, visibility, static/abstract notation, and the relationship arrows — plus the bidirectional skill that matters most: turning a diagram into code and code back into a diagram.

In an interview, say: “Let me sketch the class diagram before I write code.” This signals seniority. You’re designing structure first, not coding into a corner.

The class box: name, attributes, methods

A class is a rectangle split into three compartments, top to bottom:

+---------------------------+
|         Account           |   <- 1. Class name (centered, bold)
+---------------------------+
| - balance: double         |   <- 2. Attributes (fields)
| - owner: String           |
+---------------------------+
| + deposit(amount): void   |   <- 3. Operations (methods)
| + withdraw(amount): bool  |
| - validate(amt): bool     |
+---------------------------+
  • Name compartment: just the class name. Abstract class names are italic (in ASCII, mark them {abstract} since you can’t italicize).
  • Attributes: visibility name: Type. You may add = default for initial values and [*] / [0..1] for multiplicity.
  • Operations: visibility name(param: Type): ReturnType.

The signature format is precise but you don’t have to be exhaustive. Show the attributes and methods that matter to the design conversation. Omitting a getter is fine; omitting the field that makes the whole pattern work is not.

Tip

Don’t draw every getter/setter, constructor, or toString. Diagrams are for communication, not codegen. Show the fields and methods that reveal responsibilities and collaborations — the rest is noise that wastes your limited whiteboard time.

Visibility: the four symbols

Each member is prefixed with a visibility marker. These map directly to access modifiers in code:

SymbolNameJava/C++/TS meaningPython convention
+publicpublicplain name (balance)
-privateprivate__balance (name-mangled)
#protectedprotected_balance (single underscore)
~packagepackage-private (Java default)(module scope — rarely shown)

The ~ (package) is rare in interviews; +, -, and # cover almost everything. The mental rule: fields are usually - (encapsulation), most behavior is +, and # shows up only when a subclass genuinely needs access.

Common pitfall

A common slip is marking fields +. If you draw + balance: double, you’ve signaled that any code can mutate the balance directly — which contradicts the whole point of an Account. Default fields to - and expose behavior through + methods. An interviewer will notice public mutable state.

Static and abstract notation

Two modifiers change how a member or class is rendered:

  • Static members are underlined. In ASCII you can’t underline, so write {static} after the member, or note it inline. Static = belongs to the class, not an instance.
  • Abstract classes and methods are italic. In ASCII, append {abstract}. An abstract class can’t be instantiated; an abstract method has no body and must be overridden.
  • Interfaces get a «interface» stereotype above the name (use <<interface>> in ASCII).
+--------------------------------+
|       <<interface>>            |
|        PaymentGateway          |
+--------------------------------+
| + charge(amount): Receipt      |   (all methods implicitly public + abstract)
+--------------------------------+

+--------------------------------+
|   Shape {abstract}             |
+--------------------------------+
| # name: String                 |
+--------------------------------+
| + area(): double {abstract}    |   <- subclasses MUST implement
| + describe(): String           |   <- concrete, inherited as-is
| + instances(): int {static}    |   <- class-level counter accessor
+--------------------------------+

In an interview, when you draw {abstract} on a method, say it out loud: “area is abstract, so every concrete Shape implements its own.” This pre-empts the “what does the base class do?” question.

The relationship arrows (the part people get wrong)

Boxes are easy; the lines between them carry the design intent. Get these six right and you can express almost any LLD. Note the deliberate visual distinction between inheritance (solid line) and realization (dashed line) — they share a triangle head but the line style is the whole point:

Association   A -------> B      A holds a reference to B (uses/knows)
Aggregation   A <>------ B      A has-a B, but B can outlive A (shared)
Composition   A *------- B      A owns B; B dies with A (exclusive)
Inheritance   A ------|> B      A is-a B (extends)        -- SOLID line, triangle
Realization   A - - -|> B      A implements interface B  -- DASHED line, triangle
Dependency    A - - - -> B      A transiently uses B (param/local/return)

The single difference between inheritance and realization in UML is the line texture: a solid line for extends, a dashed line for implements. In ASCII, draw realization with dashes (- - -|>) so a reader can never confuse the two.

The two that get confused most are aggregation vs composition — both are “has-a”, but the diamond’s fill encodes lifecycle ownership:

RelationshipSymbolLifecycleExample
Aggregation (hollow diamond)<>----Part exists independentlyTeam o— Player (a player survives the team disbanding)
Composition (filled diamond)*----Part dies with wholeHouse ◆— Room (a room has no meaning without its house)

Multiplicity is written at the line ends: 1, 0..1, 1..*, *. Read Order "1" *---- "1..*" LineItem as “one Order is composed of one-or-more LineItems.”

+---------+  1        1..*  +-------------+
|  Order  | *----------------|  LineItem   |
+---------+                  +-------------+
   composition: deleting the Order deletes its LineItems
Key idea

The diamond sits on the whole (the owner), pointing away from the part. Filled diamond = composition (exclusive ownership, cascade delete). Hollow diamond = aggregation (shared, independent lifecycle). Inheritance (solid line) and realization (dashed line) triangles point to the parent/interface.

Translating a diagram into code

This is the skill that gets tested implicitly: you draw, then you implement, and the code must match the picture. Here’s a small diagram and its faithful translation.

        <<interface>>
        Notifier
        + send(msg: String): void
              /\
              :  (realization, DASHED triangle)
       +------+------+
       :             :
+--------------+  +---------------+
| EmailNotifier|  |  SmsNotifier  |
+--------------+  +---------------+
| + send(msg)  |  | + send(msg)   |
+--------------+  +---------------+

+----------------------------+  1        1  +------------+
|       AlertService         |<>----------- |  Notifier  |
+----------------------------+  aggregation +------------+
| - notifier: Notifier       |  (injected, swappable)
+----------------------------+
| + raise(msg: String): void |
+----------------------------+

Reading rules applied:

  • Notifier is an interface → no fields, abstract send.
  • EmailNotifier/SmsNotifier realize it (dashed triangle) → they implement and override send.
  • AlertService has a - notifier: Notifier field with a hollow diamond → it holds an injected, swappable Notifier, programmed to the interface, not a concrete type. Because the concrete notifier is passed in via the constructor (dependency injection), it can be created elsewhere, shared, or substituted in a test — so this is aggregation, not composition.
Watch out

This is the exact judgment interviewers probe: a constructor-injected dependency is aggregation (hollow diamond), even if the holder later owns it. Composition (filled diamond) is reserved for objects the class constructs internally and never exposes. If AlertService did this.notifier = new EmailNotifier() inside itself, then it would be composition. Injection is what makes it swappable — and swappable is the whole point of programming to an interface.

interface Notifier {
send(msg: string): void;
}

class EmailNotifier implements Notifier {
send(msg: string): void { /* send email */ }
}

class SmsNotifier implements Notifier {
send(msg: string): void { /* send sms */ }
}

class AlertService {
// '-' visibility -> private; typed to the INTERFACE (the arrow).
// Injected -> aggregation (hollow diamond): the notifier can be
// shared/substituted, so AlertService doesn't exclusively own it.
private notifier: Notifier;
constructor(notifier: Notifier) { this.notifier = notifier; }

raise(msg: string): void {
  this.notifier.send(msg);
}
}

Notice how each notation maps to a language construct: <<interface>>interface/ABC/pure-virtual struct; the realization triangle → implements/inheritance/override; the - field → private; the hollow diamond → an injected, swappable member.

Key idea

Reconciling ownership with injection. The C++ tab uses std::unique_ptr<Notifier>, which encodes exclusive ownership — destroying the AlertService destroys the notifier. So is it composition after all? This is the nuance: the diamond’s fill reflects lifecycle ownership (does the part die with the whole?), while the hollow-vs-filled heuristic most people use (“injected → hollow, internally-constructed → filled”) is a proxy for it. A unique_ptr moved in through the constructor is injected ownership — both true at once. In TS/Python the reference is freely shareable, so aggregation is the clean reading; in C++ a unique_ptr makes a composition reading defensible. The honest interview answer: “It’s injected, so I drew aggregation — but because C++ moves a unique_ptr in, the lot owns it exclusively, which leans toward composition. The point of injection here is substitutability, so I’m treating it as aggregation.” Saying this out loud shows you understand both axes.

Translating code back into a diagram

The reverse direction is just as common — you’ve written some classes and the interviewer asks “draw the relationships.” The recipe:

  1. One box per class/interface. Mark interfaces and abstract classes with the stereotype/{abstract}.
  2. Fields → attributes, mapping access modifier to + - #.
  3. Methods → operations, same visibility mapping; mark static/abstract.
  4. Field whose type is another class → draw an association/aggregation/composition line to it. Use a filled diamond if this class constructs internally and owns that object (cascade delete, not exposed); hollow if it’s injected/shared.
  5. extends solid inheritance triangle to the parent. implements dashed realization triangle to the interface.
  6. A type used only as a parameter, local, or return value (not stored) → dashed dependency arrow.

The hardest judgment is step 4: is a field association, aggregation, or composition? Ask “if this object is destroyed, should the field’s object be destroyed too — and did I build it myself or receive it?”

  • Constructed internally and not exposed → composition (*----).
  • Passed in via constructor / setter, shared elsewhere → aggregation (<>----).
  • Just a reference you read but don’t own → plain association.
Watch out

Don’t over-annotate. In most interviews a plain association arrow (A ----> B with a label and multiplicity) communicates enough. Reach for the diamonds only when lifecycle ownership is part of the design point — e.g. justifying a cascade delete, or explaining why a part can’t be shared. Spending three minutes debating hollow vs filled diamonds on a trivial field is a yellow flag.

A worked end-to-end example

Here is a richer diagram pulling together every notation — a parking-lot fragment you might draw in the first two minutes of that classic problem. Note how each relationship line is drawn to a distinct target, with the diamond seated on the ParkingLot (the whole/owner) side.

              <<interface>>
              FeeStrategy
              + fee(ticket: Ticket): double
                     /\
                     :  (realization, DASHED)
              +------+--------+
              :               :
   +------------------+  +-----------------+
   |  FlatRateFee     |  |  HourlyFee      |
   +------------------+  +-----------------+
   | + fee(ticket)    |  | - rate: double  |
   +------------------+  | + fee(ticket)   |
                         +-----------------+
                                  ^
                                  : (hollow diamond seated on ParkingLot)
                                  :
+-----------------------------+   :          1..* +-------------+
|        ParkingLot           |*----------------- |    Spot     |  (composition,
+-----------------------------+   filled diamond  +-------------+   filled on lot)
| - spots: List<Spot>         |  on lot side      | # id: int   |
| - strategy: FeeStrategy     |<>--- aggregation -+ - taken: bool|
+-----------------------------+   (to FeeStrategy)+-------------+
| + park(v: Vehicle): Ticket  |
| + count(): int {static}     |
+-----------------------------+
          :
          : dependency (Vehicle is a param of park(), never stored)
          v - - - - - - - - - - - +
                                  v
                         +----------------+
                         |    Vehicle     |
                         | # plate: String|
                         +----------------+

To be unambiguous about which line is which (ASCII diamonds are cramped), here are the three ParkingLot relationships drawn one per line, with the diamond always on the ParkingLot (whole) side:

ParkingLot *----------------> Spot          composition  (filled diamond on ParkingLot)
ParkingLot <>---------------> FeeStrategy    aggregation  (hollow diamond on ParkingLot)
ParkingLot - - - - - - - - -> Vehicle        dependency   (param of park(), not stored)

What a reader extracts in seconds:

  • FeeStrategy is an interface with two implementations (Strategy pattern — the design intent is visible from the dashed realization triangles alone).
  • ParkingLot composes its Spots (filled diamond, 1..*): the lot constructs and owns them, so destroy the lot and the spots go too.
  • ParkingLot aggregates a FeeStrategy (hollow diamond): the strategy is injected, swappable, and can be shared across lots — never constructed inside ParkingLot.
  • ParkingLot.park depends on Vehicle (dashed arrow) — it’s only a parameter, never stored.
  • count() is {static} — a lot-wide tally.

In an interview, narrate the arrows as you draw: “The lot builds and owns its spots — composition, filled diamond. But the fee strategy is injected so we can swap pricing without touching the lot — hollow diamond, aggregation. And Vehicle is just a parameter to park(), so it’s a dashed dependency, not a stored field.” You’re demonstrating that every glyph reflects a deliberate decision.

Quick reference cheat sheet

You want to expressNotationIn code
public method+ m()public
private field- fprivate / __f
protected member# mprotected / _m
static memberunderline / {static}static / @staticmethod
abstract class/methoditalic / {abstract}abstract / ABC / pure virtual
interface<<interface>>interface / ABC
is-a (extends)`---->` solid line, triangle
implements`- - ->` dashed line, triangle
has-a, owned (built internally)*---- filled diamondconstructed + owned field
has-a, shared (injected)<>---- hollow diamondinjected field
uses (knows)----> arrowstored reference
transient use- - -> dashed arrowparam/local/return only

Memorize this table. The inheritance-vs-realization distinction (solid vs dashed line) and the aggregation-vs-composition distinction (hollow vs filled diamond, injected vs internally-constructed) are the two most-tested notation facts. Being able to draw a correct, readable class diagram in under five minutes — and defend each arrow — is a concrete, repeatable senior signal.

Assessment

Pass mark: 70% on the concept questions unlocks the next lesson.

1. In a UML class box, what do the three stacked compartments represent, top to bottom?

2. An `AlertService` receives a `Notifier` through its constructor (`constructor(notifier: Notifier)`) and stores it in a private field. Which relationship best describes `AlertService` -> `Notifier`?

3. In ASCII UML, what is the ONLY notational difference between an inheritance (`extends`) arrow and a realization (`implements`) arrow?

4. Which of the following correctly justify drawing a FILLED diamond (composition) rather than a hollow diamond (aggregation)? Select all that apply. (select all)

Design problem 5

You are given this code for a music app. Draw (in ASCII UML) the class diagram showing every class/interface, member visibility, and each relationship with the CORRECT arrow/diamond, then state the multiplicities. ``` interface AudioSource { play(): void; } class LocalFile implements AudioSource { play() {} } class StreamUrl implements AudioSource { play() {} } class Track { private title: string; private source: AudioSource; // passed into constructor constructor(title: string, source: AudioSource) { ... } } class Playlist { private tracks: Track[] = []; // Playlist creates/owns Tracks; cleared when playlist deleted addTrack(t: Track): void { this.tracks.push(t); } totalPlays(user: User): number { ... } // User only used as a parameter } ``` Explain WHY each relationship is the arrow you chose.