Skip to content

Low Level DesignFoundations

UML for interviews

The UML that low level design interviews use, from class boxes and six arrows to sequence and state diagrams, with one checkout model in five languages.

Foundation25 min read

UML 2.5 defines fourteen kinds of diagram, in a specification several hundred pages long. A low level design round uses a tiny slice of it. The diagram there is a sketch. It settles three things before code depends on them: which classes exist, who owns whom, and where the extension points are. It takes about two minutes per stage. And it is wrong in some detail almost every time. That’s fine, because it is cheap to change.

Five symbols carry nearly every interview diagram:

  • The class box.
  • The plain arrow, for “knows about”.
  • The filled diamond, for “owns”.
  • The hollow triangle, for “is a” (a solid line for extends, a dashed one for implements).
  • Multiplicity numbers on the ends.

Two more are worth reading when someone else draws them: the dashed arrow for a short-lived dependency, and the hollow diamond for aggregation. Each symbol becomes a specific shape in code: a field, a constructor parameter, an extends, or a part no one else can reach. This page builds one checkout model, from bare boxes to a sequence diagram, with the code each arrow turns into.

The six arrows on one card
Association LineItem Product * 1 a field holding an object made elsewhere Composition Order LineItem 1 1..* creates its parts; they die with it Inheritance PercentOff Discount extends a class: fields and code Realization FakeCardGateway PaymentGateway implements an interface: only the contract Dependency CheckoutService Order a local, parameter or return, never stored Aggregation (read it, rarely draw it) Catalog Product shared parts: same code as association
Scroll or pinch to zoom, drag to pan
The classes come from the checkout example on this page. The arrow points at the class that is known, used or inherited from. The diamond sits on the whole.

A class is a box with three compartments: the name, the attributes, the operations. Marks in front of a member give its visibility. A few more say whether it is static or abstract. A stereotype in guillemets above the name says what kind of thing the box is: <<interface>>, <<abstract>>, <<enumeration>>, or one the team invents, like <<value>> for an immutable value object.

Class boxes, with every marking
Order - id: int - nextId: int - lines: List<LineItem> ~ status: OrderStatus + addLine(product, qty) + totalCents(): int «abstract» Discount # label: String + amountOffCents(subtotal): int + describe(): String «interface» PaymentGateway + charge(orderId, cents): bool «enumeration» OrderStatus CREATED PAID CANCELLED SHIPPED
Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

Static members are underlined ($ in Mermaid) and abstract ones are in italics (* in Mermaid). Each language spells the markings its own way, and two of them have no direct equivalent in some languages:

Marking Means Java Python C# Go TypeScript
+ public public plain name public Capitalized name default, public
- private private _name (convention), __name private lowercase name (package-wide) private or #x
# protected protected _name (convention) protected none protected
~ package no modifier none internal (assembly) lowercase name not exported
underlined, $ static, one per class static class attribute, @staticmethod static package-level var or func static
italics, * abstract abstract ABC + @abstractmethod abstract none: use an interface abstract
<<interface>> a contract with no state interface Protocol or ABC interface interface, satisfied implicitly interface

What goes in the box is a choice. Put in the fields that hold state the use cases depend on, and the methods that carry the use cases. Leave out getters and setters, constructors, equals, toString, and types that are obvious from the name. A box with three members that matter reads in a second. A box with fifteen hides the three.

There are six relationships between classes, and they split into three families: knows about (association, aggregation, composition), is a (inheritance, realization) and uses briefly (dependency).

Relationship Symbol Mermaid Means In code
Association solid line, open arrowhead LineItem --> Product holds a reference and can reach it a field, often set through the constructor
Aggregation hollow diamond on the whole Catalog o-- Product a whole made of parts that live on without it a field holding a collection of objects made elsewhere
Composition filled diamond on the whole Order *-- LineItem owns its parts: one owner, same lifetime parts created inside the owner, never shared or handed out
Inheritance solid line, hollow triangle on the parent Discount <|-- PercentOff is a, inheriting fields and code extends, :, a Python base class
Realization dashed line, hollow triangle on the interface PaymentGateway <|.. FakeCardGateway fulfills a contract implements, : on an interface, or Go’s implicit match
Dependency dashed line, open arrowhead CheckoutService ..> Order uses it briefly, and changes when it changes a parameter, a local variable, a return type, a static call

