Low Level Design — OOP & UML
Object-oriented design in practice — turning requirements into classes, UML class diagrams, and class relationships: association, aggregation, composition & inheritance.
Assumes you know
After this you should be able to
- Turn requirements into collaborating classes
- Choose composition for replaceable parts
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+).
+ = my own enrichment or a correction beyond the original material.
Section I — Notebook Summary#
═══ 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.
═══ TYPES OF LLD INTERVIEWS ═══| Type | Companies | Format |
|---|---|---|
| Theoretical | Old MNCs/banks (Oracle) | 30–45m · 3–4 Qs · OOP/patterns theory · no/toy code |
| Design | Big MNCs (Amazon, PayPal, Walmart) | 45m · 1 Q + discussion · code a few classes · not end-to-end |
| Machine Coding | Flipkart, Myntra, Razorpay, startups | 2.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)═══ 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
═══ CLASS RELATIONSHIPS (the "has-a" family) ═══ Association ⊃ Aggregation ⊃ Composition (each a stricter special case)
| Relationship | Ownership? | Child lifecycle | UML arrow |
|---|---|---|---|
| Association | none | independent | plain line ── |
| Aggregation | owner exists, but loose | independent (child survives parent) | hollow diamond ◇── |
| Composition | strong owner | dies with parent | filled 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═══ CARDINALITY (how many ↔ how many) ═══
Definition: # of instances of one class that relate to instances of another.| Pair | Cardinality | Reasoning |
|---|---|---|
| User → Passport | 1 : 1 | 1 user has 1 passport; 1 passport → 1 user |
| User → CreditCard | 1 : M | 1 user many cards; 1 card → 1 user |
| User → School | M : 1 | 1 user joins 1 school; 1 school many users |
| User → Course | M : M | user many courses; course many users |
UML notation: 1, 0..1, *, 1..*, 0..* written at each end of the line.Section II — Detailed Study Notes#
1.Why Low-Level Design?#
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.
+ 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#
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.
+ 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#
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.
| Dimension | Theoretical | Design | Machine Coding |
|---|---|---|---|
| Companies | Old MNCs, banks (Oracle) | Amazon, PayPal, Walmart | Flipkart, Myntra, Razorpay, startups |
| Focus | Knowledge of OOP/patterns | Problem solving, case study | Can you code/implement |
| Duration | 30–45 min | ~45 min | ~2.5 hours |
| Code expected | None / toy | A few classes | Full working code |
| # Questions | 3–4 | 1 + discussion | 1 requirement set |
+ 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#
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.
- Covered here: the four pillars (recap) and the three has-a relationships + cardinality.
- Covered later: SOLID principles and design patterns.
+ 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 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.
| Pillar | One-line meaning | Goal |
|---|---|---|
| Encapsulation | Bundle data+behavior, hide fields | Reduce complexity + data security |
| Abstraction | Expose what, hide how | Hide complexity + isolate change impact |
| Inheritance | Reuse via "is-a" | Eliminate redundancy + reuse |
| Polymorphism | One interface, many behaviors | Flexibility / extensibility |
+ 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#
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.
| Words | Diagrams (UML) | |
|---|---|---|
| Ambiguity | High — "I didn't mean that" | Low — standardized notation |
| Complex scenarios | Hard to explain | Easy to visualize |
| Shared understanding | Depends on phrasing | Common visual vocabulary |
+ 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#
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 │
└──────────────────────────────┘| Symbol | Visibility | Meaning |
|---|---|---|
+ | public | Accessible everywhere |
- | private | Within the class only |
# | protected | Class + subclasses |
~ | package | Same 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.
8.Class Relationships — the "Has-A" Family#
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)+ 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 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.
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 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.
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.
11.Composition#
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.
+ 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.
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)| Trait | Association | Aggregation | Composition |
|---|---|---|---|
| Ownership | None | Loose owner | Strong owner |
| Child lifecycle | Independent | Independent (survives) | Dies with parent |
| Child shared? | n/a | Belongs to one parent | Belongs to one parent |
| UML symbol | ── line | ◇── hollow diamond | ◆── filled diamond |
| Typical code | holds a reference | reference passed in | parent news child |
| Example | Teacher/Student | Car/Passenger | House/Room |
12.Cardinality#
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:
| Pair | Q: A → how many B? | Q: B → how many A? | Cardinality |
|---|---|---|---|
| User / Passport | 1 passport | 1 user | 1 : 1 |
| User / CreditCard | many cards | 1 user | 1 : M |
| User / School | 1 school | many users | M : 1 |
| User / Course | many courses | many users | M : 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.
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.
Section III — Code Samples#
§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.
// 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));
}
}Circle area = 12.57 Rectangle area = 12.00 max(int) = 7 max(double) = 9.1
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.
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());
}
}Account ****42 balance = 750.0
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.
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);
}
}Sam is taught by: Alice Bob After Sam is gone, Alice still exists: Alice
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.
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.");
}
}Computer Science has 2 profs Dept gone, but Dr. Rao still exists.
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.
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.");
}
}House has 3 rooms. House deleted -> its rooms are gone too.
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.
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);
}
}1:1 passport = P123 1:M #cards = 2 M:1 school = MIT M:M enrolled in = Algorithms
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#
# 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.Identify the visibility markers#
Goal: cement the UML ↔ Java visibility mapping. (Notes §7)
- Write a class
Profilewith: aprivatefieldemail, aprotectedfieldid, a package-private fieldnickname, and apublicgettergetEmail(). - Next to each member, write a comment with its UML symbol.
- In
main, create oneProfileand printgetEmail().
Solution
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());
}
}sam@x.com
Answer: Only getEmail() (public). From a different package and not a subclass, protected, package-private, and private are all inaccessible.
Classify five relationships#
Goal: snap-judge association vs aggregation vs composition. (Notes §8–11)
- 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
Solution
| Pair | Relationship | Why |
|---|---|---|
| (a) Library / Book | Aggregation | Library owns books, but a book survives if the library closes |
| (b) Human / Heart | Composition | Heart has no independent life; dies with the human |
| (c) Teacher / Student | Association | No ownership; both fully independent |
| (d) Playlist / Song | Aggregation | Owned by the playlist, but songs exist elsewhere too |
| (e) Order / OrderLineItem | Composition | A 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.
Code an aggregation that proves the child survives#
Goal: demonstrate, in code, that an aggregated child outlives its parent. (Notes §10, Code §Aggregation)
- Create
Player(name) andTeam(name + a list ofPlayerpassed into the constructor). - Build two players, put them in a team.
- Set the team reference to
null, then print a player's name to show it still exists.
team = null, will printing p1.name throw, print blank, or print the name? Why?Solution
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);
}
}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.
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)
- Create a
Computerthat builds its ownCPUandRAMinside its constructor. - Do not expose the parts via getters that hand out the objects.
- Print a one-line spec built from the internal parts.
Solution
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.");
}
}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.
Model all four cardinalities for a Library system#
Goal: turn cardinality questions into the right data structures. (Notes §12, Code §Cardinality)
- Decide and implement: Member→LibraryCard (1:1), Member→Fine (1:M), Member→Branch (M:1), Member↔Book via Loan (M:M).
- Use a single field for 1:1 / M:1, a list for 1:M, and a
Loanjoin class for M:M. - Print one line per relationship proving it works.
Solution
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);
}
}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.
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)
- Model: a
ParkingLotcomposes manyParkingSpots (spots die with the lot). AParkingSpotmay aggregate zero-or-oneVehicle(vehicles exist independently and come/go). AVehiclehas a 1:1RegistrationPlate. - Encapsulate fields (private) and expose only the methods you need:
park(Vehicle),leave(), and a lot-levelavailableSpots(). - In
main: create a lot with 3 spots, park 2 vehicles, print available spots, free one, print again. - Annotate each relationship in a comment with its type and cardinality.
Solution
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());
}
}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#
- LLD = blueprint before code. Detailed planning first beats jumping in or staying high-level — clear roadmap, aligned team, fewer errors.
- Three interview formats: Theoretical (theory, no code), Design (a few classes + discussion), Machine Coding (full working code). Know which you're facing.
- Four OOP pillars by goal: Encapsulation (reduce complexity + security), Abstraction (hide complexity + isolate change), Inheritance (reuse), Polymorphism (one form, many behaviors).
- Represent designs with diagrams, standardized by UML. Words are ambiguous; the class diagram is the LLD workhorse — one box, three compartments,
+ - # ~visibility. - Has-a family is a spectrum: Association (no owner) ⊃ Aggregation (owner, child survives) ⊃ Composition (child dies with parent). UML: line / ◇ / ◆.
- The one test that decides it all: "If I delete the parent, must the child die?" Yes → composition; No-but-owned → aggregation; No-owner → association.
- Cardinality drives structure: 1:1 and M:1 → a field; 1:M → a list; M:M → a join class. Always state your read direction.