Episode 9 — System Design / 9.1 — LLD Foundations

9.1.c — Class Relationships

In one sentence: Class relationships define how objects know about, use, contain, or inherit from each other — understanding these distinctions (association, aggregation, composition, dependency, inheritance, implementation) is the backbone of every LLD class diagram.


Table of Contents


1. Overview of Relationship Types

┌─────────────────────────────────────────────────────────────────┐
│             CLASS RELATIONSHIP SPECTRUM                          │
│                                                                 │
│  Weakest coupling ◄────────────────────────► Strongest coupling │
│                                                                 │
│  Dependency    Association    Aggregation    Composition         │
│  ┄┄┄┄┄┄►      ──────────    ──────◇────    ──────◆────         │
│  "uses"        "knows"       "has-a"        "owns-a"            │
│  temporarily   references    (weak whole-   (strong whole-      │
│                              part)          part, same          │
│                                             lifetime)           │
│                                                                 │
│  Inheritance (IS-A)          Implementation (CAN-DO)            │
│  ──────────────▷             ┄┄┄┄┄┄┄┄┄┄┄▷                      │
│  "is a type of"              "fulfills contract of"             │
└─────────────────────────────────────────────────────────────────┘

2. Association

Definition: A relationship where one class knows about another. Both objects exist independently. Neither controls the other's lifecycle.

Real-world analogy: A Teacher and a Student — they know about each other, but if the teacher leaves, the student still exists (and vice versa).

  ┌──────────┐         teaches         ┌──────────┐
  │ Teacher  │ ────────────────────── │ Student  │
  └──────────┘                         └──────────┘

  Both exist independently. Removing one does NOT destroy the other.

Code Example

class Student {
  private name: string;
  private studentId: string;

  constructor(name: string, studentId: string) {
    this.name = name;
    this.studentId = studentId;
  }

  getName(): string { return this.name; }
  getStudentId(): string { return this.studentId; }
}

class Teacher {
  private name: string;
  private students: Student[];  // Teacher KNOWS ABOUT students

  constructor(name: string) {
    this.name = name;
    this.students = [];
  }

  // Students are passed in — Teacher does NOT create them
  addStudent(student: Student): void {
    this.students.push(student);
  }

  removeStudent(studentId: string): void {
    this.students = this.students.filter(s => s.getStudentId() !== studentId);
    // Note: student still exists even after being removed from this teacher
  }

  listStudents(): string[] {
    return this.students.map(s => s.getName());
  }
}

// Both created independently
const alice = new Student("Alice", "S001");
const bob = new Student("Bob", "S002");
const mrSmith = new Teacher("Mr. Smith");

mrSmith.addStudent(alice);
mrSmith.addStudent(bob);

mrSmith.removeStudent("S001");
// alice still exists — it was not destroyed
console.log(alice.getName()); // "Alice"

Key Characteristics of Association

PropertyValue
Lifecycle couplingNone — both objects exist independently
DirectionCan be unidirectional or bidirectional
StrengthWeak
UML notationSolid line with no diamond
Think"knows about" / "is connected to"

3. Aggregation

Definition: A special form of association where one class is the whole and another is a part, but the part can exist independently of the whole. It is a "has-a" with weak ownership.

Real-world analogy: A Department has Employees — if the department is dissolved, the employees still exist (they can join other departments).

  ┌────────────┐        has         ┌────────────┐
  │ Department │ ◇──────────────── │  Employee  │
  └────────────┘                    └────────────┘
        whole        (hollow         part
                      diamond)
  
  If Department is destroyed, Employees SURVIVE.

Code Example

class Employee {
  private name: string;
  private employeeId: string;

  constructor(name: string, employeeId: string) {
    this.name = name;
    this.employeeId = employeeId;
  }

  getName(): string { return this.name; }
  getEmployeeId(): string { return this.employeeId; }
}

class Department {
  private name: string;
  private employees: Employee[];  // Department HAS employees (aggregation)