Three pairs are where the confusion lives:

  • Association or dependency? Ask whether the class keeps the other one in a field. Kept means association, a solid line. Seen only during a call means dependency, a dashed line. Ask the same question about constructor injection. A collaborator passed to the constructor, and stored in a field, is an association, not a dependency. That’s true even though the technique is called dependency injection.
  • Composition or aggregation? Both say “whole and parts”. Composition adds two promises. A part belongs to one whole at a time. And it goes when the whole goes. Aggregation promises neither. That’s why the UML 2.5 specification leaves its exact meaning to the modeler. And it’s why Martin Fowler’s UML Distilled recommends not using it. In code, aggregation and plain association are the same: a field of references.
  • Inheritance or realization? Inheritance takes fields and code from a class. Realization takes only a promise from an interface. The solid line versus the dashed line is the whole difference. And it matters. A subclass breaks when its parent’s code changes. An implementer breaks only when the contract does.

One rule settles every direction question: the arrowhead or triangle points at the class that is known, used or inherited from, and the diamond sits on the owner. PercentOff points at Discount, because the child knows its parent, and the parent knows nothing of its children.

A number at each end of a line counts the objects at that end, for one object at the other end. So it sits next to the class it counts. Take Order "1" *-- "1..*" LineItem. The 1..* beside LineItem says one order has one or more lines. The 1 beside Order says each line belongs to exactly one order.

Multiplicity Means Becomes in code
1 exactly one a field set in the constructor, never null
0..1 optional a nullable field, Optional, None, a nil pointer or interface
* or 0..* any number a list, set or map, empty to start
1..* at least one a collection plus a check that refuses to create or finish without one
2..4 a fixed range a collection plus a range check, like players at a table

Navigability is the arrowhead on an association. It shows which side can reach the other. LineItem --> Product means a line has a field pointing at its product. A product has no field listing its lines. A line with no arrowheads usually means “not decided yet”. A two-way association means two fields that have to be kept in step, and that’s a source of bugs. Add the second direction only when a use case needs to walk it often. Otherwise, answer that question with a lookup.

The class diagrams on this site are standard UML drawn with Mermaid, with one addition: color marks the role a class plays. Color is not part of UML, so on paper you would write the role as a stereotype instead. The key sits under each diagram.

Color on this site Role The UML you would write
Purple Facade: the entry point, coordinates a plain class, sometimes <<service>>
Blue Entity: has identity, changes over time a plain class, sometimes <<entity>>
Teal Value object: immutable, equal by its fields <<value>>
Gray Enum <<enumeration>>
Amber, dashed border Extension point: an interface or abstract class <<interface>> or <<abstract>>, italic name
Orange Implementation of an extension point the class at the tail of a realization or inheritance arrow
Any color, thick border New in this step of a staged design nothing: it is a teaching aid

The arrows follow Mermaid’s notation, which is UML’s. -->, o--, *--, <|--, <|.. and ..> are the six relationships in the table above. Quoted numbers next to a class name are multiplicities.

The requirements are short. A customer checks out a cart of products from the store’s catalog. A discount may apply. The order is charged through a payment gateway. If the charge is approved, the order is paid, and a receipt email goes out. If it is declined, the order is cancelled. Paid orders are later shipped. The model grows in four steps, the way it would in a round. First boxes, then relationships, then numbers, then one use case as a sequence.

Start from the nouns that carry rules: an order with lines, products in a catalog, discounts, a payment gateway, and a service that runs checkout. Draw them as boxes with only the members the use cases need, and no lines yet.

Stage 1: boxes only
CheckoutService + placeOrder(cart, discount): Order Order - id - lines + addLine(product, qty) + totalCents(): int «value» LineItem - qty - unitPriceCents + subtotalCents(): int Product + sku + name + priceCents Catalog + find(sku): Product «abstract» Discount + amountOffCents(subtotal): int «interface» PaymentGateway + charge(orderId, cents): bool
Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
  • New in this step
No lines yet. Each box holds only the members a use case needs.

Three decisions are already visible. LineItem copies the unit price when it is made, so a price change tomorrow does not rewrite today’s order. Discount is abstract because there is no plain discount, only kinds of discount. PaymentGateway is an interface because checkout must not care which card network is behind it. The customer, the warehouse and the email system are left out: no use case needs them as classes yet.

Interviewerminute 8

Before you draw any lines, which classes do you need, and what is on each?

What they are testing

Whether the boxes come from the requirements and carry only the members that do work. A strong answer names the price copied onto the line and why. The red flag is a box per noun in the prompt with every getter listed.

