Skip to main content

Association — Objects That Know Each Other

Editorial

Introduction

In a university system, a Professor teaches Courses, and Students enroll in those Courses. The professor doesn't own the student. The student doesn't own the course. They simply know about each other — they hold references that allow them to collaborate.

This is the most fundamental relationship between objects in OOP: Association. It means "these objects are connected" — they can communicate, send messages, and reference each other, but neither creates, owns, or controls the lifecycle of the other.

In S3, we learned how individual objects work — their attributes, methods, encapsulation. But real systems aren't built from isolated objects. They're built from objects that collaborate. This tutorial begins a four-part journey through OOP relationships:

  1. Association (this tutorial) — objects that know each other
  2. Aggregation (next) — "has-a" with independent lifecycle
  3. Composition (after that) — "has-a" with dependent lifecycle
  4. Dependency — objects that temporarily use each other

By understanding these relationships, you'll be able to draw correct UML class diagrams (S5) and make precise design decisions in every LLD interview.

What Is Association?

Association is a structural relationship between two (or more) classes that indicates objects of those classes are connected. One object "knows about" or "refers to" another.

Think of it like contacts in your phone. You have Alice's number, and Alice has yours. You're associated — you can call each other. But neither of you owns the other. If you delete Alice's contact, Alice still exists. If Alice loses her phone, you still exist. Your lifecycles are completely independent.

In code terms, association means one class holds a reference (attribute) to another class:

  • Object A has a field of type B
  • A can call methods on B through that reference
  • Neither A nor B controls the other's creation or destruction
  • Both can exist independently

Why association matters: It's the umbrella relationship in UML. Aggregation and Composition are special forms of association with additional lifecycle constraints. If you understand association first, understanding its specialized forms becomes much easier.

Types of Association

Association has two dimensions that define its nature:

Directionality

Unidirectional association: Only one class knows about the other. A Student holds a reference to their Advisor, but the Advisor class doesn't hold references back to students.

Bidirectional association: Both classes know about each other. A Professor holds a list of Course objects they teach, and each Course holds a reference back to its Professor. Both sides can navigate the relationship.

Multiplicity

Multiplicity defines how many objects participate on each side:

NotationMeaningExample
1Exactly oneA Course has exactly 1 Professor
0..1Zero or oneA Student may or may not have an Advisor
* or 0..*Zero or moreA Professor teaches zero or more Courses
1..*One or moreA Course has at least 1 enrolled Student
2..5Specific rangeA Team has 2 to 5 Members

Multiplicity is critical for UML class diagrams (S5) — it tells you whether a field should be a single reference or a collection.

UML Notation

UML class diagram of association notation: Professor, Course and Student joined by bidirectional associations (solid lines, no arrowheads) with multiplicity labels (a Course has exactly 1 Professor, a Professor teaches zero or more Courses, a Course has at least 1 Student), and a unidirectional association (open arrowhead) from Student to Advisor with the role name advisor and multiplicity 0..1

Visualization

Pick a relationship (Association / Aggregation / Composition / Dependency) and run the lifecycle test: delete one object and watch the other. Association and Aggregation leave the other object alive (independent lifecycle); Composition destroys the part with the whole (dependent lifecycle); Dependency never stored a reference at all.

Code Example

Let's model the university domain with clear associations between Professor, Course, and Student. Notice how objects hold references to each other but don't create or destroy each other.

from typing import List, Optional


class Student:
    """A student who can enroll in courses."""

    def __init__(self, student_id: str, name: str):
        self._student_id = student_id
        self._name = name
        self._enrolled_courses: List['Course'] = []  # Association: knows about courses

    @property
    def name(self) -> str:
        return self._name

    def enroll(self, course: 'Course') -> None:
        """Enroll in a course — creates a bidirectional association."""
        if course not in self._enrolled_courses:
            self._enrolled_courses.append(course)
            course.add_student(self)  # Establish the other direction
            print(f"  {self._name} enrolled in {course.title}")

    def drop(self, course: 'Course') -> None:
        """Drop a course — removes the association (doesn't destroy the course)."""
        if course in self._enrolled_courses:
            self._enrolled_courses.remove(course)
            course.remove_student(self)
            print(f"  {self._name} dropped {course.title}")

    def list_courses(self) -> None:
        course_names = [c.title for c in self._enrolled_courses]
        print(f"  {self._name}'s courses: {course_names}")