  constructor(name: string) {
    this.name = name;
    this.employees = [];
  }

  // Employees are PASSED IN — not created by the Department
  addEmployee(employee: Employee): void {
    this.employees.push(employee);
  }

  removeEmployee(employeeId: string): Employee | undefined {
    const index = this.employees.findIndex(
      e => e.getEmployeeId() === employeeId
    );
    if (index === -1) return undefined;
    return this.employees.splice(index, 1)[0];
    // Employee is returned — it still exists
  }

  getEmployeeCount(): number {
    return this.employees.length;
  }
}

// Employees exist independently of departments
const alice = new Employee("Alice", "E001");
const bob = new Employee("Bob", "E002");

const engineering = new Department("Engineering");
engineering.addEmployee(alice);
engineering.addEmployee(bob);

// Department is "destroyed" — employees survive
const removedAlice = engineering.removeEmployee("E001");
console.log(removedAlice?.getName()); // "Alice" — she still exists

// alice can join another department
const marketing = new Department("Marketing");
marketing.addEmployee(alice);

Key Characteristics of Aggregation

PropertyValue
Lifecycle couplingWeak — parts survive the whole's destruction
OwnershipWeak — whole references parts but does not create/destroy them
DirectionWhole → Part
UML notationSolid line with hollow diamond (◇) on the whole side
Think"has-a" where parts can exist alone

4. Composition

Definition: A strong form of aggregation where the whole owns the part and the part cannot exist without the whole. If the whole is destroyed, all its parts are destroyed too.

Real-world analogy: A House has Rooms — if the house is demolished, the rooms cease to exist. You cannot have a floating room without a house.

  ┌────────────┐        owns        ┌────────────┐
  │   House    │ ◆──────────────── │    Room    │
  └────────────┘                    └────────────┘
        whole        (filled         part
                      diamond)

  If House is destroyed, ALL Rooms are destroyed with it.

Code Example

class Room {
  private name: string;
  private area: number; // in square meters

  constructor(name: string, area: number) {
    this.name = name;
    this.area = area;
  }

  getName(): string { return this.name; }
  getArea(): number { return this.area; }
}

class House {
  private address: string;
  private rooms: Room[];  // House OWNS rooms (composition)

  constructor(address: string) {
    this.address = address;
    this.rooms = [];
  }

  // Rooms are CREATED by the House — not passed in from outside
  addRoom(name: string, area: number): void {
    const room = new Room(name, area); // House creates the room
    this.rooms.push(room);
  }

  getTotalArea(): number {
    return this.rooms.reduce((sum, room) => sum + room.getArea(), 0);
  }

  getRoomCount(): number {
    return this.rooms.length;
  }

  // When House is destroyed (garbage collected), all rooms go with it.
  // There is no method to "extract" a room and use it elsewhere.
}

const myHouse = new House("123 Main St");
myHouse.addRoom("Living Room", 30);
myHouse.addRoom("Bedroom", 20);
myHouse.addRoom("Kitchen", 15);

console.log(myHouse.getTotalArea()); // 65
console.log(myHouse.getRoomCount()); // 3

// If myHouse is set to null / garbage collected, all rooms are gone
// There is no way to access those Room objects independently

Another Example — Order and OrderLineItem

class OrderLineItem {
  private productId: string;
  private productName: string;
  private quantity: number;
  private unitPrice: number;

  constructor(productId: string, productName: string, quantity: number, unitPrice: number) {
    this.productId = productId;
    this.productName = productName;
    this.quantity = quantity;
    this.unitPrice = unitPrice;
  }

  getSubtotal(): number {
    return this.quantity * this.unitPrice;
  }
}

class Order {
  private orderId: string;
  private lineItems: OrderLineItem[];  // Composition — items belong to this order
  private createdAt: Date;

  constructor(orderId: string) {
    this.orderId = orderId;
    this.lineItems = [];
    this.createdAt = new Date();
  }

