Low Level Design — OOP, UML & Class Relationships

My self-contained study reference: why LLD matters, the three interview formats, the four OOP pillars, how to draw class diagrams in UML, the has-a relationship family (association / aggregation / composition), and cardinality. Code is Java (most common stable build: JDK 17+, runs on 8+).

Why LLD Interview formats 4 OOP pillars UML class diagram Association / Aggregation / Composition Cardinality 1:1 1:M M:1 M:M

+ = my own enrichment or a correction beyond the original material.

Section I — Notebook Summary

Copy by hand. Telegraphic, high-density, fully derivable from the Detailed Notes below.
═══ WHY LLD ═══
LLD = Low-Level Design → detailed blueprint BEFORE coding.
Build-a-house analogy:
  jump in       → inconsistencies, rework
  high-lvl only → vague, confusion
  detailed plan → BEST: roadmap, aligned team, fewer errors ✓
Gives: exact specs to start coding.
Goal of good design → scalable + low-maintenance + robust.
Design on paper first → know where dev is going.
Expected from devs with > 1 yr experience.
doodle: 3 little houses in a row — #1 crooked (jump in), #2 dotted outline only (high-level), #3 solid + ✓ with a ruler next to it (detailed LLD). Arrow points to #3.
═══ TYPES OF LLD INTERVIEWS ═══
TypeCompaniesFormat
TheoreticalOld MNCs/banks (Oracle)30–45m · 3–4 Qs · OOP/patterns theory · no/toy code
DesignBig MNCs (Amazon, PayPal, Walmart)45m · 1 Q + discussion · code a few classes · not end-to-end
Machine CodingFlipkart, Myntra, Razorpay, startups2.5h · full working code from requirements
recall: Theory→talk, Design→sketch+some code, Machine→ship it.
═══ PREREQUISITES ═══
- OOP lang exp (Java/C++/Python)
- OOP concepts: Encapsulation, Inheritance, Abstraction, Polymorphism
                + Association / Aggregation / Composition   (this session)
- SOLID principles            (later sessions)
- Design patterns: bonus(junior) / expected(senior)  (later)
═══ 4 PILLARS OF OOP (term → goal) ═══
Encapsulation → reduce complexity + data security  (bundle data+methods, hide fields)
Abstraction   → hide complexity + isolate impact   (expose what, hide how)
Inheritance   → kill redundant code + reuse        (is-a)
Polymorphism  → one object, many forms             (override / overload)
doodle: 4 pillars holding up a roof labeled "OOP". Under each pillar write E / A / I / P.
═══ HOW TO REPRESENT LLD ═══
Words (email/meeting/Slack) → ambiguous, "I didn't mean that", hard for complex.
Diagrams → 1 pic = 1000 words → less ambiguity, easier to visualize. ✓
UML = Unified Modelling Language → STANDARD way to draw software concepts.
═══ UML CLASS DIAGRAM (1 box, 3 compartments) ═══
┌────────────────────────┐
│      ClassName         │   ← name
├────────────────────────┤
│ - id : int             │   ← attributes (fields)
│ - name : String        │
├────────────────────────┤
│ + getName() : String   │   ← methods
│ + setName(String):void │
└────────────────────────┘
Visibility:  + public   - private   # protected   ~ package
Syntax:  attr →  vis name : Type        method → vis name(params) : Return
underline = static, italic = abstract
doodle: one rectangle split into 3 stacked boxes (name / fields / methods) — that's a class.
═══ CLASS RELATIONSHIPS (the "has-a" family) ═══
Association ⊃ Aggregation ⊃ Composition   (each a stricter special case)
RelationshipOwnership?Child lifecycleUML arrow
Associationnoneindependentplain line ──
Aggregationowner exists, but looseindependent (child survives parent)hollow diamond ◇──
Compositionstrong ownerdies with parentfilled diamond ◆──
Association  → Student ↔ Teacher        (use each other, no owner)
Aggregation  → Car ◇── Passenger        (owner, but passenger lives on)
             → Department ◇── Professor  (delete dept ⇒ profs still exist)
Composition  → House ◆── Room           (delete house ⇒ rooms gone)
             → Order ◆── OrderLineItem   (line items have no life of their own)
test: "if I delete the parent, must the child die?"  yes→Composition  no(but owned)→Aggregation  no owner→Association
doodle: two boxes joined by a line. Hollow ◇ at parent end = aggregation; fill it in ◆ = composition; no diamond = association.
═══ CARDINALITY (how many ↔ how many) ═══
Definition: # of instances of one class that relate to instances of another.
PairCardinalityReasoning
User → Passport1 : 11 user has 1 passport; 1 passport → 1 user
User → CreditCard1 : M1 user many cards; 1 card → 1 user
User → SchoolM : 11 user joins 1 school; 1 school many users
User → CourseM : Muser many courses; course many users
UML notation: 1, 0..1, *, 1..*, 0..*  written at each end of the line.
doodle: two boxes, write the number on each end of the connecting line: User [1]──[*] Course = 1-to-many. Many↔many = star on both ends.

Section II — Detailed Study Notes

One subsection per concept. + marks my enrichment/corrections. Each links to its runnable code below.

1.Why Low-Level Design?

LLD is the detailed blueprint you create before writing code, so construction is a guided execution rather than improvisation.