Now connect them. For each pair, ask the questions from the table. Does one keep the other in a field? Did it create it? Can the part outlive it? Is one a kind of the other?

Stage 2: every arrow is a decision about code

creates

CheckoutService

+placeOrder(cart, discount) : Order

Catalog

+add(Product)

+find(sku) : Product

Product

+sku

+name

+priceCents

Order

-id

-lines

+addLine(product, qty)

+apply(Discount)

+totalCents() : int

«value»

LineItem

-qty

-unitPriceCents

+subtotalCents() : int

«abstract»

Discount

#label

+amountOffCents(subtotal) : int

+describe() : String

PercentOff

-percent

+amountOffCents(subtotal) : int

«interface»

PaymentGateway

+charge(orderId, cents) : bool

FakeCardGateway

+charge(orderId, cents) : bool

creates

CheckoutService

+placeOrder(cart, discount) : Order

Catalog

+add(Product)

+find(sku) : Product

Product

+sku

+name

+priceCents

Order

-id

-lines

+addLine(product, qty)

+apply(Discount)

+totalCents() : int

«value»

LineItem

-qty

-unitPriceCents

+subtotalCents() : int

«abstract»

Discount

#label

+amountOffCents(subtotal) : int

+describe() : String

PercentOff

-percent

+amountOffCents(subtotal) : int

«interface»

PaymentGateway

+charge(orderId, cents) : bool

FakeCardGateway

+charge(orderId, cents) : bool

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

Each arrow, and the reason for it:

  • Order *-- LineItem, composition. Order creates every line inside addLine, never accepts one from outside, and never hands out its list. A line belongs to one order and has no meaning without it.
  • Catalog o-- Product, aggregation. The catalog holds products it did not create. They come from the database, and the same product can sit in a seasonal catalog too. A plain association arrow would say the same thing in code, and many teams would draw that instead.
  • LineItem --> Product, association. A line keeps a reference to its product. The arrowhead says the product does not know its lines.
  • CheckoutService --> Catalog and --> PaymentGateway, associations. Both arrive through the constructor and stay in fields for the service’s whole life. The service did not create them and does not own them, so no diamond.
  • CheckoutService ..> Order, dependency. The service builds an order and returns it without keeping it. A label like creates says why the dashed line is there.
  • Discount <|-- PercentOff and PaymentGateway <|.. FakeCardGateway. The first extends an abstract class, and inherits its label and describe. The second only promises a charge method. That’s a solid triangle line versus a dashed one.

Here is the model in code. The comments name the arrow each part implements:

