Association — Objects That Know Each Other
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:
- Association (this tutorial) — objects that know each other
- Aggregation (next) — "has-a" with independent lifecycle
- Composition (after that) — "has-a" with dependent lifecycle
- 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:
| Notation | Meaning | Example |
|---|---|---|
1 | Exactly one | A Course has exactly 1 Professor |
0..1 | Zero or one | A Student may or may not have an Advisor |
* or 0..* | Zero or more | A Professor teaches zero or more Courses |
1..* | One or more | A Course has at least 1 enrolled Student |
2..5 | Specific range | A 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
Visualization

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 CS201import 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:
| Relationship | Ownership? | Lifecycle coupling? | UML symbol | Example |
|---|---|---|---|---|
| Association | No ownership | Independent | Solid line | Professor ↔ Course |
| Aggregation | Weak ownership ("has-a") | Independent | Open diamond ◇ | Department ◇→ Professor |
| Composition | Strong ownership ("owns-a") | Dependent | Filled diamond ◆ | University ◆→ Department |
| Dependency | No ownership | Temporary | Dashed 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