Imagine being asked to build a house. There are three ways to start. Jump right in and build without a plan — you make decisions on the fly and risk inconsistencies and structural problems. Do only a high-level design — a rough blueprint of the overall shape — and you'll hit confusion and errors because the specifics were never nailed down. Or do detailed planning first: exact materials, measurements, and techniques, every aspect decided before the first brick. The third path wins. It gives a precise roadmap, aligns everyone on their role, and minimizes errors and rework, producing a structurally sound result.

Software is the same. LLD is the equivalent of those detailed construction plans for a module: which classes exist, what they hold, how they relate, and how they collaborate. You design on paper before jumping into code so you always know where the development is heading.

Why it mattersThe cost of fixing a design mistake rises sharply the later you catch it. A bad class boundary spotted on paper is a five-minute edit; the same mistake found after three teammates have built on top of it is days of rework. LLD front-loads the cheap thinking.
Interview angle"Walk me through your design before you code" is the single most common LLD-round instruction. Sketching classes and relationships first signals seniority; diving straight into code signals the opposite.

+ A useful mental model: HLD (High-Level Design) answers "what are the big boxes and how do they talk?" (services, databases, queues), while LLD answers "inside one box, what are the classes, fields, methods, and relationships?" They sit at different zoom levels — LLD is the close-up.

2.What LLD Actually Delivers

LLD produces the concrete specifications a developer needs to start coding — and a habit of designing modules on paper first.

The point of building scalable, low-maintenance, robust software is something every engineer is expected to care about, and LLD is how you get there deliberately instead of by luck. It is good practice to design modules on paper before coding because it surfaces gaps early and keeps the development process pointed in a clear direction. Once you have more than about a year of experience, some level of LLD fluency is simply expected of you.

Why it matters"Scalable, low-maintenance, robust" aren't decoration — they're the three properties that decide whether code survives contact with real users and a growing team. LLD is the lever you pull to get them on purpose.

+ Quick definitions of the three target qualities, since they're the whole goal: scalable = handles more load/features without a rewrite; low-maintenance = easy to change safely (small, well-separated pieces); robust = behaves correctly under edge cases and failures. Good relationships and clear class boundaries (the rest of these notes) are exactly the tools that buy you these.

3.The Three Types of LLD Interviews

LLD interviews come in three flavors — Theoretical, Design, and Machine Coding — and each company type favors a different one.

Theoretical rounds are common at older, large tech companies and banks (e.g., Oracle). They test knowledge with theoretical questions about OOP and design patterns. Expect 30–45 minutes, three or four questions, and no real code (toy code at most). Design rounds appear at big MNCs like Amazon, PayPal, and Walmart. They're problem-solving, case-study rounds: roughly 45 minutes, one question plus discussion, where you write code for a few classes but aren't expected to make an end-to-end program run. Machine Coding rounds are the very first round at companies like Flipkart, Myntra, and Razorpay, and at essentially every startup. They want people who can actually implement: you write end-to-end working code from a set of requirements, usually over about 2.5 hours.

DimensionTheoreticalDesignMachine Coding
CompaniesOld MNCs, banks (Oracle)Amazon, PayPal, WalmartFlipkart, Myntra, Razorpay, startups
FocusKnowledge of OOP/patternsProblem solving, case studyCan you code/implement
Duration30–45 min~45 min~2.5 hours
Code expectedNone / toyA few classesFull working code
# Questions3–41 + discussion1 requirement set
Interview angleIdentify the format before you prepare. For Theoretical, drill crisp definitions (these notes). For Design, practice talking through tradeoffs out loud. For Machine Coding, practice writing clean, compiling, end-to-end code fast under a timer.

+ These aren't mutually exclusive across a single loop — a company can run a Design round and a Machine Coding round on the same day. Treat the three as modes to be ready for, not a single label per company.

4.Prerequisites for LLD

You need hands-on OOP experience, solid grasp of the OOP concepts, SOLID principles, and ideally design patterns.

The foundation is programming experience in an OOP language — Java, C++, Python, or similar. On top of that, a good understanding of the OOP concepts: encapsulation, inheritance, abstraction, polymorphism, and the relationship trio of association, aggregation, and composition. Beyond the concepts, a good grasp of object-oriented design principles, especially SOLID. Finally, command of design patterns — a bonus for entry-level developers and effectively expected of senior ones.

+ One-line preview of SOLID so the term isn't a black box: Single responsibility, Open/closed, Liskov substitution, Interface segregation, Dependency inversion. + Design patterns are reusable, named solutions to recurring design problems (e.g., Factory, Strategy, Observer, Singleton). Both are the natural next layer on top of everything in these notes.

5.The Four Pillars of OOP (Recap)

Encapsulation, Abstraction, Inheritance, and Polymorphism each exist to solve a specific problem — learn the term by its goal.

Encapsulation bundles data and the methods that operate on it inside one unit and hides the internal fields, exposing controlled access. Its goals: reduce complexity and provide data security. Abstraction exposes what something does while hiding how it does it; goals: hide complexity and isolate the impact of changes (callers depend on a stable surface, not internals). Inheritance lets a class reuse another class's members through an "is-a" relationship; goals: eliminate redundant code and enable reuse. Polymorphism lets one object take many forms — the same call behaves differently depending on the actual type.