Main.javacheckout_service.pyOrder.cscheckout_service.gocheckout-service.ts
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// An entity the store sells. It knows nothing about orders or catalogs: every arrow that
// touches Product points at it, so Product never changes when they do.
class Product {
private final String sku;
private final String name;
private int priceCents;
Product(String sku, String name, int priceCents) {
this.sku = sku;
this.name = name;
this.priceCents = priceCents;
}
String sku() { return sku; }
String name() { return name; }
int priceCents() { return priceCents; }
void changePrice(int cents) { priceCents = cents; }
}
// Catalog o-- Product (aggregation). Products are made elsewhere and handed in, so the
// catalog holds references it did not create. In code this is the same as an association:
// a field. The hollow diamond only adds "these are its members, but they live on without it".
class Catalog {
private final Map<String, Product> bySku = new HashMap<>();
void add(Product p) { bySku.put(p.sku(), p); }
Product find(String sku) {
Product p = bySku.get(sku);
if (p == null) throw new IllegalArgumentException("unknown sku " + sku);
return p;
}
}
// LineItem --> Product (association, one way). The line holds a reference to its product;
// the product has no list of lines. The price is copied when the line is made, so a later
// price change does not rewrite an order that was already placed.
final class LineItem {
private final Product product;
private final int qty;
private final int unitPriceCents;
// Package-private constructor: code outside the package cannot make a line. Inside it,
// only Order calls this, by convention.
LineItem(Product product, int qty) {
this.product = product;
this.qty = qty;
this.unitPriceCents = product.priceCents();
}
Product product() { return product; }
int subtotalCents() { return unitPriceCents * qty; }
}
// Discount <|-- PercentOff (generalization). Abstract, so there is no plain "discount":
// in UML the name is in italics and the abstract method is marked the same way.
abstract class Discount {
protected final String label; // # protected: subclasses build on it, callers cannot see it
protected Discount(String label) { this.label = label; }
public abstract int amountOffCents(int subtotalCents);
public String describe() { return label; }
}
class PercentOff extends Discount {
private final int percent;
PercentOff(int percent) {
super(percent + "% off");
this.percent = percent;
}
@Override
public int amountOffCents(int subtotalCents) { return subtotalCents * percent / 100; }
}
// Order *-- LineItem (composition). Order creates every line inside addLine, never
// accepts a LineItem from outside and never hands out its own list. Java cannot enforce
// "dies with the whole": the garbage collector frees the lines once nothing refers to them,
// and a caller could keep one it read from lines(). So composition here is a convention the
// class keeps, and LineItem being immutable makes a kept reference harmless.
class Order {
private static int nextId = 1; // static: underlined in UML, one counter for the class
private final int id;
private final List<LineItem> lines = new ArrayList<>();
private Discount discount; // Order --> Discount, and may be null
Order() { id = nextId++; }
int id() { return id; }
void addLine(Product product, int qty) { lines.add(new LineItem(product, qty)); }
void apply(Discount d) { discount = d; }
// A read-only copy: callers can look at the parts but cannot change the order's list.
List<LineItem> lines() { return List.copyOf(lines); }
int totalCents() {
int subtotal = lines.stream().mapToInt(LineItem::subtotalCents).sum();
return discount == null ? subtotal : subtotal - discount.amountOffCents(subtotal);
}
}
// <<interface>>: the dashed box on this site. Checkout depends on this, never on a card network.
interface PaymentGateway {
boolean charge(int orderId, int cents);
}
// FakeCardGateway ..|> PaymentGateway (realization): the dashed line with a hollow triangle.
class FakeCardGateway implements PaymentGateway {
private final int limitCents;
FakeCardGateway(int limitCents) { this.limitCents = limitCents; }
public boolean charge(int orderId, int cents) { return cents <= limitCents; }
}
// CheckoutService --> Catalog and --> PaymentGateway (associations). Both arrive through the
// constructor: built outside, passed in, kept in fields. The service uses them for its whole
// life but does not own them, so the arrow is plain, not a diamond.
// CheckoutService ..> Order (dependency, "creates"): it builds an order and hands it back.
class CheckoutService {
private final Catalog catalog;
private final PaymentGateway gateway;
CheckoutService(Catalog catalog, PaymentGateway gateway) {
this.catalog = catalog;
this.gateway = gateway;
}
Order placeOrder(Map<String, Integer> cart, Discount discount) {
Order order = new Order();
cart.forEach((sku, qty) -> order.addLine(catalog.find(sku), qty));
if (discount != null) order.apply(discount);
if (!gateway.charge(order.id(), order.totalCents()))
throw new IllegalStateException("card declined");
return order; // not stored in a field: that is why the arrow is dashed
}
}
class Main {
public static void main(String[] args) {
Product mug = new Product("MUG-1", "Mug", 1200);
Product tea = new Product("TEA-9", "Green tea", 800);
Catalog catalog = new Catalog();
catalog.add(mug);
catalog.add(tea);
// main wires the associations: the service gets what it uses, it does not build it.
CheckoutService checkout = new CheckoutService(catalog, new FakeCardGateway(5_000));
Map<String, Integer> cart = new java.util.LinkedHashMap<>();
cart.put("MUG-1", 2);
cart.put("TEA-9", 1);
Order order = checkout.placeOrder(cart, new PercentOff(10));
System.out.println("order " + order.id() + ": " + order.totalCents()); // order 1: 2880
// Association: the line and the catalog point at the same Product object.
System.out.println(order.lines().get(0).product() == mug); // true
// The copied price keeps the placed order stable when the product changes.
mug.changePrice(1500);
System.out.println("after price change: " + order.totalCents()); // 2880
// Aggregation: drop the order and the product is still in the catalog.
order = null;
System.out.println(catalog.find("MUG-1").name()); // Mug
}
}
  • In these five languages, composition is a rule the owner keeps. All five are garbage collected. So no destructor shows “dies with the whole”, and nothing stops a caller from holding on to a line it read. The class keeps the promise by how it is written. Lines are built only inside Order, and stored privately. They’re never accepted from outside, and shown only as a read-only list (Go hands out copies). LineItem is immutable, so a reference someone kept cannot change the order. C++ and Rust can enforce the lifetime. These languages cannot.
  • Aggregation and association are the same code. Catalog and LineItem both hold references to products made elsewhere. The hollow diamond adds intent, not structure.
  • Constructor injection draws as a plain arrow. CheckoutService receives its catalog and gateway from outside and keeps them. The wiring code in main creates them, which is exactly why the service does not own them.
  • Go spells “is a” differently, and the diagram does not change. Go has no subclassing. So Discount becomes an interface, and the shared Describe comes from an embedded struct. Embedding is composition with forwarding. A PercentOff cannot be passed where a baseDiscount is expected. Realization needs no keyword at all in Go, Python or TypeScript. The method set is enough.
  • Some decisions don’t show in any arrow. LineItem points at Product, and also copies its price. So changing the product’s price in main leaves the placed order at 2880 cents. The diagram records that only as the unitPriceCents field. A short note next to the arrow says it better than a new symbol would.
