OOP Interview Prep
Object Relationships

Association vs Aggregation vs Composition

The Complete Comparison

LinkedIn Hook

Most developers use the word "relationship" loosely when describing class design.

"The Order has a Customer." "The Library has Books." "The Engine is part of the Car."

All three of those sentences describe different kinds of relationships. Treat them the same in code and you will create objects that outlive their owners, garbage collect things that should stay alive, and build systems where deleting one object silently breaks five others.

Association, aggregation, and composition are not synonyms. They sit on a spectrum, and the key that separates them is one question: when the owner is destroyed, what happens to the owned?

Lesson 6.4 gives you the complete comparison, the UML notation, the decision framework, and the code to back it up.

Read the full lesson -> [link]

#OOP #JavaScript #SoftwareEngineering #InterviewPrep #SystemDesign


Association vs Aggregation vs Composition thumbnail


What You'll Learn

  • The precise definitions of association, aggregation, and composition, and what each one communicates to another developer reading your code
  • How lifecycle dependency is the single key differentiator between all three relationships
  • A comprehensive comparison table covering ownership, lifecycle, UML notation, directionality, and when to use each
  • ASCII UML-style diagrams you can use in design discussions and whiteboard interviews
  • A real-world decision framework that tells you which relationship to model given any design scenario
  • Code examples in JavaScript showing all three patterns in a coherent domain

The Analogy That Separates All Three

Before writing a single line of code, picture a school.

A Teacher knows Students. The Teacher can interact with them, reference them, call methods on them. But if the Teacher leaves the school tomorrow, the Students still exist. They go to another class. The Teacher's departure has no effect on them. This is association — a "uses-a" relationship. Objects are aware of each other, but neither owns the other.

Now picture a Department. The Computer Science Department has Teachers. The department organizes them, lists them, dispatches them to courses. But if the CS Department is dissolved tomorrow, the Teachers do not disappear. They transfer to another department or leave on their own terms. The department held them, but did not own their existence. This is aggregation — a "has-a" relationship with independent lifecycle.

Now picture a House with Rooms. A Room is a physical part of the House. There is no Room without the House that contains it. If you demolish the House, the Rooms are demolished with it. You can't take Room 3 out of one House and put it in storage. It does not exist independently. This is composition — a "has-a" relationship with dependent lifecycle.

The spectrum runs from no ownership to partial ownership to total ownership. Lifecycle dependency is the ruler you measure it with.

