OOP Interview Prep
Object Relationships

Composition

When Objects Cannot Exist Without Their Owner

LinkedIn Hook

Most developers know "Has-a" means composition.

What they get wrong in interviews: they think aggregation and composition are the same thing.

They're not. The critical difference is lifecycle.

In aggregation, the parts can survive without the whole. A Professor exists even after a Department dissolves.

In composition, the parts cannot. Destroy the House, and every Room inside it is gone. The Room has no identity, no meaning, no existence outside that specific House.

That ownership, that lifecycle dependency, is what composition actually means. And interviewers ask about it specifically because it tests whether you understand object design at a structural level, not just a syntax level.

Lesson 6.3 walks through the House-Room model, four runnable JS examples, and every interview angle this topic produces.

Read the full lesson -> [link]

#OOP #JavaScript #SoftwareEngineering #InterviewPrep #ObjectRelationships


Composition thumbnail


What You'll Learn

  • What composition means as an object relationship and how it differs from aggregation at the lifecycle level
  • The "Has-a with ownership" test and when a dependent object loses meaning without its parent
  • How to implement composition correctly in JavaScript, including enforcing lifecycle dependency in code
  • Why destroying the parent must destroy all children in a true composition relationship
  • The four most common interview questions about composition and how to answer each one precisely

The Analogy — A House and Its Rooms

Picture a house. It has a living room, two bedrooms, and a kitchen. Those rooms are part of the house. They are built into the house. They are not rooms that exist somewhere independently and happen to be associated with this particular house. They were created when the house was built, and they will cease to exist when the house is demolished.

You can't move a room to a different house. You can't have a "spare room" floating in a parking lot, unattached to any building. The room has no independent existence. Its entire lifecycle is tied to the house that owns it.

That is composition. The child objects (rooms) are created by the parent (house), owned by the parent, and destroyed with the parent. No parent, no child.

