OOP Interview Prep
Object Relationships

Association

The "Uses-a" Relationship That Holds Real Systems Together

LinkedIn Hook

Here is something most OOP courses skip entirely:

Inheritance gets three chapters. Encapsulation gets flashcards. Polymorphism gets its own interview category.

And then there is association — the relationship that powers almost every real system you will ever build — buried in a footnote, if it shows up at all.

When a Teacher works with Students, a Driver operates a Car, or an Order references a Product, none of those are inheritance relationships. None of them are composition. They are associations. Objects that know about each other, use each other, and stay loosely connected.

If you cannot explain association clearly in an interview — including the difference between unidirectional and bidirectional, one-to-one vs one-to-many, and why loose coupling matters — you are leaving a gap that interviewers notice.

Lesson 6.1 closes that gap. Code, diagrams, and interview Q&A included.

Read the full lesson -> [link]

#OOP #JavaScript #SoftwareDesign #InterviewPrep #ObjectRelationships


Association thumbnail


What You'll Learn

  • What association means in OOP and why it is distinct from inheritance, aggregation, and composition
  • The four cardinality types: one-to-one, one-to-many, many-to-one, and many-to-many
  • The difference between unidirectional and bidirectional association — and when each applies
  • How loose coupling works through association and why it matters for maintainable design
  • Three common interview mistakes candidates make when explaining object relationships

What Is Association in OOP?

Think of a school. A Teacher teaches Students. The Teacher does not own the Students — Students exist independently. The Teacher does not become a Student through inheritance. They simply know about each other and interact. That "knows about and uses" relationship is association.

Association is the broadest form of object relationship in OOP. It models how objects use other objects without implying ownership or structural dependency. The related objects have their own lifecycle. One can exist without the other.

[INTERNAL-LINK: object relationships overview -> chapter 6 intro or content.md overview section]


The "Uses-a" Mental Model

Before writing code, get the mental model right. Association follows a simple test:

Can Object A do its job using Object B, while B exists independently of A?

If yes, it is association. A Driver uses a Car. The Car does not disappear when the Driver is gone. A Doctor sees Patients. Patients exist outside the Doctor's lifecycle. A Customer places an Order. Orders reference Products. Products are not owned by any one Order.

[UNIQUE INSIGHT] The "uses-a" label is deceptively simple. In practice, most real-world object relationships in production systems are associations — not inheritance, not composition. Every service-to-repository connection, every controller-to-model reference, and every event emitter-to-listener binding is association under the hood. Recognizing it changes how you design systems.


Example 1 — One-to-One Association (Driver and License)

A Driver has one License. The License is an independent object — it is issued by a government body, stored in a database, and can exist before the Driver object is created in memory. This is one-to-one unidirectional association: Driver knows about License, but License does not reference Driver.

class License {
  constructor(licenseNumber, expiry) {
    this.licenseNumber = licenseNumber;
    this.expiry = expiry;
  }

  isValid() {
    return new Date(this.expiry) > new Date();
  }
}

class Driver {
  constructor(name, license) {
    this.name = name;
    this.license = license; // association: Driver uses License
  }

  canDrive() {
    return this.license.isValid();
  }
}

const license = new License("DL-2048", "2027-12-31");
const driver = new Driver("Sarah", license);

console.log(driver.canDrive()); // true
console.log(driver.name);       // "Sarah"

// License still exists independently
console.log(license.licenseNumber); // "DL-2048"

Key observations here. Driver receives a License through its constructor — this is called dependency injection. The License object is created outside Driver. If Driver is deleted, License is unaffected. The relationship is navigable in one direction only: Driver -> License.


What Are the Cardinality Types?

Cardinality describes how many objects on each side participate in the relationship. Getting this right in an interview signals design maturity.

TypeDescriptionReal Example
One-to-OneOne A uses one BDriver - License
One-to-ManyOne A uses many BTeacher - Students
Many-to-OneMany A use one BEmployees - Department
Many-to-ManyMany A use many BStudents - Courses

[CHART: Table chart - Association cardinality types with real-world examples and code implications - Original content]


Example 2 — One-to-Many Association (Teacher and Students)

A Teacher works with many Students. Each Student exists independently and may be associated with other Teachers as well. The Teacher holds references to Student objects but does not control their creation or destruction.

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

  study() {
    return `${this.name} is studying.`;
  }
}

class Teacher {
  constructor(name) {
    this.name = name;
    this.students = []; // one-to-many: Teacher uses many Students
  }

  addStudent(student) {
    this.students.push(student);
  }

  rollCall() {
    return this.students.map(s => s.name);
  }
}

const alice = new Student("Alice", 101);
const bob   = new Student("Bob",   102);
const carol = new Student("Carol", 103);

const teacher = new Teacher("Mr. Hassan");
teacher.addStudent(alice);
teacher.addStudent(bob);
teacher.addStudent(carol);