PillarOne-line meaningGoal
EncapsulationBundle data+behavior, hide fieldsReduce complexity + data security
AbstractionExpose what, hide howHide complexity + isolate change impact
InheritanceReuse via "is-a"Eliminate redundancy + reuse
PolymorphismOne interface, many behaviorsFlexibility / extensibility
Common misconceptionEncapsulation ≠ abstraction. Encapsulation is the mechanism (private fields + getters/setters bundling state with behavior); abstraction is the perspective (hiding complexity behind a simple interface). They often appear together but answer different questions.

+ Polymorphism in Java has two concrete forms worth naming: compile-time (method overloading — same name, different parameters) and runtime (method overriding — subclass redefines a parent method, resolved by the actual object type via dynamic dispatch). Interviewers frequently ask for this distinction. See code §III · OOP pillars.

6.How to Represent an LLD

Words are ambiguous; diagrams aren't — so we standardize diagrams with UML.

You could communicate a design with words — email, a meeting, a Slack message — but words breed misunderstanding ("I didn't mean that") and struggle to convey complex scenarios. Diagrams are better: a picture is worth a thousand words, they carry less ambiguity, and they make complex structures and scenarios easy to visualize. The remaining problem: if everyone draws their own way, diagrams stop being a shared language. UML (Unified Modelling Language) solves that by providing a standard for representing software concepts in diagrams.

WordsDiagrams (UML)
AmbiguityHigh — "I didn't mean that"Low — standardized notation
Complex scenariosHard to explainEasy to visualize
Shared understandingDepends on phrasingCommon visual vocabulary
Why it mattersA class diagram you can sketch in a minute communicates structure that would take three paragraphs to describe and still be misread. In an interview, drawing it is also how you externalize and pressure-test your own thinking before committing to code.

+ UML defines many diagram types — class, sequence, use-case, activity, state, etc. For LLD the class diagram (static structure) and the sequence diagram (how objects interact over time) are the two you'll actually use most.

7.The UML Class Diagram

A class is drawn as one box split into three compartments: name, attributes, and methods.

The class diagram is the workhorse of LLD: it shows the static structure — the classes and how they relate. Each class is a rectangle divided into three stacked compartments. The top holds the class name. The middle lists attributes (fields) as visibility name : Type. The bottom lists methods as visibility name(params) : ReturnType. Visibility markers are a small, fixed vocabulary.

┌──────────────────────────────┐
│           BankAccount        │
├──────────────────────────────┤
│ - accountNo : String         │
│ - balance   : double         │
├──────────────────────────────┤
│ + deposit(amount:double):void│
│ + getBalance() : double      │
└──────────────────────────────┘
SymbolVisibilityMeaning
+publicAccessible everywhere
-privateWithin the class only
#protectedClass + subclasses
~packageSame package +

+ Two more conventions worth memorizing: underlined members are static, and italic class or method names are abstract. + The attribute/method ordering convention is visibility name : type — note the type comes after the name with a colon, which is the reverse of Java's own int id; syntax. This trips people up.

PitfallDon't cram every getter/setter and trivial field into the diagram. A class diagram is a communication tool — show the members that matter to the design and the relationships. Over-detailing makes it unreadable.

8.Class Relationships — the "Has-A" Family

Beyond "is-a" (inheritance), classes relate by "has-a": association, aggregation, and composition — three points on one spectrum of ownership and lifecycle.

Take a simple requirement set for a Manager at "XYZ Limited": the manager is an employee of XYZ; the manager uses a swipe card to enter the premises; the manager has workers under them; the manager is responsible for a project's success; and the manager's salary is judged by that project's success. Each clause hides a different kind of relationship between objects, and naming them precisely is the whole game.

The deciding questions are always two: Is there ownership? and What happens to the child's lifecycle when the parent goes away? Crucially, aggregation and composition are special cases of association — they're association plus increasing strictness — which is exactly why it can be hard to decide which to use.

          ASSOCIATION  (no ownership, independent lives)
                │
                ▼ add an owner (loose)
          AGGREGATION  (owner, but child outlives parent)
                │
                ▼ tighten: child can't live without parent
          COMPOSITION  (child dies with parent)
Interview angle"What's the difference between aggregation and composition?" is a top-5 LLD theory question. The crisp answer: both have an owner; in composition the child cannot exist without the parent and is destroyed with it, in aggregation the child has its own lifecycle and survives.

+ In code these typically look like: association/aggregation = the parent holds a reference to an object created elsewhere and passed in; composition = the parent creates and owns the child internally so nothing outside can hold it. The lifecycle rule falls straight out of who constructs the object.

9.Association

Association: objects use each other, have independent lifecycles, and there is no ownership between them.

Association is the loosest relationship. The objects use one another but each has its own lifetime and can exist without the other — there is no single owner. In the manager example, the manager and the swipe card use each other, yet each exists independently; neither owns the other. A classic textbook example: a teacher and a student — multiple students can have a single teacher and a single student can have multiple teachers. Neither creates or destroys the other.

Why it mattersAssociation is your default when two objects merely collaborate. Reaching for aggregation or composition when there's no real ownership over-constrains the design and couples lifecycles that should be independent.

Real-world examples: Doctor ↔ Patient, Author ↔ Reader, Driver ↔ Passenger on a single ride. + Direction matters: association can be unidirectional (A knows B, B doesn't know A) or bidirectional (both reference each other). In UML it's a plain solid line; an arrowhead indicates navigability/direction.

10.Aggregation

Aggregation: there IS ownership and the child can't belong to two parents at once, but the child has an independent lifecycle and survives the parent.

Aggregation is like association but with one owner. From the manager requirements, "the manager has workers who work under him" is aggregation: the Manager object owns the Worker objects, and a given Worker can't simultaneously belong to some other unrelated parent (a Worker doesn't belong to a SwipeCard). But — and this is the defining trait — a Worker has their own lifetime, disconnected from the Manager. If the Manager object is deleted, the Worker object does not die. Another clean example: a car may have passengers; they come and go, and they clearly outlive any single car.