class Professor:
    """A professor who teaches courses."""

    def __init__(self, employee_id: str, name: str):
        self._employee_id = employee_id
        self._name = name
        self._courses: List['Course'] = []  # Association: knows about courses

    @property
    def name(self) -> str:
        return self._name

    def assign_course(self, course: 'Course') -> None:
        """Start teaching a course — bidirectional association."""
        if course not in self._courses:
            self._courses.append(course)
            course.set_professor(self)
            print(f"  Prof. {self._name} assigned to {course.title}")

    def list_courses(self) -> None:
        course_names = [c.title for c in self._courses]
        print(f"  Prof. {self._name} teaches: {course_names}")


class Course:
    """A course that has a professor and enrolled students."""

    def __init__(self, code: str, title: str, max_capacity: int = 30):
        self._code = code
        self._title = title
        self._max_capacity = max_capacity
        self._professor: Optional[Professor] = None  # Association: knows its professor
        self._students: List[Student] = []            # Association: knows its students

    @property
    def title(self) -> str:
        return self._title

    def set_professor(self, professor: Professor) -> None:
        self._professor = professor

    def add_student(self, student: Student) -> None:
        if len(self._students) < self._max_capacity:
            self._students.append(student)

    def remove_student(self, student: Student) -> None:
        self._students.remove(student)

    def get_info(self) -> None:
        prof_name = f"Prof. {self._professor.name}" if self._professor else "TBD"
        student_names = [s.name for s in self._students]
        print(f"  {self._code}: {self._title} | Professor: {prof_name} | Students: {student_names}")


# Demonstration — objects exist independently, references connect them
print("--- Creating objects independently ---")
alice = Student("S001", "Alice")
bob = Student("S002", "Bob")
prof_smith = Professor("P001", "Smith")
cs101 = Course("CS101", "Data Structures")
cs201 = Course("CS201", "Algorithms")

print("\n--- Establishing associations ---")
prof_smith.assign_course(cs101)
prof_smith.assign_course(cs201)
alice.enroll(cs101)
alice.enroll(cs201)
bob.enroll(cs101)

print("\n--- Querying through associations ---")
prof_smith.list_courses()
alice.list_courses()
cs101.get_info()

print("\n--- Removing an association (not destroying the object) ---")
alice.drop(cs101)  # Alice drops CS101 — but CS101 still exists
cs101.get_info()    # CS101 still exists with Bob enrolled
alice.list_courses()  # Alice still exists, now only in CS201
import java.util.ArrayList;
import java.util.List;

class Student {
    private final String studentId;
    private final String name;
    private final List<Course> enrolledCourses = new ArrayList<>(); // Association

    public Student(String studentId, String name) {
        this.studentId = studentId;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void enroll(Course course) {
        if (!enrolledCourses.contains(course)) {
            enrolledCourses.add(course);
            course.addStudent(this); // Bidirectional
            System.out.println("  " + name + " enrolled in " + course.getTitle());
        }
    }

    public void drop(Course course) {
        if (enrolledCourses.remove(course)) {
            course.removeStudent(this);
            System.out.println("  " + name + " dropped " + course.getTitle());
        }
    }

    public void listCourses() {
        List<String> names = enrolledCourses.stream().map(Course::getTitle).toList();
        System.out.println("  " + name + "'s courses: " + names);
    }
}

class Professor {
    private final String employeeId;
    private final String name;
    private final List<Course> courses = new ArrayList<>(); // Association

    public Professor(String employeeId, String name) {
        this.employeeId = employeeId;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void assignCourse(Course course) {
        if (!courses.contains(course)) {
            courses.add(course);
            course.setProfessor(this); // Bidirectional
            System.out.println("  Prof. " + name + " assigned to " + course.getTitle());
        }
    }

    public void listCourses() {
        List<String> names = courses.stream().map(Course::getTitle).toList();
        System.out.println("  Prof. " + name + " teaches: " + names);
    }
}

class Course {
    private final String code;
    private final String title;
    private final int maxCapacity;
    private Professor professor;                           // Association
    private final List<Student> students = new ArrayList<>(); // Association