Interviewerminute 12

Why is Order to LineItem a filled diamond, but Catalog to Product hollow?

What they are testing

Whether you tie the symbols to lifetime and exclusive ownership, then to code. A good answer: “Order creates its lines, and nobody else can hold one. Products are created elsewhere, and shared between catalogs and orders.” The red flag is “composition is a stronger has-a”, with nothing about who creates the part or what happens when the whole goes.

Pick each arrow below, to see its symbol and the code it becomes in each language. Then delete the object on the left, and watch what happens to the one on the right. The quiz underneath gives a plain description, and asks which arrow fits.

Pick an arrow

Association (solid line, open arrowhead) LineItem holds a reference to a Product and can reach it. The arrowhead says which way: Product has no list of lines.

MermaidLineItem "*" --> "1" Product

class LineItem {
    // A field holding an object made somewhere else.
    private final Product product;

    LineItem(Product product, int qty) {
        this.product = product;
    }
}
Say this out loud

Order creates its line items and never hands them out, so that’s composition. The catalog only holds products that live elsewhere, so it gets a hollow diamond, and in code that’s just a map of references.

Numbers turn a sketch into something that constrains the code. Add them where the answer is not obvious or where a rule hides in it. The service and gateway arrows are unchanged and left out here.

Stage 3: how many, and which way

*

*

1

1..*

*

1

*

0..1

Catalog

+find(sku) : Product

Product

+sku

+priceCents

Order

-id

+addLine(product, qty)

+totalCents() : int

«value»

LineItem

-qty

-unitPriceCents

«abstract»

Discount

+amountOffCents(subtotal) : int

*

*

1

1..*

*

1

*

0..1

Catalog

+find(sku) : Product

Product

+sku

+priceCents

Order

-id

+addLine(product, qty)

+totalCents() : int

«value»

LineItem

-qty

-unitPriceCents

«abstract»

Discount

+amountOffCents(subtotal) : int

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

Each number is a rule the code has to keep:

End Rule Where the code keeps it
Order "1" *-- ... a line belongs to exactly one order only Order creates lines, and never accepts one
... *-- "1..*" LineItem an order has at least one line placeOrder refuses an empty cart
LineItem "*" --> "1" Product every line has a product, and a product is on many lines a non-null field set in the constructor
Order "*" --> "0..1" Discount at most one discount per order a nullable field, not a list
Catalog "*" o-- "*" Product a product can be in several catalogs a map of references, with no back-pointer on Product

The 1 at the whole end of a composition is not a coincidence. A part with two owners would not know whose lifetime to share. So composition always has 1 or 0..1 there. If the honest answer is “a line item can be on several orders”, it is not composition.

Navigability is a cost decision. Every arrowhead above points one way. “Which orders contain this product?” is a real question for the warehouse team. But adding Product --> Order would mean every new order edits a product, and the two lists can drift apart. A query over orders answers it, without a second field to keep in step.

Stage 4: one use case as a sequence diagram

Section titled “Stage 4: one use case as a sequence diagram”

A class diagram shows structure. It cannot show order: that the price is computed before the charge, or what happens when the charge is declined. A sequence diagram shows one use case over time. It earns its place when a flow crosses three or more objects, when the order of calls is the point, or when a failure branch changes the outcome. Checkout has all three.

The notation. Everything but par appears in the diagram below:

Element Drawn as Means
Lifeline a box at the top with a dashed line down one object, or the actor, over time
Synchronous message solid line, filled arrowhead a call; the caller waits
Return dashed line, arrowhead the reply to a call; often left out when obvious
Asynchronous message solid line, open arrowhead send and move on: a queue, an event, a new thread
Create a dashed arrow to a lifeline that starts partway down (Mermaid draws a plain message) new: the object did not exist before this point
loop, alt, opt frames a labeled box around messages, guard in [ ] repeat, choose one branch, or do it only if
par frame a box split by dashed lines the parts run at the same time
Stage 4: placeOrder, approved and declined
OrderReceipt queuePaymentGatewayCatalogCheckoutServiceOrderReceipt queuePaymentGatewayCatalogCheckoutServiceloop[each cart entry]opt[discount given]alt[approved][declined]CustomerplaceOrder(cart, discount)new Order()find(sku)productaddLine(product, qty)apply(discount)totalCents()2880charge(orderId, 2880)approved or declinedmarkPaid()receipt for orderIdcancel()orderCustomerOrderReceipt queuePaymentGatewayCatalogCheckoutServiceOrderReceipt queuePaymentGatewayCatalogCheckoutServiceloop[each cart entry]opt[discount given]alt[approved][declined]CustomerplaceOrder(cart, discount)new Order()find(sku)productaddLine(product, qty)apply(discount)totalCents()2880charge(orderId, 2880)approved or declinedmarkPaid()receipt for orderIdcancel()orderCustomer
Scroll or pinch to zoom, drag to pan
  • Color key
  • Entity
  • Queue or stream or Interface
  • Facade

The diagram and the code match message for message. The stage 4 code adds the order’s status, a check for an empty cart, and the receipt queue:

Order.javacheckout_service.pyCheckoutService.cscheckout_service.gocheckout-service.ts
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
// Product, Catalog, LineItem, Discount and PercentOff are unchanged from the model above.
class Product {
private final String sku;
private final String name;
private final int priceCents;
Product(String sku, String name, int priceCents) {
this.sku = sku;
this.name = name;
this.priceCents = priceCents;
}
String sku() { return sku; }
String name() { return name; }
int priceCents() { return priceCents; }
}
class Catalog {
private final Map<String, Product> bySku = new HashMap<>();
void add(Product p) { bySku.put(p.sku(), p); }
Product find(String sku) {
Product p = bySku.get(sku);
if (p == null) throw new IllegalArgumentException("unknown sku " + sku);
return p;
}
}
final class LineItem {
private final Product product;
private final int qty;
private final int unitPriceCents;
LineItem(Product product, int qty) {
if (qty < 1) throw new IllegalArgumentException("qty must be at least 1");
this.product = product;
this.qty = qty;
this.unitPriceCents = product.priceCents();
}
int subtotalCents() { return unitPriceCents * qty; }
}
abstract class Discount {
protected final String label;
protected Discount(String label) { this.label = label; }
public abstract int amountOffCents(int subtotalCents);
public String describe() { return label; }
}
class PercentOff extends Discount {
private final int percent;
PercentOff(int percent) {
super(percent + "% off");
this.percent = percent;
}
@Override
public int amountOffCents(int subtotalCents) { return subtotalCents * percent / 100; }
}
// The states from the state diagram. An enum, because the set is fixed and small.
enum OrderStatus { CREATED, PAID, CANCELLED, SHIPPED }
// Multiplicities become types and checks:
// "1..*" LineItem -> a list, and placeOrder refuses an empty cart
// "0..1" Discount -> a field that may be null
// Each transition method is one arrow of the state diagram; any other move throws.
class Order {
private static int nextId = 1;
private final int id;
private final List<LineItem> lines = new ArrayList<>();
private Discount discount;
private OrderStatus status = OrderStatus.CREATED;
Order() { id = nextId++; }
int id() { return id; }
OrderStatus status() { return status; }
void addLine(Product product, int qty) { lines.add(new LineItem(product, qty)); }
void apply(Discount d) { discount = d; }
int totalCents() {
int subtotal = lines.stream().mapToInt(LineItem::subtotalCents).sum();
return discount == null ? subtotal : subtotal - discount.amountOffCents(subtotal);
}
void markPaid() { moveTo(OrderStatus.PAID, OrderStatus.CREATED); }
void cancel() { moveTo(OrderStatus.CANCELLED, OrderStatus.CREATED); }
void ship() { moveTo(OrderStatus.SHIPPED, OrderStatus.PAID); }
private void moveTo(OrderStatus next, OrderStatus from) {
if (status != from)
throw new IllegalStateException("cannot go from " + status + " to " + next);
status = next;
}
}
interface PaymentGateway {
boolean charge(int orderId, int cents);
}
class FakeCardGateway implements PaymentGateway {
private final int limitCents;
FakeCardGateway(int limitCents) { this.limitCents = limitCents; }
public boolean charge(int orderId, int cents) { return cents <= limitCents; }
}
// placeOrder is the sequence diagram, message by message.
class CheckoutService {
private final Catalog catalog;
private final PaymentGateway gateway;
// Stands in for a message queue. Adding to it is the async arrow: send and move on,
// and a mailer drains it later. The customer never waits for the email.
private final Queue<Integer> receiptQueue;
CheckoutService(Catalog catalog, PaymentGateway gateway, Queue<Integer> receiptQueue) {
this.catalog = catalog;
this.gateway = gateway;
this.receiptQueue = receiptQueue;
}
Order placeOrder(Map<String, Integer> cart, Discount discount) {
if (cart.isEmpty()) throw new IllegalArgumentException("an order needs at least one line");
Order order = new Order(); // the create message: a new lifeline starts here
for (var entry : cart.entrySet()) { // the loop frame
Product p = catalog.find(entry.getKey()); // sync call, waits for the return
order.addLine(p, entry.getValue());
}
if (discount != null) order.apply(discount); // an opt frame, if the diagram shows it
if (gateway.charge(order.id(), order.totalCents())) { // the alt frame, first branch
order.markPaid();
receiptQueue.add(order.id()); // async: no return arrow
} else { // the alt frame, else branch
order.cancel();
}
return order; // the dashed return arrow to the caller
}
}
class Main {
public static void main(String[] args) {
Catalog catalog = new Catalog();
catalog.add(new Product("MUG-1", "Mug", 1200));
catalog.add(new Product("TEA-9", "Green tea", 800));
Queue<Integer> receipts = new ArrayDeque<>();
CheckoutService checkout =
new CheckoutService(catalog, new FakeCardGateway(5_000), receipts);
Map<String, Integer> cart = new LinkedHashMap<>();
cart.put("MUG-1", 2);
cart.put("TEA-9", 1);
Order paid = checkout.placeOrder(cart, new PercentOff(10));
System.out.println(paid.id() + " " + paid.status() + " " + paid.totalCents()); // 1 PAID 2880
Order declined = checkout.placeOrder(Map.of("MUG-1", 5), null);
System.out.println(declined.id() + " " + declined.status()); // 2 CANCELLED
paid.ship();
System.out.println(paid.status() + ", receipts queued for " + receipts); // SHIPPED, receipts queued for [1]
try {
declined.ship(); // no arrow from CANCELLED to SHIPPED in the state diagram
} catch (IllegalStateException e) {
System.out.println(e.getMessage()); // cannot go from CANCELLED to SHIPPED
}
}
}