Common misconception"Ownership" in aggregation does not mean "controls lifecycle." It means a directional has-a with a clear parent, but the child can be created before and live after the parent. The lifecycle independence is what separates it from composition.

Real-world examples: Department ◇── Professor (close the department, professors still exist and move elsewhere); Team ◇── Player; Playlist ◇── Song (delete the playlist, the songs remain). + UML notation: a hollow (unfilled) diamond on the owner/parent end of the line. Mnemonic: hollow diamond = loose hold.

doodle: box "Car" ◇── box "Passenger"; draw a tiny stick figure walking AWAY from a crashed car to remember the child survives.

11.Composition

Composition: the child has no independent lifecycle — if the parent is deleted, every child is deleted too.

Composition is the strictest has-a. The child object does not have an independent lifecycle, and if the parent is deleted all children go with it. One framing: "the manager and the project are dependent on each other and share the same lifetime" — the project won't succeed without a good manager and the manager's increments depend on the project — making them heavily interdependent, where if one is garbage-collected the other effectively is too. A cleaner, more standard example: a hotel has rooms — destroy the hotel and the rooms are destroyed with it. The rooms have no meaning or existence apart from the hotel.

Why it mattersComposition encodes a real-world constraint directly into your object lifetimes: some things genuinely cannot outlive their container. Modeling that as composition prevents orphaned, meaningless objects (a Room with no Hotel).

+ A note on the source's primary example: framing Manager ↔ Project as composition is debatable — it's really describing a tight two-way dependency, not a contained-part-with-no-independent-existence. The textbook-clean composition test is the Hotel/Room or House/Room case, where the part literally cannot exist without the whole. Lean on those examples in an interview.

Real-world examples: House ◆── Room; Order ◆── OrderLineItem; Human ◆── Heart; Document ◆── Paragraph. + UML notation: a filled (solid) diamond on the parent end. Mnemonic: filled diamond = lifelong, fully owned. In code the parent usually news the child inside itself.

doodle: box "House" ◆── box "Room"; draw the House box on fire 🔥 with the Room box inside it also burning = both die together.

Putting the has-a family together

Deciding between the three can be genuinely tricky because aggregation and composition are subsets of association. Use this decision flow:

Q1: Is there a clear owner (parent has-a child)?
      NO  → ASSOCIATION
      YES → Q2
Q2: If the parent is deleted, must the child be deleted too?
      YES → COMPOSITION   (◆ filled diamond, parent creates child)
      NO  → AGGREGATION   (◇ hollow diamond, child passed in)
TraitAssociationAggregationComposition
OwnershipNoneLoose ownerStrong owner
Child lifecycleIndependentIndependent (survives)Dies with parent
Child shared?n/aBelongs to one parentBelongs to one parent
UML symbol── line◇── hollow diamond◆── filled diamond
Typical codeholds a referencereference passed inparent news child
ExampleTeacher/StudentCar/PassengerHouse/Room

12.Cardinality

Cardinality describes how many instances of one class relate to how many instances of another.

Cardinality is the count side of a relationship: for a link between two classes, how many objects participate at each end. You determine it by asking the question in both directions and combining the answers. The four canonical cases, worked through with a User class:

PairQ: A → how many B?Q: B → how many A?Cardinality
User / Passport1 passport1 user1 : 1
User / CreditCardmany cards1 user1 : M
User / School1 schoolmany usersM : 1
User / Coursemany coursesmany usersM : M

Read each as "User → X". 1:1 a user has exactly one passport and a passport belongs to one user. 1:M a user has many credit cards but each card belongs to one user. M:1 a user joins one school but a school holds many users. M:M a user enrolls in many courses and a course holds many users.

Why it mattersCardinality drives concrete code and schema decisions: a 1:M becomes a List<Card> on the User; an M:M typically needs a join/association table (e.g., Enrollment linking User and Course). Getting cardinality wrong means the wrong data structure everywhere downstream.

+ Standard UML multiplicity notation written at each line end: 1 (exactly one), 0..1 (optional one), * or 0..* (zero or more), 1..* (one or more). So a User-to-Course many-to-many is drawn User 1..* ──── 0..* Course (or *—*). + M:M is the case to watch — it almost always needs an extra class to hold the relationship plus any per-link data (enrollment date, grade). See Code Sample: Cardinality / Enrollment.

Pitfall1:M vs M:1 is just a matter of which side you read first — they're the same relationship from opposite ends. State your reference direction ("User → School") so M:1 vs 1:M is unambiguous.

Section III — Code Samples

Runnable Java (JDK 17+, standard library only). Each block: filename, run command, inline comments, expected output.

§OOP Pillars (encapsulation, inheritance, polymorphism, abstraction)

Links back to Notes §5. One file demonstrating all four pillars, including both overloading (compile-time) and overriding (runtime) polymorphism.

OopPillars.javarun: javac OopPillars.java && java OopPillars
// Abstraction: callers use Shape without knowing each subclass's math.
abstract class Shape {
    abstract double area();              // "what", not "how"
}