  // Order CREATES the line items internally
  addItem(productId: string, productName: string, quantity: number, unitPrice: number): void {
    const item = new OrderLineItem(productId, productName, quantity, unitPrice);
    this.lineItems.push(item);
  }

  getTotal(): number {
    return this.lineItems.reduce((sum, item) => sum + item.getSubtotal(), 0);
  }

  getItemCount(): number {
    return this.lineItems.length;
  }

  // If the Order is deleted, all OrderLineItems are deleted.
  // An OrderLineItem has NO meaning outside its Order.
}

Key Characteristics of Composition

PropertyValue
Lifecycle couplingStrong — parts die with the whole
OwnershipStrong — whole creates and destroys parts
DirectionWhole → Part (exclusive)
UML notationSolid line with filled diamond (◆) on the whole side
Think"owns-a" where parts cannot exist alone

5. Aggregation vs Composition — Deep Comparison

This is one of the most commonly confused distinctions in LLD interviews.

DimensionAggregation (◇)Composition (◆)
LifecyclePart survives when whole is destroyedPart is destroyed when whole is destroyed
OwnershipWeak — whole references partsStrong — whole owns parts exclusively
CreationParts are typically created externally and passed inParts are typically created by the whole
SharingA part can belong to multiple wholesA part belongs to exactly one whole
Real-worldDepartment ◇── EmployeeHouse ◆── Room
Real-worldPlaylist ◇── SongOrder ◆── OrderLineItem
Real-worldUniversity ◇── ProfessorHuman ◆── Heart
Code signalConstructor receives objects / add(existingObj)Constructor/method creates objects internally
Destructionremove() returns the part (still alive)delete whole = parts gone (no return)

The Litmus Test

Ask yourself two questions:

1. Can the PART exist without the WHOLE?
   YES → Aggregation    (Employee without Department = still a person)
   NO  → Composition    (Room without House = meaningless)

2. Can the PART belong to MULTIPLE wholes at the same time?
   YES → Aggregation    (A Song can be in multiple Playlists)
   NO  → Composition    (An OrderLineItem belongs to ONE Order)

Side-by-Side Code Pattern

// AGGREGATION pattern — parts passed in from outside
class Playlist {
  private songs: Song[] = [];

  addSong(song: Song): void {    // Song created elsewhere, passed in
    this.songs.push(song);
  }

  removeSong(songId: string): Song | undefined {
    const idx = this.songs.findIndex(s => s.getId() === songId);
    if (idx === -1) return undefined;
    return this.songs.splice(idx, 1)[0]; // Song returned — still alive
  }
}

// COMPOSITION pattern — parts created internally
class Invoice {
  private lineItems: InvoiceLineItem[] = [];

  addLineItem(description: string, amount: number): void {
    // InvoiceLineItem created HERE by Invoice — not passed in
    const item = new InvoiceLineItem(description, amount);
    this.lineItems.push(item);
  }

  // No method to "extract" a line item for use elsewhere
  // When Invoice is gone, line items are gone
}

6. Dependency

Definition: The weakest relationship. Class A uses Class B temporarily (in a method parameter, local variable, or return type) but does NOT hold a persistent reference to it.

Real-world analogy: You use a taxi to get somewhere. You do not own the taxi, you do not keep a reference to it. The taxi exists before you call it and after you leave.

  ┌────────────┐                    ┌────────────┐
  │   Order    │ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄► │   Logger   │
  └────────────┘     uses           └────────────┘
                  (dashed arrow)

  Order USES Logger temporarily — does not store a reference to it.

Code Example

class Logger {
  static log(message: string): void {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}

class EmailService {
  static sendEmail(to: string, subject: string, body: string): boolean {
    console.log(`Sending email to ${to}: ${subject}`);
    return true;
  }
}

class OrderProcessor {
  // OrderProcessor DEPENDS on Logger and EmailService
  // but does NOT hold a reference to them as properties