Reading one against the other:

  • Each solid arrow with a filled head is a method call on the field or local the class diagram promised: catalog.find, gateway.charge. If the sequence needs to call something the class diagram does not connect, one of the two diagrams is wrong.
  • The loop frame is the for loop, and the guard names what it iterates.
  • The alt frame is the if/else on the charge result. Each branch shows only its own messages, so the reader sees that a declined card never queues a receipt.
  • The async arrow is the queue write. It has no return arrow, because nothing comes back. The customer does not wait for the email, and a mailer drains the queue later.
  • The create message starts Order’s lifeline partway down, which shows that the order does not exist before checkout starts.
Interviewerminute 22

Walk me through what happens when the card is declined.

What they are testing

Whether you can trace one flow across objects, in order, and name the branch. The order exists and has a total. The charge comes back declined. The order is cancelled, and no receipt is queued. Drawing the alt frame takes thirty seconds, and answers it. The red flag is describing only the happy path, or a design where the receipt is sent before the charge.

When an object has a lifecycle with rules about which moves are allowed, a state diagram says it faster than prose. Order is one: it can be paid only once, shipped only after payment, and never shipped once cancelled.

Order status: the only legal moves

new Order()

markPaid, card approved

cancel, card declined

ship

CREATED

PAID

CANCELLED

SHIPPED

new Order()

markPaid, card approved

cancel, card declined

ship

CREATED

PAID

CANCELLED

SHIPPED

Scroll or pinch to zoom, drag to pan
  • Color key
  • State
  • Ends well

Each arrow is one method in the stage 4 code, and every move the diagram lacks throws. That’s why declined.ship() fails with “cannot go from CANCELLED to SHIPPED”. Strict UML writes a transition label as trigger [guard] / effect, as in charge [approved] / markPaid. A plain phrase like the ones above is enough in a sketch. A state diagram is worth drawing for vending machines, elevators, ATMs, bookings and orders. Sometimes states start carrying different behavior, not just different allowed moves. Then the state pattern turns each state into a class.