// Inheritance: Circle IS-A Shape -> reuses the Shape type.
class Circle extends Shape {
    private double r;                    // Encapsulation: field is private
    Circle(double r) { this.r = r; }
    double area() { return Math.PI * r * r; }   // Polymorphism: overrides area()
}

class Rectangle extends Shape {
    private double w, h;
    Rectangle(double w, double h) { this.w = w; this.h = h; }
    double area() { return w * h; }             // runtime polymorphism (override)
}

class MathUtil {
    // Compile-time polymorphism: OVERLOADING (same name, diff params)
    static int max(int a, int b)       { return a > b ? a : b; }
    static double max(double a, double b) { return a > b ? a : b; }
}

public class OopPillars {
    public static void main(String[] args) {
        Shape[] shapes = { new Circle(2), new Rectangle(3, 4) };
        for (Shape s : shapes) // Same call s.area() -> different behavior per actual type
        System.out.printf("%s area = %.2f%n", s.getClass().getSimpleName(), s.area());

        System.out.println("max(int)    = " + MathUtil.max(7, 3));
        System.out.println("max(double) = " + MathUtil.max(7.5, 9.1));
    }
}
Expected output
Circle area = 12.57
Rectangle area = 12.00
max(int)    = 7
max(double) = 9.1
Why it behaves this way: s.area() is resolved at runtime by the object's actual class (dynamic dispatch), so the loop prints different formulas — that's overriding. MathUtil.max is resolved at compile time by argument types — that's overloading. The private r, w, h fields show encapsulation; extends Shape shows inheritance; the abstract class shows abstraction.

§A UML Class as Java (visibility markers)

Links back to Notes §7. The three-compartment diagram (name / attributes / methods) realized in code, showing how UML visibility maps to Java keywords.

BankAccount.javarun: javac BankAccount.java && java BankAccount
public class BankAccount {
    // ---- UML middle compartment: attributes ----
    private String accountNo;   // UML  - accountNo : String   (-, private)
    private double balance;     // UML  - balance   : double

    public BankAccount(String accountNo) {  // UML + (public) constructor
        this.accountNo = accountNo;
        this.balance = 0.0;
    }

    // ---- UML bottom compartment: methods ----
    public void deposit(double amount) {        // + deposit(amount:double):void
        if (amount <= 0) throw new IllegalArgumentException("amount must be > 0");
        balance += amount;
    }

    public double getBalance() { return balance; }   // + getBalance() : double

    protected String maskedNo() {                    // # (protected)
        return "****" + accountNo.substring(accountNo.length() - 2);
    }

    public static void main(String[] args) {
        BankAccount acc = new BankAccount("100042");
        acc.deposit(500);
        acc.deposit(250);
        System.out.println("Account " + acc.maskedNo() + " balance = " + acc.getBalance());
    }
}
Expected output
Account ****42 balance = 750.0
Mapping: private→-, public→+, protected→#, package-private (no modifier)→~. Note UML writes name : Type while Java writes Type name — reversed order.

§Association — Teacher / Student

Links back to Notes §9. No ownership; both objects are created independently and merely reference each other.

AssociationDemo.javarun: javac AssociationDemo.java && java AssociationDemo
import java.util.*;

class Teacher {
    String name;
    Teacher(String name) { this.name = name; }
}

class Student {
    String name;
    // Association: a student references teachers but does NOT own them.
    List<Teacher> teachers = new ArrayList<>();
    Student(String name) { this.name = name; }
    void addTeacher(Teacher t) { teachers.add(t); }  // many-to-many link
}

public class AssociationDemo {
    public static void main(String[] args) {
        Teacher alice = new Teacher("Alice");   // created independently
        Teacher bob   = new Teacher("Bob");
        Student sam = new Student("Sam");
        sam.addTeacher(alice);
        sam.addTeacher(bob);

        System.out.print(sam.name + " is taught by: ");
        for (Teacher t : sam.teachers) System.out.print(t.name + " ");
        System.out.println();
        // Deleting Sam (drop reference) does NOT affect the teachers:
        sam = null;
        System.out.println("After Sam is gone, Alice still exists: " + alice.name);
    }
}
Expected output
Sam is taught by: Alice Bob
After Sam is gone, Alice still exists: Alice
Why: alice and bob exist on their own; the student only holds references. Removing the student leaves the teachers untouched — that independence is the signature of association.

§Aggregation — Department / Professor

Links back to Notes §10. Ownership exists (Department has-a Professor), but professors are created outside and survive the department.

AggregationDemo.javarun: javac AggregationDemo.java && java AggregationDemo
import java.util.*;

class Professor {
    String name;
    Professor(String name) { this.name = name; }
}

class Department {
    String name;
    // Aggregation: professors are PASSED IN, not created here.
    private List<Professor> professors;
    Department(String name, List<Professor> professors) {
        this.name = name;
        this.professors = professors;   // shares externally-owned objects
    }
    List<Professor> getProfessors() { return professors; }
}

public class AggregationDemo {
    public static void main(String[] args) {
        // Professors exist BEFORE the department and can outlive it.
        Professor p1 = new Professor("Dr. Rao");
        Professor p2 = new Professor("Dr. Lee");
        List<Professor> staff = new ArrayList<>(List.of(p1, p2));

        Department cs = new Department("Computer Science", staff);
        System.out.println(cs.name + " has " + cs.getProfessors().size() + " profs");

        cs = null;  // "delete" the department
        System.out.println("Dept gone, but " + p1.name + " still exists.");
    }
}
Expected output
Computer Science has 2 profs
Dept gone, but Dr. Rao still exists.
Why: the professors are constructed in main and injected into the department. The department references but doesn't own their lifecycle, so destroying it leaves them alive — the hollow-diamond aggregation behavior.