  processOrder(order: Order): void {
    // Logger used as a local dependency — not a field
    Logger.log(`Processing order ${order.getId()}`);

    // Validate
    if (!this.validateOrder(order)) {
      Logger.log(`Order ${order.getId()} validation failed`);
      return;
    }

    // Process payment
    this.chargePayment(order);

    // EmailService used temporarily
    EmailService.sendEmail(
      order.getCustomerEmail(),
      "Order Confirmed",
      `Your order ${order.getId()} has been confirmed.`
    );

    Logger.log(`Order ${order.getId()} processed successfully`);
  }

  private validateOrder(order: Order): boolean {
    return order.getItemCount() > 0;
  }

  private chargePayment(order: Order): void {
    // payment logic
  }
}

// OrderProcessor does NOT store Logger or EmailService as fields
// It uses them temporarily within methods — that is dependency

Dependency vs Association

AspectDependency (┄┄►)Association (────)
DurationTemporary (within a method call)Persistent (stored as a field)
StorageNo field referenceHas a field reference
CouplingVery weakWeak to moderate
Code signalMethod parameter, local variable, static callClass property / field

7. Inheritance (IS-A)

Definition: A child class inherits all properties and methods from a parent class and can add or override them. This represents the "is-a" relationship.

         ┌────────────────┐
         │    Vehicle      │
         │ ────────────── │
         │ speed: number  │
         │ fuel: number   │
         │ ────────────── │
         │ accelerate()   │
         │ brake()        │
         └───────┬────────┘
                 │ extends
        ┌────────┴────────┐
        │                 │
  ┌─────▼──────┐    ┌────▼────────┐
  │    Car     │    │  Motorcycle │
  │ ────────── │    │ ─────────── │
  │ doors: 4   │    │ sidecar: no│
  │ ────────── │    │ ─────────── │
  │ openTrunk()│    │ wheelie()  │
  └────────────┘    └─────────────┘

  Car IS-A Vehicle. Motorcycle IS-A Vehicle.
  Both inherit speed, fuel, accelerate(), brake().

Code Example

class Vehicle {
  protected speed: number = 0;
  protected fuel: number = 100;
  protected brand: string;

  constructor(brand: string) {
    this.brand = brand;
  }

  accelerate(amount: number): void {
    if (this.fuel <= 0) {
      console.log("No fuel!");
      return;
    }
    this.speed += amount;
    this.fuel -= amount * 0.1;
  }

  brake(amount: number): void {
    this.speed = Math.max(0, this.speed - amount);
  }

  getInfo(): string {
    return `${this.brand} — Speed: ${this.speed}, Fuel: ${this.fuel.toFixed(1)}`;
  }
}

class Car extends Vehicle {
  private doors: number;

  constructor(brand: string, doors: number = 4) {
    super(brand);
    this.doors = doors;
  }

  openTrunk(): void {
    console.log(`${this.brand} trunk opened`);
  }

  // Override parent method for car-specific behavior
  accelerate(amount: number): void {
    const limited = Math.min(amount, 200 - this.speed); // Car speed limit
    super.accelerate(limited);
  }
}

class Motorcycle extends Vehicle {
  constructor(brand: string) {
    super(brand);
  }

  wheelie(): void {
    if (this.speed > 30) {
      console.log(`${this.brand} does a wheelie!`);
    } else {
      console.log("Need more speed for a wheelie");
    }
  }
}

const myCar = new Car("Toyota", 4);
myCar.accelerate(80);          // Inherited + overridden method
myCar.openTrunk();              // Car-specific method
console.log(myCar.getInfo());   // Inherited method

const myBike = new Motorcycle("Ducati");
myBike.accelerate(50);          // Inherited method
myBike.wheelie();               // Motorcycle-specific method

UML Notation for Inheritance