[IMAGE: A house outline in neon green on a dark background (#0a0a1a). Inside the house: three labeled boxes — "Living Room", "Bedroom 1", "Bedroom 2". An arrow from the house to each room labeled "owns and creates". A second version on the right shows the house struck through in neon orange — and all three room boxes are also struck through. Caption: "Rooms cannot exist without their House."]

Compare this to a university department and its professors (aggregation). If the Economics Department dissolves, the professors don't disappear. They existed before the department hired them, and they'll exist after it closes. The department had a relationship with those professors, but it did not own their existence.

Composition = owned existence. Aggregation = borrowed reference.

[INTERNAL-LINK: aggregation definition and examples → Lesson 6.2: Aggregation]


What Is Composition?

Composition is a "Has-a" relationship where the parent object creates the child objects, owns them exclusively, and the child objects cannot exist independently of the parent.

Three properties define a true composition relationship:

  1. Creation: The parent creates the child. The child is not passed in from outside.
  2. Exclusive ownership: No other object holds a reference to that child. It belongs to one owner.
  3. Lifecycle dependency: When the parent is destroyed, the child is destroyed automatically. The child's lifespan is bounded by the parent's lifespan.

If any of these three properties is missing, you're looking at aggregation, not composition.

[CHART: Two-column comparison table — Left column: "Composition" with rows: Relationship = Has-a (strong), Lifecycle = Child dies with parent, Ownership = Parent creates and owns child, Independence = Child cannot exist alone, Example = House-Room. Right column: "Aggregation" with rows: Relationship = Has-a (weak), Lifecycle = Child survives parent, Ownership = Parent holds a reference, Independence = Child can exist independently, Example = Department-Professor. Dark background, neon green headers for Composition, neon orange headers for Aggregation.]

[INTERNAL-LINK: has-a vs is-a relationships → Lesson 6.1: Association]


How to Implement Composition in JavaScript

JavaScript doesn't enforce composition at the language level. There's no final or owned keyword. You implement composition through design: the parent creates child instances internally, never accepts them as constructor parameters, and exposes a destroy() method that nullifies all child references.

The pattern has two non-negotiable rules:

  • Child objects are instantiated inside the parent constructor, not passed in.
  • The parent's destroy() method explicitly nullifies all child references, making them eligible for garbage collection.

Example 1 — House and Rooms

This is the canonical composition example. Rooms are created inside House, stored only in House, and removed when House is destroyed.

class Room {
  constructor(name) {
    this.name = name;
    this.active = true;
    console.log(`Room created: ${name}`);
  }

  describe() {
    return this.active ? `Room: ${this.name}` : `Room: ${this.name} [destroyed]`;
  }

  destroy() {
    this.active = false;
    console.log(`Room destroyed: ${this.name}`);
  }
}

class House {
  constructor(address) {
    this.address = address;
    // Composition: House creates its own Rooms
    // Rooms are not passed in from outside
    this.rooms = [
      new Room("Living Room"),
      new Room("Bedroom 1"),
      new Room("Bedroom 2"),
    ];
    console.log(`House built at: ${address}`);
  }

  listRooms() {
    this.rooms.forEach((room) => console.log(room.describe()));
  }

  // Destroying the House destroys every Room it owns
  destroy() {
    console.log(`Demolishing house at: ${this.address}`);
    this.rooms.forEach((room) => room.destroy());
    this.rooms = []; // Clear all references
    console.log("House demolished. All rooms are gone.");
  }
}

// Usage
const myHouse = new House("42 Maple Street");
myHouse.listRooms();

// Destroy the parent — all children go with it
myHouse.destroy();
myHouse.listRooms(); // No rooms remain

Output:

Room created: Living Room
Room created: Bedroom 1
Room created: Bedroom 2
House built at: 42 Maple Street
Room: Living Room
Room: Bedroom 1
Room: Bedroom 2
Demolishing house at: 42 Maple Street
Room destroyed: Living Room
Room destroyed: Bedroom 1
Room destroyed: Bedroom 2
House demolished. All rooms are gone.

The rooms exist only because the house exists. When the house is demolished, every room is explicitly torn down with it. This is lifecycle dependency in code.

[IMAGE: Code output visualization on dark background. Two states side by side. Left: "House ACTIVE" with three green Room boxes inside it, each labeled with a name. Right: "House DESTROYED" with three red Room boxes all marked "destroyed" and a label "rooms = []". Arrow between states labeled ".destroy()". Neon green and neon orange color scheme.]


Example 2 — Document and Pages

A document owns its pages. Pages do not float around independently waiting to be assigned to a document. When the document is deleted, its pages are gone.

class Page {
  constructor(pageNumber, content) {
    this.pageNumber = pageNumber;
    this.content = content;
    this.alive = true;
  }

  read() {
    if (!this.alive) {
      return `Page ${this.pageNumber} no longer exists.`;
    }
    return `Page ${this.pageNumber}: "${this.content}"`;
  }

  destroy() {
    this.alive = false;
    this.content = null;
  }
}

class Document {
  constructor(title, pageContents) {
    this.title = title;
    // Document creates its own Pages — composition
    this.pages = pageContents.map(
      (content, index) => new Page(index + 1, content)
    );
  }

  readAll() {
    console.log(`Document: ${this.title}`);
    this.pages.forEach((page) => console.log(page.read()));
  }

  // Deleting the Document deletes all Pages
  delete() {
    console.log(`Deleting document: "${this.title}"`);
    this.pages.forEach((page) => page.destroy());
    this.pages = [];
    this.title = null;
    console.log("Document deleted. All pages are gone.");
  }
}

// Usage
const report = new Document("Q4 Report", [
  "Executive summary here.",
  "Financial data here.",
  "Recommendations here.",
]);

report.readAll();
report.delete();
report.readAll(); // No pages remain

[PERSONAL EXPERIENCE]: The Document-Page model comes up constantly in real applications. Content management systems, PDF editors, note-taking apps — they all have this ownership structure. Getting the deletion logic right (nullifying child references, not just removing from an array) is the part developers miss in production code. Orphaned child objects that can't be garbage collected because one reference was missed are a real source of memory leaks.


Example 3 — Order and Order Items

An e-commerce order owns its line items. An OrderItem for "3x Blue Sneakers" was created specifically for this order. If the order is cancelled and deleted, those items have no meaning or existence outside of it.

class OrderItem {
  constructor(productName, quantity, unitPrice) {
    this.productName = productName;
    this.quantity = quantity;
    this.unitPrice = unitPrice;
    this.valid = true;
  }

  total() {
    return this.valid ? this.quantity * this.unitPrice : 0;
  }

  summary() {
    if (!this.valid) return "Item no longer exists.";
    return `${this.productName} x${this.quantity} @ $${this.unitPrice} = $${this.total()}`;
  }

  destroy() {
    this.valid = false;
    this.productName = null;
    this.quantity = 0;
    this.unitPrice = 0;
  }
}

class Order {
  constructor(orderId) {
    this.orderId = orderId;
    this.createdAt = new Date();
    // Items are created inside Order — composition
    this.items = [];
  }

  addItem(productName, quantity, unitPrice) {
    const item = new OrderItem(productName, quantity, unitPrice);
    this.items.push(item);
  }

  orderTotal() {
    return this.items.reduce((sum, item) => sum + item.total(), 0);
  }

  printReceipt() {
    console.log(`Order #${this.orderId}`);
    this.items.forEach((item) => console.log(item.summary()));
    console.log(`Total: $${this.orderTotal()}`);
  }

  // Cancel and delete the Order — all Items are destroyed with it
  cancel() {
    console.log(`Cancelling order #${this.orderId}`);
    this.items.forEach((item) => item.destroy());
    this.items = [];
    console.log(`Order #${this.orderId} cancelled. All items removed.`);
  }
}

// Usage
const order = new Order("ORD-1042");
order.addItem("Blue Sneakers", 2, 89.99);
order.addItem("White T-Shirt", 3, 24.99);
order.addItem("Black Belt", 1, 39.99);

order.printReceipt();
order.cancel();
order.printReceipt(); // No items remain, total = $0

[UNIQUE INSIGHT]: The Order-OrderItem relationship is one of the clearest real-world compositions you can point to in an interview. Notice what makes it composition and not aggregation: you would never create an OrderItem outside of an Order context and store it somewhere else. The item literally has no meaning without an order number attached. That "no meaning without the parent" test is the fastest way to identify a composition relationship in a domain model — faster than memorizing definitions.


Example 4 — Car and Engine

A car's engine is built into that car during manufacturing. It's not a portable unit you can detach, share with another car, and reattach. The engine was purpose-built for this car, and when the car is scrapped, the engine goes with it.

class Engine {
  constructor(type, horsepower) {
    this.type = type;
    this.horsepower = horsepower;
    this.running = false;
    this.intact = true;
  }

  start() {
    if (!this.intact) {
      console.log("Engine no longer exists.");
      return;
    }
    this.running = true;
    console.log(`${this.type} engine started (${this.horsepower}hp)`);
  }

  stop() {
    this.running = false;
    console.log("Engine stopped.");
  }

  destroy() {
    this.running = false;
    this.intact = false;
    console.log(`Engine destroyed.`);
  }

  status() {
    if (!this.intact) return "Engine: destroyed";
    return `Engine: ${this.type}, ${this.horsepower}hp, ${this.running ? "running" : "stopped"}`;
  }
}

class Car {
  constructor(make, model, engineType, horsepower) {
    this.make = make;
    this.model = model;
    // Engine is created inside Car — composition
    // You cannot construct a Car and pass in a pre-existing Engine
    this.engine = new Engine(engineType, horsepower);
    this.active = true;
    console.log(`Car built: ${make} ${model}`);
  }

  drive() {
    if (!this.active) {
      console.log("This car has been scrapped.");
      return;
    }
    this.engine.start();
    console.log(`${this.make} ${this.model} is driving.`);
  }

  status() {
    console.log(`Car: ${this.make} ${this.model} | ${this.engine.status()}`);
  }

  // Scrap the car — the engine is destroyed with it
  scrap() {
    console.log(`Scrapping car: ${this.make} ${this.model}`);
    this.engine.destroy(); // Engine cannot survive the car
    this.engine = null;
    this.active = false;
    console.log("Car scrapped.");
  }
}

// Usage
const car = new Car("Toyota", "Camry", "V6", 301);
car.drive();
car.status();

// Destroy the car — engine goes with it
car.scrap();
car.drive();   // "This car has been scrapped."
car.status();  // engine reference is null

[IMAGE: Two-panel diagram on dark background. Left panel: a Car box in neon green containing an Engine box also in neon green, labeled "Engine created inside Car". Right panel: Car box struck through in neon orange, Engine box also struck through — label "Car.scrap() destroys both". White monospace labels throughout.]


Common Mistakes

Mistake 1 — Passing child objects into the constructor

// WRONG: This is aggregation, not composition
class House {
  constructor(address, rooms) {  // Rooms created outside and passed in
    this.address = address;
    this.rooms = rooms;           // House holds a reference, doesn't own creation
  }
}

const livingRoom = new Room("Living Room"); // Created independently
const house = new House("42 Maple", [livingRoom]); // Passed in from outside

// livingRoom still exists even if house is gone — that is NOT composition
// CORRECT: Rooms created inside the constructor
class House {
  constructor(address, roomNames) {
    this.address = address;
    this.rooms = roomNames.map((name) => new Room(name)); // House creates them
  }
}

The test is simple: can you create the child object on line 1 and the parent on line 2? If yes, it's aggregation. In composition, the child objects come into existence inside the parent constructor.

Mistake 2 — Forgetting to destroy children when the parent is destroyed

// WRONG: Parent is "destroyed" but children are still referenced
class Order {
  cancel() {
    this.cancelled = true;
    // Items are still in this.items — still in memory, still accessible
  }
}
// CORRECT: Explicitly destroy and dereference children
class Order {
  cancel() {
    this.cancelled = true;
    this.items.forEach((item) => item.destroy());
    this.items = []; // Remove references so GC can collect them
  }
}

Mistake 3 — Confusing composition with inheritance

A common interview error is conflating "composition over inheritance" (a design principle about code reuse) with the composition object relationship. They use the same word, but they describe different things.

"Composition over inheritance" means: prefer building a class by holding references to other objects rather than extending a parent class.

The composition relationship (this lesson) means: a "Has-a" relationship where the child cannot exist without the parent.

They are related concepts, but they are not the same thing. Be precise about which one you mean in an interview.

[INTERNAL-LINK: composition over inheritance as a design principle → Chapter 4, Lesson 4.6: Composition vs Inheritance]

Mistake 4 — Assuming composition only applies to nested objects

Students often assume composition only applies when one object is visually "inside" another (like rooms in a house). Composition applies any time the child's existence is semantically dependent on the parent. An OrderItem isn't spatially inside an Order, but its existence is fully dependent on that order. The spatial intuition helps, but the logical test (can this child exist without this parent?) is the reliable one.


Interview Questions

Q1: What is composition in OOP, and how does it differ from aggregation?

Composition is a "Has-a" relationship where the child object's lifecycle is entirely dependent on the parent. The parent creates the child internally, owns it exclusively, and when the parent is destroyed, the child is destroyed automatically. Aggregation is also a "Has-a" relationship, but the child can exist independently of the parent. A Room cannot exist without its House (composition). A Professor can exist without a Department (aggregation). The key differentiator is always lifecycle ownership.


Q2: How do you implement composition in JavaScript? Give a code example.

You implement composition by instantiating child objects inside the parent's constructor, never accepting them as parameters from outside. The parent also provides a destroy() method that calls destroy on every child and then nullifies the references.

class Engine {
  constructor(type) { this.type = type; this.intact = true; }
  destroy() { this.intact = false; }
}

class Car {
  constructor(make) {
    this.make = make;
    this.engine = new Engine("V6"); // Created internally — composition
  }
  scrap() {
    this.engine.destroy(); // Child destroyed with parent
    this.engine = null;
  }
}

The key signals are: child created internally, no external reference exists, and the parent's cleanup method destroys the child.


Q3: Why does lifecycle matter when choosing between composition and aggregation?

Lifecycle matters because it determines ownership semantics and how you write cleanup code. If child objects can survive the parent (aggregation), you must not destroy them in the parent's destructor or cleanup method, since other objects may still hold references to them. If child objects must not survive the parent (composition), you must destroy them explicitly, because they have no valid state or meaning outside that parent. Choosing the wrong relationship type leads to either memory leaks (not cleaning up composition children) or dangling pointer bugs (destroying aggregation children that are still in use elsewhere).


Q4: Give three real-world examples of composition and explain why each qualifies.

  1. House and Rooms: Rooms are built into the house, have no independent address or existence, and are demolished when the house is demolished. The parent creates and owns the children.

  2. Order and OrderItems: An order item exists only in the context of a specific order. It encodes a quantity, a product, and an order-specific price. Delete the order, and the item has no meaning.

  3. Human body and Heart: A heart is not a portable component shared between bodies. It is created with and for one body. When the body dies, the heart ceases to function as a living organ. This is the biological version of lifecycle dependency.

In each case, the child's identity, meaning, and existence are inseparable from the parent that owns it.


Q5: Can you have composition without explicitly calling destroy() in JavaScript?

In JavaScript, you can rely on garbage collection to clean up child objects once the parent is dereferenced, provided no external references to the children exist. If the parent is the sole reference holder for all children (true composition), setting parent = null will make all children eligible for GC automatically.

However, relying on implicit GC is not sufficient for real applications. Child objects often hold resources beyond memory: open connections, event listeners, timers, or file handles. An explicit destroy() method that cleans up those resources before nullifying references is required for correct composition implementation in production JS code.


Cheat Sheet

COMPOSITION — Quick Reference

Definition:    "Has-a" with STRONG ownership.
               Child CANNOT exist independently.
               Parent creates, owns, and destroys the child.

Lifecycle:     Child lifespan = bounded by parent lifespan.
               Parent destroyed -> child destroyed.

vs Aggregation:
  - Aggregation: child survives parent (borrowed reference)
  - Composition: child dies with parent (owned creation)

Quick Test:    "Can this child object exist without this parent?"
               YES = Aggregation
               NO  = Composition

JS Pattern:
  class Parent {
    constructor() {
      this.child = new Child();  // Created internally
    }
    destroy() {
      this.child.destroy();      // Explicitly destroyed
      this.child = null;         // Reference removed
    }
  }

Real Examples:
  House     -> Rooms          (Composition)
  Document  -> Pages          (Composition)
  Order     -> OrderItems     (Composition)
  Car       -> Engine         (Composition)
  Body      -> Heart          (Composition)

  Department -> Professors    (Aggregation - NOT composition)
  Playlist   -> Songs         (Aggregation - NOT composition)
  Team       -> Players       (Aggregation - NOT composition)

Do NOT confuse with:
  "Composition over Inheritance" = a design principle (Lesson 4.6)
  "Composition relationship"     = this lesson (lifecycle ownership)


This is Lesson 6.3 of the OOP Interview Prep Course — 8 chapters, 41 lessons.

On this page