console.log(teacher.rollCall()); // ["Alice", "Bob", "Carol"]

// Students exist outside the teacher
console.log(alice.study()); // "Alice is studying."

Notice that alice, bob, and carol are created outside Teacher. Removing the teacher object from memory does not affect any Student instance. This independent lifecycle is the core trait separating association from composition.

[INTERNAL-LINK: composition deep dive -> ch04 composition-vs-inheritance lesson]


Unidirectional vs Bidirectional Association

Direction describes which object knows about the other.

Unidirectional: Only one side holds a reference. Teacher knows its Students; Students do not hold a reference back to Teacher. This is simpler, easier to maintain, and should be your default.

Bidirectional: Both sides hold references to each other. Teacher knows its Students and each Student knows its Teacher. This is useful when both sides need to navigate the relationship but introduces coupling that must be managed carefully.

[IMAGE: Simple UML-style diagram showing unidirectional arrow (Teacher -> Student) vs bidirectional arrow (Teacher <-> Student) - search: "UML class diagram association arrows white background"]


Example 3 — Bidirectional Association (Teacher and Student)

class Student {
  constructor(name) {
    this.name = name;
    this.teacher = null; // will hold a reference back to Teacher
  }

  assignTeacher(teacher) {
    this.teacher = teacher;
  }

  getTeacherName() {
    return this.teacher ? this.teacher.name : "No teacher assigned";
  }
}

class Teacher {
  constructor(name) {
    this.name = name;
    this.students = [];
  }

  addStudent(student) {
    this.students.push(student);
    student.assignTeacher(this); // both sides now reference each other
  }
}

const teacher = new Teacher("Ms. Priya");
const dan     = new Student("Dan");
const eve     = new Student("Eve");

teacher.addStudent(dan);
teacher.addStudent(eve);

console.log(dan.getTeacherName());          // "Ms. Priya"
console.log(teacher.students[1].name);      // "Eve"
console.log(eve.getTeacherName());          // "Ms. Priya"

Both objects reference each other now. The risk with bidirectional association is circular references — especially in memory-managed environments like Node.js or browsers. Always ask: does the Student side truly need to navigate back to Teacher? If not, keep it unidirectional.

[PERSONAL EXPERIENCE] In production codebases, bidirectional associations look clean on a diagram but become a maintenance burden fast. Circular references can cause memory leaks in long-running Node.js services if objects are not dereferenced properly. The safest default is unidirectional association. Add the reverse reference only when the business logic genuinely requires it.


Example 4 — Many-to-Many Association (Students and Courses)

Many Students can enroll in many Courses. A single Course is used by many Students. Neither owns the other. This is the most complex cardinality but also the most common in domain modeling.

class Course {
  constructor(title) {
    this.title = title;
    this.enrolledStudents = [];
  }

  enroll(student) {
    if (!this.enrolledStudents.includes(student)) {
      this.enrolledStudents.push(student);
      student.addCourse(this); // keep both sides in sync
    }
  }

  roster() {
    return this.enrolledStudents.map(s => s.name);
  }
}

class Student {
  constructor(name) {
    this.name = name;
    this.courses = [];
  }

  addCourse(course) {
    if (!this.courses.includes(course)) {
      this.courses.push(course);
    }
  }

  schedule() {
    return this.courses.map(c => c.title);
  }
}

const math    = new Course("Mathematics");
const physics = new Course("Physics");

const sam  = new Student("Sam");
const tina = new Student("Tina");

math.enroll(sam);
math.enroll(tina);
physics.enroll(tina);

console.log(math.roster());     // ["Sam", "Tina"]
console.log(tina.schedule());   // ["Mathematics", "Physics"]
console.log(sam.schedule());    // ["Mathematics"]

The includes guard on both enroll and addCourse prevents duplicate references from building up. In real systems, many-to-many associations are typically managed through a join table in the database layer and a dedicated junction class in the domain layer.

[INTERNAL-LINK: aggregation and ownership -> ch06 02-aggregation lesson]


Why Loose Coupling Matters

Association is the primary tool for achieving loose coupling. Loose coupling means objects depend on each other through minimal, well-defined interfaces — not through deep structural dependencies.

A Teacher that holds Student references is loosely coupled to Student. You can swap out a Student implementation, mock it in tests, or replace it entirely without touching Teacher. Contrast this with inheritance, where a subclass is structurally locked to its parent.

[UNIQUE INSIGHT] The practical payoff of loose coupling through association shows up most clearly in testing. A Teacher class that receives Student objects through a constructor or method parameter can be unit-tested by passing in mock students. No real database, no real DOM, no real network. This is the design principle behind dependency injection frameworks in every major language.


Visual Prompts

[IMAGE: A whiteboard-style diagram showing four boxes: Teacher, Student, Course, License - connected by labeled arrows showing "teaches", "enrolls in", "holds" - clean dark background, neon green labels - search: "OOP association diagram class boxes arrows dark background"]