  ┌──────────┐
  │  Parent  │
  └─────▲────┘
        │        (solid line + hollow triangle arrow)
        │        pointing FROM child TO parent
  ┌─────┴────┐
  │  Child   │
  └──────────┘

8. Implementation (Interface Realization)

Definition: A class promises to provide the behavior defined by an interface. The class must implement all methods declared in the interface.

  ┌──────────────────┐
  │  <<interface>>   │
  │    Printable     │
  │ ──────────────── │
  │ print(): void    │
  └────────▲─────────┘
           ┆         (dashed line + hollow triangle arrow)
           ┆         pointing FROM class TO interface
  ┌────────┴─────────┐
  │     Invoice      │
  │ ──────────────── │
  │ print(): void    │  ← must implement this
  └──────────────────┘

Code Example

// Interface — the contract
interface Searchable {
  search(query: string): SearchResult[];
  indexDocument(doc: Document): void;
  removeFromIndex(docId: string): void;
}

interface Cacheable {
  getFromCache(key: string): unknown | null;
  setInCache(key: string, value: unknown, ttlSeconds: number): void;
  invalidateCache(key: string): void;
}

// A class can implement MULTIPLE interfaces
class ProductCatalog implements Searchable, Cacheable {
  private products: Map<string, Product> = new Map();
  private cache: Map<string, { value: unknown; expiresAt: number }> = new Map();
  private searchIndex: Map<string, string[]> = new Map();

  // Must implement ALL Searchable methods
  search(query: string): SearchResult[] {
    const matchingIds = this.searchIndex.get(query.toLowerCase()) ?? [];
    return matchingIds
      .map(id => this.products.get(id))
      .filter(Boolean)
      .map(p => ({ id: p!.id, title: p!.title, score: 1.0 }));
  }

  indexDocument(doc: Document): void {
    const words = doc.content.toLowerCase().split(/\s+/);
    words.forEach(word => {
      const existing = this.searchIndex.get(word) ?? [];
      existing.push(doc.id);
      this.searchIndex.set(word, existing);
    });
  }

  removeFromIndex(docId: string): void {
    this.searchIndex.forEach((ids, key) => {
      this.searchIndex.set(key, ids.filter(id => id !== docId));
    });
  }

  // Must implement ALL Cacheable methods
  getFromCache(key: string): unknown | null {
    const entry = this.cache.get(key);
    if (!entry) return null;
    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key);
      return null;
    }
    return entry.value;
  }

  setInCache(key: string, value: unknown, ttlSeconds: number): void {
    this.cache.set(key, {
      value,
      expiresAt: Date.now() + ttlSeconds * 1000,
    });
  }

  invalidateCache(key: string): void {
    this.cache.delete(key);
  }
}

Inheritance vs Implementation

AspectInheritance (extends)Implementation (implements)
Keywordextendsimplements
What is inheritedProperties + methods + implementationOnly the contract (method signatures)
CountSingle parent only (in most languages)Multiple interfaces allowed
UML arrowSolid line + hollow triangle (▷)Dashed line + hollow triangle (▷)
PurposeCode reuse + specializationContract compliance + polymorphism
Think"is a type of""can do / fulfills the contract of"

9. Multiplicity (Cardinality)

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

Common Multiplicity Notations

NotationMeaningExample
1Exactly oneA Person has exactly 1 Passport
0..1Zero or one (optional)A Student has 0 or 1 Advisor
* or 0..*Zero or moreA Customer has 0 or more Orders
1..*One or moreAn Order has at least 1 LineItem
m..nBetween m and nA Car has 3..5 Passengers

One-to-One (1:1)

  ┌──────────┐    1        1    ┌──────────┐
  │  Person  │ ──────────────── │ Passport │
  └──────────┘                  └──────────┘

  Each Person has exactly one Passport.
  Each Passport belongs to exactly one Person.
class Passport {
  private passportNumber: string;
  private expiryDate: Date;

  constructor(passportNumber: string, expiryDate: Date) {
    this.passportNumber = passportNumber;
    this.expiryDate = expiryDate;
  }