Most of UML does not belong in a 45 minute design round. Knowing what to leave out is part of the skill.

Diagram or feature In an LLD round
Class diagram Always, in stages, with the five symbols
Sequence diagram For the one or two flows that cross several objects or have a failure branch
State diagram When an entity has a lifecycle with forbidden moves
Use case diagram Skip. A numbered list of use cases says the same thing faster
Activity diagram, flowchart Skip. A short method or a sequence diagram shows the logic better
Component, deployment, package diagrams Skip in LLD. Service boundaries belong to a system design round
Object, communication, timing, interaction overview Skip
Qualifiers, association classes, derived attributes, OCL constraints Skip. A one-line note next to the arrow does the job
Getters, setters, constructors, every type Skip. Show what carries a use case
Idea Looks like UML composition because The difference
“Favor composition over inheritance” Same word The design advice means object composition: holding other objects in fields and delegating to them. In UML terms that is usually a plain association. See composition vs inheritance.
Dependency injection It contains “dependency” An injected object kept in a field is a UML association. UML’s dependency is a use that leaves no field behind.
ER diagram Boxes, lines, cardinalities ER diagrams model tables and foreign keys, with crow’s feet for “many”. Class diagrams model objects with behavior. A foreign key in the database is often an association in code.
Go struct embedding Looks like extends Embedding forwards fields and methods, but the outer type is not the inner type. It is composition, not inheritance.
Flowchart or activity diagram Shows a process A flowchart shows steps inside one routine. A sequence diagram shows which object does each step, which is what a design round is checking.

On this site, every LLD problem uses these symbols. A few show one idea especially clearly:

  • Parking lot: the lot aggregates levels, and each level is composed of its spots, a filled and a hollow diamond in the same diagram.
  • File system: File and Directory both inherit from Entry, the hollow triangle at the center of the composite pattern.
  • Logging framework: appenders and formatters are interfaces with several implementations, a page full of dashed triangles.
  • ATM: the withdrawal sequence diagram uses an alt frame for a cash jam, the failure branch that decides the design.
  • Vending machine: a state diagram of the machine first, then one class per state.
  • Elevator: a state diagram of what one tick can do to a car. And a sequence diagram of a hall call, from button to arrival.
  • Movie ticket booking: a seat’s life as a state diagram, and a slow payment as a sequence. It’s the same pair this page draws for an order.
Interviewer asksWhat is the difference between aggregation and composition, and how would I see it in code?

Composition means each part has one owner and shares its lifetime. In code the owner creates the parts, keeps them private and never hands them out, so when the owner is gone nothing references them. Aggregation means the parts are referenced but live independently, which in code is the same as a plain association. I use composition when the lifetime rule matters and a plain arrow otherwise.

Interviewer asksA class gets its repository through the constructor. Which arrow do you draw?

A solid arrow from the class to the repository interface: an association, because it is kept in a field. Not a diamond, because the class did not create it and does not own its lifetime. Not a dashed dependency arrow, because it outlives a single call. The repository’s implementation hangs off the interface with a dashed triangle.

Interviewer asksWhen would you draw a sequence diagram instead of just writing the code?

When one use case crosses three or more objects, and the order or the failure branch is the point. Examples are checkout, a booking with a hold and a confirm, and an ATM withdrawal. It shows in a minute that the charge happens before the receipt. And the alt frame makes the declined path visible. For a call inside one class, the code is clearer.

Interviewer asksYour diagram has an arrow from Order to Product and one from Product to Order. Do you need both?

Only if a use case walks both ways often. Two-way means two fields that must change together, and each class now changes when the other does. I’d keep Order to Product, through the line items, and answer “which orders contain this product?” with a query.

Decision We chose We gave up
What goes in a box Fields that hold state and methods that carry use cases A complete member list
Owner and parts Composition only when the owner creates the parts and nobody else holds them Aggregation’s extra nuance, drawn as a plain arrow instead
Injected collaborators Association: a solid arrow to an interface The literal reading of “dependency injection”
Arrow direction Toward the class that is known, used or inherited from Nothing: drawn the other way, it says something false
Navigability One way, the way a use case walks Walking back without a lookup
Behavior over time One sequence diagram for the flow with a failure branch, one state diagram for a lifecycle Activity, deployment and the other ten diagram types