§Composition — House / Room

Links back to Notes §11. The House creates and owns its Rooms internally; nothing outside can hold them, so they die with the House.

CompositionDemo.javarun: javac CompositionDemo.java && java CompositionDemo
import java.util.*;

public class CompositionDemo {

    // Room is an inner/owned part. It has no meaning without a House.
    static class House {
        private final List<Room> rooms = new ArrayList<>();

        House(int roomCount) {
            // Composition: the House CREATES its own rooms internally.
            for (int i = 1; i <= roomCount; i++) rooms.add(new Room("Room-" + i));
        }
        int roomCount() { return rooms.size(); }

        // Rooms are never exposed for outside ownership.
        class Room {
            private final String label;
            Room(String label) { this.label = label; }
        }
    }

    public static void main(String[] args) {
        House h = new House(3);
        System.out.println("House has " + h.roomCount() + " rooms.");

        h = null;  // delete the house
        // No external reference to any Room exists, so all rooms are
        // unreachable now -> eligible for garbage collection with the house.
        System.out.println("House deleted -> its rooms are gone too.");
    }
}
Expected output
House has 3 rooms.
House deleted -> its rooms are gone too.
Why: rooms are built inside the House constructor and never handed out. Once the house reference is dropped, nothing else can reach the rooms, so they become garbage-collectable together — that's the "child dies with parent" lifecycle of composition.

§Cardinality — the four cases + an M:M join class

Links back to Notes §12. Shows 1:1, 1:M, M:1 as fields/lists and the M:M case modeled with an explicit Enrollment link.

CardinalityDemo.javarun: javac CardinalityDemo.java && java CardinalityDemo
import java.util.*;

class Passport { String number; Passport(String n){number=n;} }      // 1:1
class CreditCard { String last4; CreditCard(String n){last4=n;} }     // 1:M
class School { String name; School(String n){name=n;} }               // M:1
class Course { String title; Course(String t){title=t;} }             // M:M

class User {
    String name;
    Passport passport;                 // 1:1  user has exactly one
    List<CreditCard> cards = new ArrayList<>();  // 1:M user has many
    School school;                     // M:1  many users -> one school
    User(String name){ this.name = name; }
}

// M:M needs a join class to link the two sides (+ per-link data).
class Enrollment {
    User user; Course course; String date;
    Enrollment(User u, Course c, String d){ user=u; course=c; date=d; }
}

public class CardinalityDemo {
    public static void main(String[] args) {
        User u = new User("Sam");
        u.passport = new Passport("P123");           // 1:1
        u.cards.add(new CreditCard("4242"));         // 1:M
        u.cards.add(new CreditCard("1881"));
        u.school = new School("MIT");                 // M:1

        Course algo = new Course("Algorithms");      // M:M via Enrollment
        List<Enrollment> enrollments = new ArrayList<>();
        enrollments.add(new Enrollment(u, algo, "2026-06-23"));

        System.out.println("1:1  passport      = " + u.passport.number);
        System.out.println("1:M  #cards        = " + u.cards.size());
        System.out.println("M:1  school        = " + u.school.name);
        System.out.println("M:M  enrolled in   = " + enrollments.get(0).course.title);
    }
}
Expected output
1:1  passport      = P123
1:M  #cards        = 2
M:1  school        = MIT
M:M  enrolled in   = Algorithms
Why: 1:1 and M:1 are single fields; 1:M is a list. M:M can't be a clean field on either side (both would need lists pointing at each other), so the standard fix is a dedicated Enrollment class — the code equivalent of a database join table, which also gives you a home for link data like the enrollment date.

Section IV — Hands-On Lab

Practice from warm-up to challenge. Predict the output before you run, then verify.
Setup (one time)terminal
# 1. Check the toolchain (need JDK 17+; works on 8+)
java -version
javac -version

# 2. Make a working folder
mkdir lld-lab && cd lld-lab

# 3. Compile + run pattern used in every exercise
#    (put each public class in a file matching its name)
javac Solution.java
java Solution

# Tip: one file can hold many classes; only ONE public class,
# and the file name must match that public class.
Warm-up · 1

Identify the visibility markers

Goal: cement the UML ↔ Java visibility mapping. (Notes §7)

  1. Write a class Profile with: a private field email, a protected field id, a package-private field nickname, and a public getter getEmail().
  2. Next to each member, write a comment with its UML symbol.
  3. In main, create one Profile and print getEmail().
Predict then verify: Which of the four members could a class in a different package (not a subclass) legally access? Write your answer, then test by reasoning about each modifier.
Solution
Profile.javarun: javac Profile.java && java Profile
public class Profile {
    private String email;       // UML:  -  (private)
    protected int id;           // UML:  #  (protected)
    String nickname;            // UML:  ~  (package-private, no modifier)
    public String getEmail() {  // UML:  +  (public)
        return email;
    }
    public Profile(String email, int id, String nickname) {
        this.email = email; this.id = id; this.nickname = nickname;
    }
    public static void main(String[] args) {
        Profile p = new Profile("sam@x.com", 1, "sammy");
        System.out.println(p.getEmail());
    }
}
Expected output
sam@x.com