  getNumber(): string { return this.passportNumber; }
}

class Person {
  private name: string;
  private passport: Passport;  // Exactly one passport

  constructor(name: string, passport: Passport) {
    this.name = name;
    this.passport = passport;
  }

  getPassportNumber(): string {
    return this.passport.getNumber();
  }
}

One-to-Many (1:N)

  ┌──────────┐    1        *    ┌──────────┐
  │ Customer │ ──────────────── │  Order   │
  └──────────┘                  └──────────┘

  One Customer can have many Orders.
  Each Order belongs to exactly one Customer.
class Order {
  private orderId: string;
  private total: number;

  constructor(orderId: string, total: number) {
    this.orderId = orderId;
    this.total = total;
  }

  getId(): string { return this.orderId; }
}

class Customer {
  private name: string;
  private orders: Order[];  // Zero or more orders

  constructor(name: string) {
    this.name = name;
    this.orders = [];
  }

  placeOrder(order: Order): void {
    this.orders.push(order);
  }

  getOrderCount(): number {
    return this.orders.length;
  }

  getTotalSpent(): number {
    return this.orders.reduce((sum, o) => sum + o.getTotal(), 0);
  }
}

Many-to-Many (M:N)

  ┌──────────┐    *        *    ┌──────────┐
  │ Student  │ ──────────────── │  Course  │
  └──────────┘                  └──────────┘

  A Student can enroll in many Courses.
  A Course can have many Students.

Many-to-many relationships are often resolved with a junction/association class:

  ┌──────────┐    1    *  ┌─────────────┐  *    1  ┌──────────┐
  │ Student  │ ────────── │ Enrollment  │ ────────── │  Course  │
  └──────────┘            │             │            └──────────┘
                          │ enrollDate  │
                          │ grade       │
                          └─────────────┘
class Course {
  private courseId: string;
  private title: string;

  constructor(courseId: string, title: string) {
    this.courseId = courseId;
    this.title = title;
  }

  getId(): string { return this.courseId; }
  getTitle(): string { return this.title; }
}

class Student {
  private studentId: string;
  private name: string;

  constructor(studentId: string, name: string) {
    this.studentId = studentId;
    this.name = name;
  }

  getId(): string { return this.studentId; }
  getName(): string { return this.name; }
}

// Junction class resolves the many-to-many relationship
class Enrollment {
  private student: Student;
  private course: Course;
  private enrollDate: Date;
  private grade: string | null;

  constructor(student: Student, course: Course) {
    this.student = student;
    this.course = course;
    this.enrollDate = new Date();
    this.grade = null;
  }

  assignGrade(grade: string): void {
    this.grade = grade;
  }

  getStudent(): Student { return this.student; }
  getCourse(): Course { return this.course; }
  getGrade(): string | null { return this.grade; }
}

// University manages enrollments
class University {
  private enrollments: Enrollment[] = [];

  enroll(student: Student, course: Course): Enrollment {
    const enrollment = new Enrollment(student, course);
    this.enrollments.push(enrollment);
    return enrollment;
  }

  getStudentsInCourse(courseId: string): Student[] {
    return this.enrollments
      .filter(e => e.getCourse().getId() === courseId)
      .map(e => e.getStudent());
  }

  getCoursesForStudent(studentId: string): Course[] {
    return this.enrollments
      .filter(e => e.getStudent().getId() === studentId)
      .map(e => e.getCourse());
  }
}

10. Master Comparison Table

RelationshipSymbolDirectionLifecycle Tied?StrengthCode Signal
Dependency┄┄┄►A uses BNoWeakestMethod parameter, local variable, static call
Association─────A knows BNoWeakField reference, both exist independently
Aggregation◇────A has BNo (part survives)MediumField reference, parts passed in from outside
Composition◆────A owns BYes (part dies)StrongField reference, parts created internally
Inheritance──▷B is-a AN/AStrongextends keyword
Implementation┄┄▷B can-do AN/AMediumimplements keyword