    public Course(String code, String title, int maxCapacity) {
        this.code = code;
        this.title = title;
        this.maxCapacity = maxCapacity;
    }

    public String getTitle() {
        return title;
    }

    public void setProfessor(Professor professor) {
        this.professor = professor;
    }

    public void addStudent(Student student) {
        if (students.size() < maxCapacity) {
            students.add(student);
        }
    }

    public void removeStudent(Student student) {
        students.remove(student);
    }

    public void getInfo() {
        String profName = professor != null ? "Prof. " + professor.getName() : "TBD";
        List<String> studentNames = students.stream().map(Student::getName).toList();
        System.out.println("  " + code + ": " + title + " | Professor: " + profName + " | Students: " + studentNames);
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("--- Creating objects independently ---");
        Student alice = new Student("S001", "Alice");
        Student bob = new Student("S002", "Bob");
        Professor profSmith = new Professor("P001", "Smith");
        Course cs101 = new Course("CS101", "Data Structures", 30);
        Course cs201 = new Course("CS201", "Algorithms", 30);

        System.out.println("\n--- Establishing associations ---");
        profSmith.assignCourse(cs101);
        profSmith.assignCourse(cs201);
        alice.enroll(cs101);
        alice.enroll(cs201);
        bob.enroll(cs101);

        System.out.println("\n--- Querying through associations ---");
        profSmith.listCourses();
        alice.listCourses();
        cs101.getInfo();

        System.out.println("\n--- Removing an association (not the object) ---");
        alice.drop(cs101);
        cs101.getInfo();
        alice.listCourses();
    }
}

Association vs Other Relationships

Association is the most general relationship. It's useful to understand where it sits relative to the specialized forms you'll learn next:

RelationshipOwnership?Lifecycle coupling?UML symbolExample
AssociationNo ownershipIndependentSolid lineProfessor ↔ Course
AggregationWeak ownership ("has-a")IndependentOpen diamond ◇Department ◇→ Professor
CompositionStrong ownership ("owns-a")DependentFilled diamond ◆University ◆→ Department
DependencyNo ownershipTemporaryDashed arrow ┄→Student ┄→ Printer

The key insight: All four are about how objects relate. The difference is in ownership strength and lifecycle coupling. Association is the baseline — no ownership, no lifecycle coupling, just "I know about you."

In the next three tutorials, we'll progressively tighten these constraints using the same university domain, so you can see exactly how each relationship builds on Association.

Common Mistakes

Mistake 1: Confusing Association with Dependency

The problem: Treating every method call as an association. If a Student passes a Printer to a print_transcript() method and never stores it, that's a dependency (temporary use), not an association (stored reference).

The rule: Association implies a structural connection — one object holds an attribute referencing the other. Dependency implies temporary use — the reference exists only during a method call.

Mistake 2: Forgetting Bidirectional Synchronization

The problem: When Professor adds a Course to their list, but the Course's professor field isn't updated. The association is broken — navigable from one side but not the other.

The fix: Always maintain both sides of a bidirectional association. If professor.assignCourse(course) adds to the professor's list, it must also call course.setProfessor(this).

Mistake 3: Using Association When Composition is Appropriate

The problem: Modeling Order and OrderItem as a plain association. In reality, OrderItems don't make sense without their parent Order — they should use Composition (dependent lifecycle).

The rule: If the child object has no meaning without the parent → use Composition, not Association.

Key Takeaways

  • Association is the foundational OOP relationship — objects that know about each other through stored references
  • Neither object owns or controls the other's lifecycle — both exist independently
  • Associations can be unidirectional (one side knows) or bidirectional (both sides know)
  • Multiplicity specifies how many objects participate: 1, 0..1, *, 1..*
  • In UML, association is drawn as a solid line between classes — no diamonds, no arrowheads for bidirectional
  • Association is the umbrella — Aggregation (open diamond ◇) and Composition (filled diamond ◆) are specialized forms with additional lifecycle semantics
  • In the next tutorial, we'll tighten the relationship with Aggregation — where one object has another but doesn't control its lifecycle