Answer: Only getEmail() (public). From a different package and not a subclass, protected, package-private, and private are all inaccessible.

Warm-up · 2

Classify five relationships

Goal: snap-judge association vs aggregation vs composition. (Notes §8–11)

  1. For each pair, decide the relationship and justify with the two questions (owner? child dies with parent?):
    • (a) Library ↔ Book
    • (b) Human ↔ Heart
    • (c) Teacher ↔ Student
    • (d) Playlist ↔ Song
    • (e) Order ↔ OrderLineItem
Predict then verify: Two of these are composition. Which two, and what's the shared tell? Decide before opening the solution.
Solution
PairRelationshipWhy
(a) Library / BookAggregationLibrary owns books, but a book survives if the library closes
(b) Human / HeartCompositionHeart has no independent life; dies with the human
(c) Teacher / StudentAssociationNo ownership; both fully independent
(d) Playlist / SongAggregationOwned by the playlist, but songs exist elsewhere too
(e) Order / OrderLineItemCompositionA line item is meaningless without its order; deleted with it

The two compositions are (b) and (e) — the shared tell is that the child has no independent existence outside its parent.

Core · 3

Code an aggregation that proves the child survives

Goal: demonstrate, in code, that an aggregated child outlives its parent. (Notes §10, Code §Aggregation)

  1. Create Player (name) and Team (name + a list of Player passed into the constructor).
  2. Build two players, put them in a team.
  3. Set the team reference to null, then print a player's name to show it still exists.
Predict then verify: After team = null, will printing p1.name throw, print blank, or print the name? Why?
Solution
TeamDemo.javarun: javac TeamDemo.java && java TeamDemo
import java.util.*;

class Player { String name; Player(String n){ name = n; } }

class Team {
    String name;
    private List<Player> players;
    Team(String name, List<Player> players){  // players passed in -> aggregation
        this.name = name; this.players = players;
    }
}

public class TeamDemo {
    public static void main(String[] args) {
        Player p1 = new Player("Ada");
        Player p2 = new Player("Linus");
        Team t = new Team("Hackers", new ArrayList<>(List.of(p1, p2)));
        System.out.println("Team built: " + t.name);
        t = null;                                  // delete the team
        System.out.println("Team gone, player alive: " + p1.name);
    }
}
Expected output
Team built: Hackers
Team gone, player alive: Ada

It prints "Ada". p1 is a separate local reference created in main; nulling the team only drops the team's hold, not the player itself. That independence is exactly what makes it aggregation, not composition.

Core · 4

Code a composition where the child is unreachable after deletion

Goal: model a parent that creates and fully owns its children. (Notes §11, Code §Composition)

  1. Create a Computer that builds its own CPU and RAM inside its constructor.
  2. Do not expose the parts via getters that hand out the objects.
  3. Print a one-line spec built from the internal parts.
Predict then verify: Could external code keep a CPU alive after the Computer is gone? Decide why your design makes that impossible.
Solution
Computer.javarun: javac Computer.java && java Computer
public class Computer {
    private final CPU cpu;     // owned parts, created internally
    private final RAM ram;

    Computer(String cpuModel, int gb) {
        this.cpu = new CPU(cpuModel);   // composition: parent NEWs the children
        this.ram = new RAM(gb);
    }
    String spec() { return cpu.model + " / " + ram.gb + "GB"; }

    private static class CPU { String model; CPU(String m){ model = m; } }
    private static class RAM { int gb;      RAM(int g){ gb = g; } }

    public static void main(String[] args) {
        Computer pc = new Computer("M3", 16);
        System.out.println("Spec: " + pc.spec());
        pc = null;  // both CPU and RAM become unreachable with it
        System.out.println("Computer deleted -> CPU & RAM gone too.");
    }
}
Expected output
Spec: M3 / 16GB
Computer deleted -> CPU & RAM gone too.

No external code can keep a part alive, because the CPU/RAM are created inside the constructor and never returned. With no outside reference, they're unreachable the moment the Computer is — the defining composition lifecycle.

Core · 5

Model all four cardinalities for a Library system

Goal: turn cardinality questions into the right data structures. (Notes §12, Code §Cardinality)

  1. Decide and implement: Member→LibraryCard (1:1), Member→Fine (1:M), Member→Branch (M:1), Member↔Book via Loan (M:M).
  2. Use a single field for 1:1 / M:1, a list for 1:M, and a Loan join class for M:M.
  3. Print one line per relationship proving it works.
Predict then verify: Which relationship forces an extra class, and why can't it be a plain field/list on either side?
Solution
LibrarySystem.javarun: javac LibrarySystem.java && java LibrarySystem
import java.util.*;

class LibraryCard { String id; LibraryCard(String i){ id = i; } }
class Fine { double amt; Fine(double a){ amt = a; } }
class Branch { String name; Branch(String n){ name = n; } }
class Book { String title; Book(String t){ title = t; } }

class Member {
    String name;
    LibraryCard card;                       // 1:1
    List<Fine> fines = new ArrayList<>();    // 1:M
    Branch branch;                          // M:1
    Member(String n){ name = n; }
}

class Loan {                                // M:M join (+ link data)
    Member member; Book book; String due;
    Loan(Member m, Book b, String d){ member=m; book=b; due=d; }
}