Quick Decision Flowchart

Does Class A reference Class B at all?
├── NO → No relationship (or possibly Dependency if used in a method)
└── YES
    ├── Is it stored as a FIELD?
    │   ├── NO → DEPENDENCY (temporary use)
    │   └── YES
    │       ├── Does A CREATE B internally?
    │       │   ├── YES → Does B make sense without A?
    │       │   │   ├── NO → COMPOSITION (◆)
    │       │   │   └── YES → AGGREGATION (◇)
    │       │   └── NO (B passed in) → ASSOCIATION or AGGREGATION
    │       │       └── Is A a "whole" and B a "part"?
    │       │           ├── YES → AGGREGATION (◇)
    │       │           └── NO → ASSOCIATION
    ├── Does B extend A? → INHERITANCE
    └── Does B implement A? → IMPLEMENTATION

11. Real-World Example — E-Commerce System

┌─────────────────────────────────────────────────────────────────────┐
│                   E-COMMERCE CLASS RELATIONSHIPS                    │
│                                                                     │
│  ┌───────────────┐                                                  │
│  │ <<interface>> │                                                  │
│  │ PaymentMethod │◄┄┄┄┄┄┄┄┄┄┄┄┄┐                                  │
│  └───────────────┘              ┆ implements                        │
│                          ┌──────┴──────┐                            │
│                          │ CreditCard  │                            │
│                          │   PayPal    │                            │
│                          │   Crypto    │                            │
│                          └─────────────┘                            │
│                                                                     │
│  ┌──────────┐  1     *  ┌──────────┐  ◆  1..*  ┌───────────────┐  │
│  │ Customer │──────────│  Order   │◆─────────│ OrderLineItem │  │
│  └──────────┘           └────┬─────┘            └───────────────┘  │
│                              │                                      │
│                              │ ┄┄┄► uses                           │
│                              │                                      │
│                         ┌────▼──────────┐                           │
│                         │ PaymentMethod │ (dependency — used in     │
│                         └───────────────┘  processPayment method)   │
│                                                                     │
│  ┌──────────┐  ◇  *     ┌──────────┐                               │
│  │ Category │◇──────────│ Product  │  (aggregation — product       │
│  └──────────┘            └──────────┘   can exist without category) │
│                                                                     │
│  ┌──────────┐                                                       │
│  │  Product │──▷ BaseEntity  (inheritance — Product IS-A            │
│  │ Customer │──▷ BaseEntity   BaseEntity with id, createdAt)       │
│  │  Order   │──▷ BaseEntity                                        │
│  └──────────┘                                                       │
└─────────────────────────────────────────────────────────────────────┘

12. Key Takeaways

  1. Association = "knows about" — both objects exist independently. The weakest stored-reference relationship.
  2. Aggregation = "has-a" with weak ownership — parts survive the whole's destruction. Parts can belong to multiple wholes.
  3. Composition = "owns-a" with strong ownership — parts are destroyed with the whole. Parts belong to exactly one whole.
  4. Dependency = "uses temporarily" — no persistent reference. The weakest relationship overall.
  5. Inheritance = "is-a" — child reuses parent's code and can override behavior. Use sparingly; prefer composition for flexibility.
  6. Implementation = "can-do" — a class promises to fulfill an interface contract. Enables polymorphism with multiple interfaces.
  7. Multiplicity describes how many instances relate (1:1, 1:N, M:N). Many-to-many is often resolved with a junction class.
  8. The aggregation vs composition litmus test: Can the part exist without the whole? If no, it is composition.

Explain-It Challenge

Draw on paper (or describe verbally) the class relationships for a Music Streaming App: User, Playlist, Song, Artist, Album, Subscription. Label each relationship as association, aggregation, composition, or inheritance. Then justify each choice — "Why composition here and not aggregation?"


Previous → 9.1.b — OOP Fundamentals Next → 9.1.d — UML Diagrams