[IMAGE: Three-row illustration. Row 1: two peer boxes (Teacher, Student) connected by a simple line arrow — labeled "Association: uses-a, no ownership". Row 2: Department box with hollow diamond connected to Teacher box — labeled "Aggregation: has-a, independent lifecycle". Row 3: House box with filled diamond connected to Room box, Room has a red border — labeled "Composition: has-a, dependent lifecycle". Dark background #0a0a1a, neon green highlights on composition row, neon orange on aggregation row]


What Is Association?

Association is the most general relationship between two classes. One object holds a reference to another and interacts with it, but neither object owns the other. Both exist independently and neither controls the other's lifecycle. (Martin Fowler, UML Distilled, 2003)

Association can be unidirectional (A knows about B, but B does not know about A) or bidirectional (both know about each other). It can also carry multiplicity: one-to-one, one-to-many, or many-to-many. A Teacher referencing a list of Student objects is a one-to-many association. A Student also referencing a Teacher makes it bidirectional.

The critical characteristic: destroying either object has no effect on the other. The references may become stale, but the objects survive on their own.

[INTERNAL-LINK: full lesson on association with more examples → Lesson 6.1: Association]

UML Notation for Association

Association (plain line, optional arrow for direction):

  Teacher ---------> Student
          "teaches"

  Or bidirectional:
  Teacher <--------> Student

  Multiplicity notation:
  Teacher 1 -------> * Student
  (one Teacher teaches many Students)

What Is Aggregation?

Aggregation is a specialized form of association that implies ownership without lifecycle control. The containing object ("the whole") has the component ("the part"), but the component can exist independently of the whole. If the whole is destroyed, the part is not. (Robert C. Martin, Agile Software Development, 2002)

The hollow diamond in UML sits on the "whole" side of the relationship. It signals: this end holds the other, but does not own it to the point of controlling its life.

A Department aggregates Teacher objects. Teachers belong to a department in an organizational sense. But teachers existed before the department was created and will exist after it is dissolved. The department holds references, not exclusive ownership.

[INTERNAL-LINK: full lesson on aggregation with lifecycle examples → Lesson 6.2: Aggregation]

UML Notation for Aggregation

Aggregation (hollow diamond on the "whole" side):

  Department <>-------> Teacher
              "employs"

  The hollow diamond (<>) sits on Department.
  It reads: "Department has Teacher(s), but Teacher can exist without Department."

  With multiplicity:
  Department 1 <>-------> * Teacher

What Is Composition?

Composition is the strongest form of object relationship. The component's existence depends entirely on the composite. The composite creates the component, owns it exclusively, and destroys it when it is itself destroyed. (Gang of Four, Design Patterns, Addison-Wesley, 1994)

The filled diamond in UML sits on the composite side. It signals: this end not only holds the other, it defines whether it exists at all.

A House composed of Room objects is the classic example. A Room has no meaning outside the house it belongs to. A Form that creates FormField objects owns them completely. An Order that creates OrderLine objects controls their entire lifecycle. When the parent is gone, the children go with it.

[INTERNAL-LINK: full lesson on composition with code examples → Lesson 6.3: Composition]

UML Notation for Composition

Composition (filled diamond on the composite side):

  House [+]-------> Room
         "contains"

  The filled diamond ([+]) sits on House.
  It reads: "House owns Room. Room cannot exist without House."

  With multiplicity:
  House 1 [+]-------> 1..* Room
  (a House must have at least one Room)

[IMAGE: ASCII-style UML diagram showing all three relationships stacked vertically with their diamond notation differences. Neon green for composition row, neon orange for aggregation, white for association. Search terms: "UML association aggregation composition diamond notation diagram"]


The Complete Comparison Table

ASSOCIATION vs AGGREGATION vs COMPOSITION — FULL COMPARISON
---------------------------------------------------------------------------
Feature            Association         Aggregation          Composition
---------------------------------------------------------------------------
Relationship       "Uses-a"            "Has-a" (weak)       "Has-a" (strong)
type

Ownership          No ownership        Partial ownership    Full exclusive
                                       (holds, not owns)    ownership

Lifecycle          Fully               Independent —        Dependent —
dependency         independent         part survives        part destroyed
                   — neither           whole's              with whole
                   controls the        destruction
                   other

UML symbol         Plain line or       Hollow diamond       Filled diamond
                   arrow               on the "whole"       on the composite
                   (no diamond)        side                 side

UML notation       A -------> B        A <>------> B        A [+]-----> B

Direction          Uni or bi-          Usually              Usually
                   directional         unidirectional       unidirectional

Object creation    Neither creates     Whole does NOT       Composite
responsibility     the other           typically create     creates the
                   (external)          the part             part

Can part be        Yes — exists        Yes — existed        No — only
shared?            before and          before the           meaningful
                   after the           whole, can join      inside the
                   relationship        another whole        composite

Real-world         Teacher-Student,    Department-          House-Room,
examples           Doctor-Patient,     Teacher,             Order-OrderLine,
                   Driver-Car          Library-Book,        Form-FormField,
                   (uses it,           Team-Player          Brain-Neuron
                   doesn't own it)

In JavaScript      Object holds a      Object holds an      Object creates
                   reference           array/reference      children in
                   to another          of externally        constructor,
                   object              created objects      owns them

Coupling level     Loose               Medium               Tight (by design)

When to use        Objects need        Objects are          Part has no
                   to interact         organized            identity or
                   without             together but         meaning outside
                   ownership           remain               the whole
                                       independent

Deletion           Neither             Whole can be         Whole MUST
behavior           affects the         deleted without      delete parts
                   other               deleting parts       (or leak occurs)
---------------------------------------------------------------------------

[CHART: Horizontal spectrum bar - "Relationship Strength" axis from left (weakest) to right (strongest). Three labeled points: Association (left), Aggregation (center), Composition (right). Below the bar, three sub-labels: "No ownership / Independent lifecycle", "Partial ownership / Independent lifecycle", "Full ownership / Dependent lifecycle". Neon green for Composition, neon orange for Aggregation, white for Association. Source: standard UML specification]


Code Example 1 — Association: Teacher and Student

In association, both objects are created outside the relationship. Neither owns the other. The Teacher holds a reference to Student objects, but those students will survive any changes to the Teacher.

// Association: Teacher and Student exist independently.
// Teacher references Students, but does not own them.

class Student {
  constructor(name, id) {
    this.name = name;
    this.id = id;
  }

  study(subject) {
    console.log(`${this.name} is studying ${subject}`);
  }
}

class Teacher {
  constructor(name, subject) {
    this.name = name;
    this.subject = subject;
    // Teacher holds references to students — it did not create them
    // They are passed in from the outside (external ownership)
    this.students = [];
  }

  addStudent(student) {
    // Teacher receives an externally created Student
    // It does not control the Student's lifecycle
    this.students.push(student);
  }

  removeStudent(studentId) {
    // Removing from the list does not destroy the Student object
    // The Student continues to exist elsewhere
    this.students = this.students.filter(s => s.id !== studentId);
  }

  conductClass() {
    console.log(`${this.name} is teaching ${this.subject} to:`);
    this.students.forEach(s => {
      console.log(`  - ${s.name}`);
      s.study(this.subject);
    });
  }
}

// Students are created independently — not inside the Teacher
const alice = new Student('Alice', 'S001');
const bob = new Student('Bob', 'S002');
const carol = new Student('Carol', 'S003');

const mrJones = new Teacher('Mr. Jones', 'Mathematics');

// Association: Teacher references externally created Students
mrJones.addStudent(alice);
mrJones.addStudent(bob);
mrJones.conductClass();
// Mr. Jones is teaching Mathematics to:
//   - Alice
//   Alice is studying Mathematics
//   - Bob
//   Bob is studying Mathematics

// Key demonstration: removing the teacher does NOT affect students
mrJones.removeStudent('S001');
// Alice is NOT destroyed — she still exists independently
console.log(alice.name);   // Alice — fully alive, unaffected

// Students can also be associated with multiple teachers
const msLee = new Teacher('Ms. Lee', 'Physics');
msLee.addStudent(alice);   // Alice is now in two teachers' lists — no conflict
msLee.addStudent(carol);
msLee.conductClass();
// Ms. Lee is teaching Physics to:
//   - Alice
//   Alice is studying Physics
//   - Carol
//   Carol is studying Physics

The Student objects were created before the Teacher and exist after any teacher reference is dropped. Neither mrJones nor msLee owns them. Alice can be in both teachers' lists simultaneously. This is the defining characteristic of association.


Code Example 2 — Aggregation: Department and Teacher

In aggregation, the Department organizes Teachers. It holds them, manages them, and can remove them from its roster. But the Teachers were created externally and will survive the Department's removal.

// Aggregation: Department HAS Teachers, but Teachers can exist independently.
// Hollow diamond on Department in UML notation.

class Teacher {
  constructor(name, specialization) {
    this.name = name;
    this.specialization = specialization;
    this.currentDepartment = null;
  }

  assignToDepartment(deptName) {
    this.currentDepartment = deptName;
    console.log(`${this.name} assigned to ${deptName}`);
  }

  leaveDepartment() {
    console.log(`${this.name} leaving ${this.currentDepartment}`);
    this.currentDepartment = null;
  }

  teach() {
    const loc = this.currentDepartment || 'unassigned';
    console.log(`${this.name} (${this.specialization}) teaching in ${loc}`);
  }
}

class Department {
  constructor(name) {
    this.name = name;
    // Department holds references to Teachers it does not own
    // Teachers were created outside and passed in
    this.teachers = [];
  }

  hire(teacher) {
    // Aggregation: we receive an existing Teacher, not create a new one
    // The teacher continues to exist outside this department's control
    this.teachers.push(teacher);
    teacher.assignToDepartment(this.name);
  }

  transfer(teacher, targetDepartment) {
    // Teacher moves to another department — not destroyed
    this.teachers = this.teachers.filter(t => t.name !== teacher.name);
    teacher.leaveDepartment();
    targetDepartment.hire(teacher);
  }

  disband() {
    // Department is dissolved. Teachers are NOT destroyed.
    // They leave the roster and become unassigned — still alive.
    console.log(`Disbanding ${this.name} department...`);
    this.teachers.forEach(t => t.leaveDepartment());
    this.teachers = [];
    // After this line, the Department object may be garbage collected.
    // The Teacher objects remain fully alive.
  }

  listTeachers() {
    console.log(`${this.name} Department staff:`);
    this.teachers.forEach(t => console.log(`  - ${t.name} (${t.specialization})`));
  }
}

// Teachers exist independently — created before any department
const drSmith = new Teacher('Dr. Smith', 'Algorithms');
const drPatel = new Teacher('Dr. Patel', 'Databases');
const drKim = new Teacher('Dr. Kim', 'Networks');

const csDept = new Department('Computer Science');
const mathDept = new Department('Mathematics');

csDept.hire(drSmith);
// Dr. Smith assigned to Computer Science
csDept.hire(drPatel);
// Dr. Patel assigned to Computer Science
csDept.hire(drKim);
// Dr. Kim assigned to Computer Science

csDept.listTeachers();
// Computer Science Department staff:
//   - Dr. Smith (Algorithms)
//   - Dr. Patel (Databases)
//   - Dr. Kim (Networks)

// Transfer: Dr. Patel moves to Math. Not destroyed — just reassigned.
csDept.transfer(drPatel, mathDept);
// Dr. Patel leaving Computer Science
// Dr. Patel assigned to Mathematics

// Disband the CS Department — Teachers survive
csDept.disband();
// Disbanding Computer Science department...
// Dr. Smith leaving Computer Science
// Dr. Kim leaving Computer Science

// Teachers are still fully alive after their department is gone
drSmith.teach();   // Dr. Smith (Algorithms) teaching in unassigned
drKim.teach();     // Dr. Kim (Networks) teaching in unassigned
drPatel.teach();   // Dr. Patel (Databases) teaching in Mathematics — still active

[PERSONAL EXPERIENCE]: The transfer and disband methods in this example are the parts that developers get wrong in practice. The instinct when removing an object from a collection is often to call some destroy or cleanup method on it. With aggregation, that is the wrong call. The collection removes its reference; the object carries on. This distinction matters most in systems that manage large pools of shared resources, such as connection pools, worker threads, or cached service instances.


Code Example 3 — Composition: House and Room

In composition, the House creates its Rooms internally. The Rooms have no meaningful existence outside the House that owns them. Destroying the House means destroying its Rooms.

// Composition: House OWNS Rooms. Rooms are created by House and cannot exist alone.
// Filled diamond on House in UML notation.

class Room {
  constructor(type, squareMeters) {
    this.type = type;
    this.squareMeters = squareMeters;
    // Room is intentionally simple — it has no standalone factory or public constructor
    // In strict composition, Room should only be created by House
    // JS lacks access control to enforce this, so we document the intent clearly
    this.isActive = true;
  }

  describe() {
    return `${this.type} (${this.squareMeters} sqm)`;
  }

  deactivate() {
    // Called by House when it is demolished — Room loses meaning
    this.isActive = false;
  }
}

class House {
  constructor(address) {
    this.address = address;
    // Composition: House creates Rooms itself inside the constructor
    // Rooms are born owned — they are never passed in from outside
    this.rooms = [
      new Room('Living Room', 35),
      new Room('Kitchen', 20),
      new Room('Master Bedroom', 25),
    ];
    this.demolished = false;
  }

  addRoom(type, squareMeters) {
    if (this.demolished) {
      throw new Error('Cannot add a room to a demolished house');
    }
    // New rooms are only created through the House — never standalone
    const room = new Room(type, squareMeters);
    this.rooms.push(room);
    return room;
  }

  getRoom(type) {
    return this.rooms.find(r => r.type === type);
  }

  getTotalArea() {
    return this.rooms.reduce((total, r) => total + r.squareMeters, 0);
  }

  listRooms() {
    console.log(`Rooms at ${this.address}:`);
    this.rooms.forEach(r => console.log(`  - ${r.describe()}`));
    console.log(`  Total: ${this.getTotalArea()} sqm`);
  }

  demolish() {
    // Composition: when the House is destroyed, all Rooms are destroyed with it
    // The House is responsible for the full lifecycle of its components
    console.log(`Demolishing house at ${this.address}...`);
    this.rooms.forEach(r => r.deactivate());
    this.rooms = [];   // Release all references — Rooms become unreachable
    this.demolished = true;
    console.log('House demolished. All rooms destroyed.');
  }
}

const house = new House('123 Oak Street');
house.addRoom('Guest Bedroom', 18);
house.addRoom('Home Office', 15);

house.listRooms();
// Rooms at 123 Oak Street:
//   - Living Room (35 sqm)
//   - Kitchen (20 sqm)
//   - Master Bedroom (25 sqm)
//   - Guest Bedroom (18 sqm)
//   - Home Office (15 sqm)
//   Total: 113 sqm

// Key: you cannot sensibly reference a Room outside its House
// The Room was never exposed for external creation
const kitchen = house.getRoom('Kitchen');
console.log(kitchen.describe());    // Kitchen (20 sqm)

// Demolish the house — all rooms go with it
house.demolish();
// Demolishing house at 123 Oak Street...
// House demolished. All rooms destroyed.

// The internal room references are cleared — rooms are unreachable
// The `kitchen` variable above still holds a reference to that object in JS,
// but the object is now deactivated and the house no longer knows about it
console.log(kitchen.isActive);      // false — Room was deactivated by demolish()
// In a strictly typed language, the Room type would prevent standalone instantiation.
// In JS, we enforce the intent through documentation and design conventions.

[UNIQUE INSIGHT]: JavaScript cannot enforce composition at the language level the way Java can with package-private constructors. The convention used here — creating parts inside the owner's constructor and never exposing a public constructor for the part — is the closest JavaScript equivalent. In TypeScript, combining a private constructor on the part class with a static factory method on the composite is the cleaner enforcement pattern. The intent must be made explicit in code structure, not just comments.


Code Example 4 — The Decision Framework in One Domain

Seeing all three relationships side by side in a single coherent domain is what interviewers are testing when they ask you to "model a library system." This example shows how to apply all three decisions in one design.

// A Library system modeled with all three relationship types.
// The same domain, three different relationship strengths.

// --- ASSOCIATION: Borrower uses a LibraryCard ---
// Both exist independently. Neither owns the other.

class LibraryCard {
  constructor(cardNumber, issuedTo) {
    this.cardNumber = cardNumber;
    this.issuedTo = issuedTo;
    this.isActive = true;
  }
}

class Borrower {
  constructor(name) {
    this.name = name;
    // Association: Borrower references a LibraryCard it did not create
    // The card exists independently and could be reassigned
    this.card = null;
  }

  assignCard(libraryCard) {
    // Card is created and managed elsewhere — Borrower just holds a reference
    this.card = libraryCard;
  }

  revokeCard() {
    // Revoking the association does not destroy the card object
    this.card = null;
  }
}

// --- AGGREGATION: Library HAS Books, but Books exist independently ---
// Books could move between libraries, be sold, or be donated.

class Book {
  constructor(title, isbn) {
    this.title = title;
    this.isbn = isbn;
    this.currentLibrary = null;
  }
}

class Library {
  constructor(name) {
    this.name = name;
    // Aggregation: Library holds externally created Books
    // Books are not created by the Library — they existed before
    this.catalog = [];
  }

  addBook(book) {
    // Aggregation: receive existing Book object, do not create it
    book.currentLibrary = this.name;
    this.catalog.push(book);
  }

  transferBook(book, targetLibrary) {
    // Book is not destroyed — it moves to another library
    this.catalog = this.catalog.filter(b => b.isbn !== book.isbn);
    targetLibrary.addBook(book);
  }

  close() {
    // Library closes. Books are NOT destroyed.
    // They move to storage or are donated — still exist.
    console.log(`${this.name} is closing. Redistributing catalog...`);
    this.catalog.forEach(b => { b.currentLibrary = null; });
    this.catalog = [];
  }
}

// --- COMPOSITION: Library HAS Shelves. Shelves only exist inside a Library. ---
// A Shelf has no meaning without the Library that contains it.

class Shelf {
  constructor(section, capacity) {
    this.section = section;
    this.capacity = capacity;
    this.slots = capacity;
  }

  describe() {
    return `Section ${this.section} (${this.slots}/${this.capacity} slots free)`;
  }
}

class FullLibrary {
  constructor(name, shelfSections) {
    this.name = name;
    // Composition: Shelves are created by FullLibrary, owned exclusively
    // Shelves cannot be transferred to another library — they are physical parts
    this.shelves = shelfSections.map(
      (section) => new Shelf(section, 50)
    );
  }

  listShelves() {
    console.log(`Shelves in ${this.name}:`);
    this.shelves.forEach(s => console.log(`  - ${s.describe()}`));
  }

  demolish() {
    // Composition: Shelves are destroyed with the Library
    console.log(`Demolishing ${this.name}. All shelves removed.`);
    this.shelves = [];
  }
}

// Putting it all together — three relationship types in one domain

// Association: card and borrower exist independently
const card1 = new LibraryCard('LC-001', 'Alice');
const alice = new Borrower('Alice');
alice.assignCard(card1);   // Alice uses the card — does not own it
alice.revokeCard();        // Card still exists after this

// Aggregation: books exist before the library
const book1 = new Book('Clean Code', '978-0132350884');
const book2 = new Book('The Pragmatic Programmer', '978-0135957059');
const cityLibrary = new Library('City Central Library');
cityLibrary.addBook(book1);
cityLibrary.addBook(book2);
cityLibrary.close();       // Library closes — books survive
console.log(book1.title);  // Clean Code — still accessible

// Composition: shelves are created inside the library, cannot exist alone
const mainBranch = new FullLibrary('Main Branch', ['A', 'B', 'C', 'D']);
mainBranch.listShelves();
// Shelves in Main Branch:
//   - Section A (50/50 slots free)
//   - Section B (50/50 slots free)
//   - Section C (50/50 slots free)
//   - Section D (50/50 slots free)

mainBranch.demolish();
// Demolishing Main Branch. All shelves removed.
// Shelves are now unreachable — they are gone with the library

[ORIGINAL DATA]: The three-relationship pattern above (uses-a for cards, has-a independent for catalog, has-a dependent for infrastructure) maps cleanly to how real library management systems distinguish between reference entities (borrowers, catalog items that move across branches) and structural entities (shelves, reading carrels, rooms) that belong to a single physical location. The same separation applies in e-commerce: Cart is composed of CartItems (composition), Order references a Customer (association), and a Warehouse aggregates Products (aggregation, since products exist in supplier systems independently).


The Lifecycle Dependency Decision Framework

When you face a design decision, ask these three questions in order. The first "yes" determines your answer.

DECISION FRAMEWORK — WHICH RELATIONSHIP TO MODEL
==================================================

Step 1: Does Object B have ANY meaning or identity
        outside of Object A?

  YES -> Can B be shared between multiple A instances?
         YES -> Association (uses-a, no ownership)
         NO  -> Aggregation (has-a, shared lifecycle possible)

  NO  -> Composition (has-a, B exists only inside A)

----------------------------------------------------------

QUICK CHECKS
------------

Association signals:
  - B was created outside A and passed in
  - B can be null (the relationship is optional)
  - B can be reassigned to a different A
  - B can reference multiple A instances simultaneously

Aggregation signals:
  - B was created outside A, then added to A's collection
  - Removing B from A does not destroy B
  - B could be transferred to a different A (e.g., transfer a teacher)
  - A can be destroyed independently of B

Composition signals:
  - B is created inside A (usually in the constructor)
  - B's public interface is only meaningful through A
  - Destroying A should destroy all of B's instances
  - B cannot be sensibly transferred to a different A

----------------------------------------------------------

REAL-WORLD MAPPING
-------------------
Association:   Driver -> Car (driver uses car, doesn't own it)
               Doctor -> Patient (interaction, no ownership)
               Order -> Customer (order references customer)

Aggregation:   Team -> Player (player can leave and join another)
               Playlist -> Song (song can be in many playlists)
               University -> Department (dept can be reorganized)

Composition:   Order -> OrderLine (line has no meaning outside order)
               Car -> Engine (this engine belongs to this car)
               Document -> Paragraph (paragraphs are parts of the doc)
----------------------------------------------------------

[IMAGE: Flowchart diagram showing the three decision steps above as a clean tree. Start node: "Does B have meaning outside A?" Two branches: Yes leads to "Can B be shared?" with two sub-branches (Association, Aggregation). No leads directly to Composition. Neon green for Composition node, neon orange for Aggregation node, white for Association node. Dark background.]


Common Mistakes

  • Using composition when aggregation is correct. If you create the component inside the owner's constructor and tightly bind its lifecycle, but the component could reasonably exist independently (like a Teacher in a Department), you've modeled the wrong relationship. The component ends up being destroyed unnecessarily when the owner is removed. Always ask: "Could this object sensibly exist without this owner?" If yes, lean toward aggregation.

  • Treating aggregation and association as identical. Both allow independent lifecycles, so developers often conflate them. The distinction is ownership intent and directionality. Aggregation implies the "whole" actively organizes and manages the "parts" as a collection. Association is looser — one object references another for interaction, not organization. The difference matters most when documenting intent for other developers reading the code.

  • Leaking composed parts through public getters. If a House exposes getRoom() and callers store that Room reference outside the House, you've accidentally created a hybrid. The Room is conceptually composed, but outside callers now hold a reference that outlives any demolish call. Either return copies, return read-only views, or document clearly that callers must not hold Room references beyond the House's lifecycle.

  • Saying "composition is always better than aggregation" in an interview. Composition and aggregation are not on a quality spectrum. They describe different real-world facts. A library's books are genuinely independent of any single library. Modeling them with composition would be semantically wrong — it would imply books cease to exist when a library branch closes. Use the relationship that matches the domain reality.

  • Confusing this chapter's "composition" with the GoF "composition over inheritance" principle. They share the word but mean different things. The GoF principle says to prefer object assembly (holding component references) over class hierarchies. This chapter's composition is a specific UML relationship with a lifecycle dependency characteristic. An interview question can involve both, so name them precisely: "UML composition" vs "composition over inheritance."


Interview Questions

Q: What is the key difference between aggregation and composition?

The key difference is lifecycle dependency. In aggregation, the part can exist independently of the whole. A Teacher aggregated by a Department continues to exist when the department is dissolved — they transfer elsewhere. In composition, the part's existence depends entirely on the whole. An OrderLine composed by an Order has no meaning outside that order — when the order is cancelled, its lines are gone. Both are "has-a" relationships. Lifecycle is the only differentiator.

Q: How would you model a Car and its Engine in UML terms? What relationship type is it?

It depends on the domain context, and that nuance is what makes the question interesting. In most real-world modeling, an Engine is composed by a Car. The Engine is manufactured as part of this specific car, it is not transferable to another car without significant rework, and it has no standalone operational meaning outside the Car. This is composition — filled diamond on Car in UML. However, in a parts-supply system where engines are inventory items that get assigned to cars, the relationship could reasonably be aggregation. The domain context, not the objects themselves, determines which relationship is correct.

Q: How do you represent association, aggregation, and composition in JavaScript code, since JS has no UML syntax?

The relationships are expressed through construction and reference patterns. For association: both objects are created externally, and one holds a reference passed in from outside — this.card = externalCard. For aggregation: objects are created externally and added to a collection — this.teachers.push(externalTeacher), with removal not triggering destruction. For composition: the composite creates its parts internally, usually in the constructor — this.rooms = [new Room(...)], and a demolish or cleanup method explicitly nullifies all part references. The patterns document intent; JavaScript itself does not enforce the lifecycle rules.

Q: Can a class have aggregation relationships with some properties and composition relationships with others at the same time?

Yes, and most real classes do. A Car is typically composed of its Engine and Chassis (parts with no independent existence) while having an aggregation relationship with its current Driver or Owner (people who exist independently). A Hospital might be composed of Ward objects (physical rooms that only exist inside the hospital) while aggregating Doctor objects (doctors who have careers independent of any single hospital). Mixing relationship types within a single class is normal and correct when it reflects the actual domain structure.

Q: In an interview, how would you decide which of the three relationships to model for a given scenario?

Three questions in order. First: does the object have meaningful existence outside this owner? If no, it is composition. Second: if yes, is the relationship primarily about organization and collection management, or just loose interaction? Organization with a "whole holds parts" structure points to aggregation. Loose interaction points to association. Third, check the construction pattern: composition means the composite creates the parts; aggregation means the parts are passed in from outside; association means neither constructs the other and either can be null. If you answer all three and they point to different conclusions, trust the lifecycle question — it is the most discriminating.


Quick Reference Cheat Sheet

ASSOCIATION vs AGGREGATION vs COMPOSITION — CHEAT SHEET
========================================================

CORE QUESTION
--------------
"When A is destroyed, what happens to B?"

  B is destroyed too        ->  Composition   (filled diamond)
  B survives, is managed    ->  Aggregation   (hollow diamond)
  B survives, is independent ->  Association  (plain line/arrow)

UML NOTATION SUMMARY
---------------------
Association:   A ---------> B         (plain line, optional arrow)
Aggregation:   A <>-------> B         (hollow diamond on A)
Composition:   A [+]------> B         (filled diamond on A)

JAVASCRIPT CODE PATTERNS
--------------------------
Association:
  class A {
    setB(b) { this.b = b; }      // B passed in from outside
    clearB() { this.b = null; }  // clearing does not destroy B
  }

Aggregation:
  class A {
    addB(b) { this.parts.push(b); }     // B already existed
    removeB(b) { /* filter, not destroy */ }
    dissolve() {
      this.parts.forEach(b => b.cleanup()); // B survives
      this.parts = [];
    }
  }

Composition:
  class A {
    constructor() {
      this.parts = [new B(), new B()];   // A creates B internally
    }
    destroy() {
      this.parts.forEach(b => b.deactivate());
      this.parts = [];                   // B is gone when A is gone
    }
  }

REAL-WORLD EXAMPLES
--------------------
Association:   Teacher <-> Student       (interact, no ownership)
               Order -> Customer         (references, not owns)
               Driver -> Car             (uses it temporarily)

Aggregation:   Department <>-> Teacher  (teacher outlives dept)
               Library <>-> Book        (book can move branches)
               Team <>-> Player         (player can transfer)

Composition:   House [+]-> Room         (room dies with house)
               Order [+]-> OrderLine    (line dies with order)
               Form [+]-> FormField     (field only in this form)

LIFECYCLE TABLE
----------------
                  A destroyed?   B created by?   B shared?
Association       B survives     External         Yes
Aggregation       B survives     External         Yes (but managed by A)
Composition       B destroyed    A (internally)   No

INTERVIEW TRAPS
----------------
Trap 1: "Composition vs composition over inheritance" — different concepts.
        UML composition = lifecycle dependency.
        GoF "favor composition" = use has-a over is-a in class design.

Trap 2: Saying aggregation and association are the same — they are not.
        Aggregation implies organized collection + whole/part structure.
        Association is pure interaction with no ownership implied.

Trap 3: Saying one is always better — they model different domain truths.
        Use the one that matches reality, not the one that sounds stronger.

CHAPTER 6 SUMMARY
------------------
Lesson 6.1  Association      uses-a, no ownership, independent lifecycle
Lesson 6.2  Aggregation      has-a (weak), organized, independent lifecycle
Lesson 6.3  Composition      has-a (strong), dependent lifecycle
Lesson 6.4  All three        comparison, decision framework, UML notation

Association vs Aggregation vs Composition visual 1


Previous: Lesson 6.3 - Composition Next: Lesson 7.1 - Single Responsibility Principle


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

On this page