public class LibrarySystem {
    public static void main(String[] args) {
        Member m = new Member("Sam");
        m.card = new LibraryCard("LC-1");            // 1:1
        m.fines.add(new Fine(2.50));                 // 1:M
        m.fines.add(new Fine(1.00));
        m.branch = new Branch("Downtown");           // M:1

        Book b = new Book("Clean Code");             // M:M via Loan
        List<Loan> loans = new ArrayList<>();
        loans.add(new Loan(m, b, "2026-07-01"));

        System.out.println("1:1 card   = " + m.card.id);
        System.out.println("1:M fines  = " + m.fines.size());
        System.out.println("M:1 branch = " + m.branch.name);
        System.out.println("M:M loan   = " + loans.get(0).book.title);
    }
}
Expected output
1:1 card   = LC-1
1:M fines  = 2
M:1 branch = Downtown
M:M loan   = Clean Code

The M:M (Member↔Book) forces the Loan class. If you tried plain lists on both sides you'd duplicate the link and have nowhere to store per-loan data (due date). A dedicated join class owns the relationship cleanly.

Challenge · 6

Mini machine-coding round: model a Parking Lot's structure

Goal: combine everything — visibility, the has-a family, and cardinality — into one small, compiling design, the way a machine-coding round expects. (Notes §3, §8–12)

  1. Model: a ParkingLot composes many ParkingSpots (spots die with the lot). A ParkingSpot may aggregate zero-or-one Vehicle (vehicles exist independently and come/go). A Vehicle has a 1:1 RegistrationPlate.
  2. Encapsulate fields (private) and expose only the methods you need: park(Vehicle), leave(), and a lot-level availableSpots().
  3. In main: create a lot with 3 spots, park 2 vehicles, print available spots, free one, print again.
  4. Annotate each relationship in a comment with its type and cardinality.
Predict then verify: Before coding, write down the relationship + cardinality for each of the three pairs. Then confirm your code matches: does the spot create the vehicle (composition) or receive it (aggregation)? It must receive it.
Solution
ParkingLot.javarun: javac ParkingLot.java && java ParkingLot
import java.util.*;

// 1:1  Vehicle --- RegistrationPlate
class RegistrationPlate {
    private final String number;
    RegistrationPlate(String number){ this.number = number; }
    String number(){ return number; }
}

class Vehicle {
    private final RegistrationPlate plate;   // 1:1 composition-ish (plate owned)
    Vehicle(String plateNo){ this.plate = new RegistrationPlate(plateNo); }
    String plate(){ return plate.number(); }
}

class ParkingSpot {
    private final int id;
    private Vehicle vehicle;                  // AGGREGATION, 0..1 (vehicle passed in)
    ParkingSpot(int id){ this.id = id; }
    boolean isFree(){ return vehicle == null; }
    boolean park(Vehicle v){                  // receive, never create -> aggregation
        if (!isFree()) return false;
        this.vehicle = v;
        return true;
    }
    void leave(){ this.vehicle = null; }      // vehicle survives; just unlinked
    int id(){ return id; }
}

public class ParkingLot {
    // COMPOSITION, 1:M  ParkingLot <>--- ParkingSpot (spots created inside)
    private final List<ParkingSpot> spots = new ArrayList<>();

    ParkingLot(int size){
        for (int i = 1; i <= size; i++) spots.add(new ParkingSpot(i)); // lot OWNS spots
    }
    ParkingSpot park(Vehicle v){
        for (ParkingSpot s : spots) if (s.park(v)) return s;
        return null;  // lot full
    }
    long availableSpots(){
        return spots.stream().filter(ParkingSpot::isFree).count();
    }

    public static void main(String[] args) {
        ParkingLot lot = new ParkingLot(3);
        Vehicle car1 = new Vehicle("KA-01-1111");   // vehicles exist independently
        Vehicle car2 = new Vehicle("KA-02-2222");

        ParkingSpot s1 = lot.park(car1);
        lot.park(car2);
        System.out.println("Parked " + car1.plate() + " at spot " + s1.id());
        System.out.println("Available: " + lot.availableSpots());

        s1.leave();   // car1 drives off; the Vehicle object still exists
        System.out.println("After " + car1.plate() + " leaves -> Available: " + lot.availableSpots());
    }
}
Expected output
Parked KA-01-1111 at spot 1
Available: 1
After KA-01-1111 leaves -> Available: 2

Relationship map: ParkingLot ◆── ParkingSpot (composition, 1:M — spots created in the lot's constructor, gone with it). ParkingSpot ◇── Vehicle (aggregation, 0..1 — vehicle is passed into park(), survives leave()). Vehicle ── RegistrationPlate (1:1). Fields are private; only park/leave/availableSpots are exposed (encapsulation).

Self-check rubric — how to know you've got it

  • Visibility: you can map + - # ~ to public/private/protected/package without looking.
  • Relationship instinct: given any two classes, you reach the right answer in <10s using the two questions (owner? dies with parent?).
  • Code tell: you can state the difference in code — composition = parent news the child; aggregation = child passed in; association = mutual references, no owner.
  • Cardinality → structure: you instantly map 1:1/M:1 → field, 1:M → list, M:M → join class, and can explain why M:M needs the extra class.
  • Interview readiness: you can explain aggregation vs composition out loud in one sentence each, with a clean example (Car/Passenger vs House/Room).
  • End-to-end: Exercise 6 compiles and runs first try, with every relationship correctly annotated.

★Key Takeaways