[CHART: Comparison table - Association vs Aggregation vs Composition - columns: Ownership, Lifecycle dependency, Directionality, Real example - Source: original lesson content]


Common Mistakes

1. Confusing association with inheritance. If you model "Teacher uses Student" with class Teacher extends Student, that is a category error. Use a reference, not extends. The "uses-a" test catches this: Student is not a kind of Teacher.

2. Making everything bidirectional. Bidirectional association is not more correct than unidirectional — it is more expensive. Every bidirectional reference you add is a synchronization point you must maintain. Default to unidirectional. Add the reverse only when the behavior requires it.

3. Ignoring lifecycle when choosing between association and composition. If you model a relationship as association but the referenced object cannot logically exist without the container, you likely have composition. The independence test is critical: can the referenced object exist on its own in your domain?

4. Creating tight coupling by instantiating dependencies inside the class. If Teacher creates Student objects inside its own constructor (this.students = [new Student(...)]), you have coupled Teacher to a specific Student implementation. Pass references in from outside. That keeps the association loose.

5. Forgetting to clean up bidirectional references. In long-running applications, objects that hold mutual references and are never dereferenced will not be garbage-collected. Always provide a remove method that clears both sides of a bidirectional association.


Interview Questions

Q1. What is association in OOP? How is it different from inheritance?

Association is a "uses-a" relationship between two independent objects. One object holds a reference to another and uses it, but neither is a specialized version of the other. Inheritance is an "is-a" relationship that creates a structural parent-child hierarchy. In association, both objects maintain separate lifecycles.

Q2. What is the difference between unidirectional and bidirectional association?

In unidirectional association, only one object holds a reference to the other. Navigation is possible in one direction only. In bidirectional association, both objects reference each other, allowing navigation from either side. Unidirectional is simpler and preferred by default. Bidirectional should be used only when both sides genuinely need to navigate the relationship.

Q3. How does association support loose coupling?

Association connects objects through references rather than structural inheritance. The associated objects interact through public interfaces. This means you can replace, mock, or extend one side without changing the other. Loose coupling reduces the ripple effect of changes and makes unit testing significantly easier.

Q4. What is the difference between one-to-many and many-to-many association? Give a real example.

In one-to-many association, one object uses multiple instances of another — for example, one Teacher works with many Students. In many-to-many association, multiple objects on both sides reference each other — for example, many Students enroll in many Courses, and each Course has many Students. Many-to-many associations often require a junction or registry object to manage the relationship in complex systems.

Q5. How is association different from aggregation and composition?

All three are "uses-a" or "has-a" relationships, but they differ in ownership and lifecycle:

  • Association: no ownership, both objects are fully independent.
  • Aggregation: one object logically contains another, but the contained object can exist independently (weak ownership).
  • Composition: one object owns another, and the owned object cannot exist without the owner (strong ownership, shared lifecycle).

[INTERNAL-LINK: aggregation details -> ch06 02-aggregation.md] [INTERNAL-LINK: composition details -> ch06 03-composition.md]


Quick Reference — Cheat Sheet

ASSOCIATION AT A GLANCE
-----------------------

Relationship type : "uses-a"
Ownership         : None — both objects are independent
Lifecycle         : Separate — one can exist without the other
UML notation      : Plain arrow (no diamond, no filled diamond)

CARDINALITY
-----------
One-to-One    : Driver ---uses---> License
One-to-Many   : Teacher ---uses---> [Student, Student, Student]
Many-to-One   : [Employee, Employee] ---uses---> Department
Many-to-Many  : [Student] ---uses---> [Course] (both sides reference each other)

DIRECTION
---------
Unidirectional : A knows about B | B does not know about A
                 Default choice — simpler, fewer sync issues
Bidirectional  : A knows about B | B knows about A
                 Use only when both sides need navigation

LOOSE COUPLING CHECKLIST
------------------------
- Pass dependencies in from outside (constructor or method params)
- Do not instantiate associated objects inside the class
- Depend on a minimal interface, not a concrete implementation
- Provide removal methods for bidirectional associations

CODE PATTERN (unidirectional one-to-many)
-----------------------------------------
class Teacher {
  constructor(name) {
    this.name = name;
    this.students = [];        // holds references, does not own them
  }
  addStudent(student) {
    this.students.push(student);
  }
}

class Student {
  constructor(name) {
    this.name = name;          // no reference back to Teacher
  }
}

INDEPENDENCE TEST
-----------------
Ask: "Can the referenced object exist without the container?"
Yes -> Association (or Aggregation)
No  -> Composition

[ORIGINAL DATA] The independence test and the unidirectional-first rule described above are distilled from reviewing common interview feedback patterns and real codebase refactoring work. They are not sourced from a single reference but reflect consistent design guidance across the OOP and software architecture community.


Previous: Lesson 5.5 - Overloading vs Overriding | Next: Lesson 6.2 - Aggregation


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

On this page