Aggregation in OOP: Has-A Relationship Explained

Updated 2026-03-11

TL;DR

Aggregation is a “has-a” relationship where one object contains references to other objects as parts, but those parts can exist independently. Unlike composition, destroying the container doesn’t destroy the parts. Think of a university having students—the students can exist even if the university closes.

Prerequisites: Basic understanding of classes and objects, familiarity with object references and constructors, knowledge of lists/collections in Python. You should be comfortable creating classes with instance variables and methods.

After this topic: Implement aggregation relationships in your code, distinguish aggregation from composition and association, design class structures where objects contain independent parts, and explain the lifecycle implications of aggregated objects in technical interviews.

Core Concept

What is Aggregation?

Aggregation represents a whole-part relationship where the “whole” object contains references to “part” objects, but the parts have independent lifecycles. The container doesn’t own the parts—it merely uses them. If you delete the container, the parts continue to exist.

The key phrase is “has-a” with independence. A library has books, but books exist independently. A team has players, but players exist outside the team context.

Aggregation vs. Other Relationships

Aggregation vs. Composition: In composition, the whole owns the parts (strong ownership). Destroy a house, and its rooms are destroyed. In aggregation, parts are shared or independent. Destroy a playlist, and the songs still exist.

Aggregation vs. Association: Association is a general relationship between objects (uses, knows about). Aggregation is a specific type of association that emphasizes the whole-part structure. A customer placing an order is association. A shopping cart containing products is aggregation.

Characteristics of Aggregation

  1. Independent Lifecycle: Parts can be created before the whole and exist after the whole is destroyed.
  2. Shared Parts: Multiple containers can reference the same part objects.
  3. Weak Ownership: The container doesn’t control the part’s lifetime.
  4. Navigability: Typically one-way—the whole knows about parts, but parts don’t necessarily know about the whole.

When to Use Aggregation

Use aggregation when:

  • Objects have a clear whole-part relationship
  • Parts can exist independently or be shared
  • You’re modeling collections or groups
  • Parts might belong to multiple containers simultaneously

Common examples: Department-Employee, Course-Student, Playlist-Song, Team-Player, Library-Book.

Visual Guide

Aggregation Relationship Diagram

classDiagram
    class University {
        -name: string
        -students: List~Student~
        +add_student(student)
        +remove_student(student)
        +get_students()
    }
    class Student {
        -name: string
        -id: string
        +get_info()
    }
    University o-- Student : has
    note for University "Empty diamond indicates aggregation"
    note for Student "Students exist independently"

UML representation of aggregation. The empty diamond points to the whole (University). Students can exist without the university.

Aggregation vs Composition Lifecycle

graph TD
    A[Create Student Objects] --> B[Create University]
    B --> C[Add Students to University]
    C --> D[Delete University]
    D --> E[Students Still Exist]
    
    F[Create House] --> G[House Creates Rooms]
    G --> H[Delete House]
    H --> I[Rooms Are Destroyed]
    
    style E fill:#90EE90
    style I fill:#FFB6C6
    
    subgraph Aggregation
    A
    B
    C
    D
    E
    end
    
    subgraph Composition
    F
    G
    H
    I
    end

Lifecycle comparison: In aggregation, parts survive the container’s destruction. In composition, parts are destroyed with the container.

Examples

Example 1: University and Students

This example shows a university that aggregates students. Students exist independently and can be shared between multiple universities.

class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
    
    def get_info(self):
        return f"Student: {self.name} (ID: {self.student_id})"

class University:
    def __init__(self, name):
        self.name = name
        self.students = []  # Aggregation: references to existing students
    
    def add_student(self, student):
        if student not in self.students:
            self.students.append(student)
            print(f"{student.name} enrolled at {self.name}")
    
    def remove_student(self, student):
        if student in self.students:
            self.students.remove(student)
            print(f"{student.name} left {self.name}")
    
    def list_students(self):
        return [s.get_info() for s in self.students]

# Usage demonstrating independent lifecycle
student1 = Student("Alice", "S001")
student2 = Student("Bob", "S002")
student3 = Student("Charlie", "S003")

print("Students exist before university:")
print(student1.get_info())
print(student2.get_info())

# Create university and add existing students
uni = University("Tech University")
uni.add_student(student1)
uni.add_student(student2)

print("\nUniversity students:")
for info in uni.list_students():
    print(info)

# Delete university, students still exist
del uni
print("\nAfter university deletion, students still exist:")
print(student1.get_info())
print(student2.get_info())
print(student3.get_info())  # Never added to university

Expected Output:

Students exist before university:
Student: Alice (ID: S001)
Student: Bob (ID: S002)

Alice enrolled at Tech University
Bob enrolled at Tech University

University students:
Student: Alice (ID: S001)
Student: Bob (ID: S002)

After university deletion, students still exist:
Student: Alice (ID: S001)
Student: Bob (ID: S002)
Student: Charlie (ID: S003)

Key Points:

  • Students are created independently before the university
  • University stores references (not copies) to students
  • Deleting the university doesn’t affect student objects
  • Student3 never joined the university but exists independently

Java Equivalent Note: In Java, you’d use ArrayList<Student> and the same reference semantics apply. The garbage collector handles cleanup.


Example 2: Playlist and Songs (Shared Aggregation)

This example demonstrates how parts can be shared across multiple containers—a song can belong to multiple playlists.

class Song:
    def __init__(self, title, artist, duration):
        self.title = title
        self.artist = artist
        self.duration = duration  # in seconds
    
    def __str__(self):
        return f"{self.title} by {self.artist} ({self.duration}s)"

class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []  # Aggregation: songs can be in multiple playlists
    
    def add_song(self, song):
        self.songs.append(song)
        print(f"Added '{song.title}' to {self.name}")
    
    def get_total_duration(self):
        return sum(song.duration for song in self.songs)
    
    def list_songs(self):
        print(f"\n{self.name}:")
        for i, song in enumerate(self.songs, 1):
            print(f"  {i}. {song}")
        print(f"Total duration: {self.get_total_duration()}s")

# Create songs independently
song1 = Song("Bohemian Rhapsody", "Queen", 354)
song2 = Song("Stairway to Heaven", "Led Zeppelin", 482)
song3 = Song("Hotel California", "Eagles", 391)

# Create multiple playlists
rock_classics = Playlist("Rock Classics")
road_trip = Playlist("Road Trip Mix")

# Same songs can be in multiple playlists (shared aggregation)
rock_classics.add_song(song1)
rock_classics.add_song(song2)
rock_classics.add_song(song3)

road_trip.add_song(song2)  # song2 is in both playlists
road_trip.add_song(song3)  # song3 is in both playlists

rock_classics.list_songs()
road_trip.list_songs()

# Delete one playlist, songs still exist and are in the other playlist
del rock_classics
print("\nAfter deleting 'Rock Classics', songs still exist:")
print(song1)
print(song2)
road_trip.list_songs()

Expected Output:

Added 'Bohemian Rhapsody' to Rock Classics
Added 'Stairway to Heaven' to Rock Classics
Added 'Hotel California' to Rock Classics
Added 'Stairway to Heaven' to Road Trip Mix
Added 'Hotel California' to Road Trip Mix

Rock Classics:
  1. Bohemian Rhapsody by Queen (354s)
  2. Stairway to Heaven by Led Zeppelin (482s)
  3. Hotel California by Eagles (391s)
Total duration: 1227s

Road Trip Mix:
  1. Stairway to Heaven by Led Zeppelin (482s)
  2. Hotel California by Eagles (391s)
Total duration: 873s

After deleting 'Rock Classics', songs still exist:
Bohemian Rhapsody by Queen (354s)
Stairway to Heaven by Led Zeppelin (482s)

Road Trip Mix:
  1. Stairway to Heaven by Led Zeppelin (482s)
  2. Hotel California by Eagles (391s)
Total duration: 873s

Key Points:

  • Songs are created once and shared across multiple playlists
  • The same song object appears in multiple containers (shared aggregation)
  • Deleting one playlist doesn’t affect songs or other playlists
  • This is memory-efficient—no duplication of song data

Try it yourself: Add a method to remove a song from a playlist. What happens if you remove a song from one playlist? Does it affect other playlists? (Answer: No, because each playlist has its own list of references.)


Example 3: Department and Employees (With Bidirectional Navigation)

This example shows optional bidirectional navigation while maintaining aggregation semantics.

class Employee:
    def __init__(self, name, emp_id):
        self.name = name
        self.emp_id = emp_id
        self.departments = []  # An employee can be in multiple departments
    
    def join_department(self, department):
        if department not in self.departments:
            self.departments.append(department)
    
    def leave_department(self, department):
        if department in self.departments:
            self.departments.remove(department)
    
    def list_departments(self):
        return [dept.name for dept in self.departments]

class Department:
    def __init__(self, name):
        self.name = name
        self.employees = []  # Aggregation
    
    def add_employee(self, employee):
        if employee not in self.employees:
            self.employees.append(employee)
            employee.join_department(self)  # Bidirectional link
            print(f"{employee.name} joined {self.name}")
    
    def remove_employee(self, employee):
        if employee in self.employees:
            self.employees.remove(employee)
            employee.leave_department(self)  # Maintain consistency
            print(f"{employee.name} left {self.name}")
    
    def list_employees(self):
        return [emp.name for emp in self.employees]

# Create employees independently
emp1 = Employee("Alice", "E001")
emp2 = Employee("Bob", "E002")
emp3 = Employee("Charlie", "E003")

# Create departments
engineering = Department("Engineering")
research = Department("Research")

# Add employees to departments
engineering.add_employee(emp1)
engineering.add_employee(emp2)
research.add_employee(emp2)  # Bob works in both departments
research.add_employee(emp3)

print(f"\nEngineering employees: {engineering.list_employees()}")
print(f"Research employees: {research.list_employees()}")
print(f"\nBob's departments: {emp2.list_departments()}")

# Remove employee from one department
engineering.remove_employee(emp2)
print(f"\nAfter Bob leaves Engineering:")
print(f"Engineering employees: {engineering.list_employees()}")
print(f"Bob's departments: {emp2.list_departments()}")
print(f"Bob still exists: {emp2.name} ({emp2.emp_id})")

Expected Output:

Alice joined Engineering
Bob joined Engineering
Bob joined Research
Charlie joined Research

Engineering employees: ['Alice', 'Bob']
Research employees: ['Bob', 'Charlie']

Bob's departments: ['Engineering', 'Research']

Bob left Engineering

After Bob leaves Engineering:
Engineering employees: ['Alice']
Bob's departments: ['Research']
Bob still exists: Bob (E002)

Key Points:

  • Bidirectional navigation allows both objects to know about each other
  • Employees can belong to multiple departments (many-to-many aggregation)
  • Removing from one department doesn’t delete the employee
  • Consistency is maintained on both sides of the relationship

C++ Note: In C++, you’d use pointers or references. Be careful with raw pointers—consider using std::shared_ptr for aggregation to handle shared ownership properly.

Try it yourself: What happens if you delete a department? Modify the code to handle department deletion gracefully, ensuring employees are notified and their department lists are updated.

Common Mistakes

1. Confusing Aggregation with Composition

Mistake: Treating aggregated objects as if they’re owned by the container, or vice versa.

# WRONG: Creating parts inside the container (this is composition, not aggregation)
class Library:
    def __init__(self):
        self.books = [Book("Title1"), Book("Title2")]  # Books created here

# RIGHT: Accepting existing parts (aggregation)
class Library:
    def __init__(self):
        self.books = []  # Empty, will add existing books
    
    def add_book(self, book):  # Book created elsewhere
        self.books.append(book)

Why it matters: In interviews, you’ll be asked to justify your design choices. If you say “aggregation” but implement composition, it shows unclear thinking. The key question: “Who creates and owns the lifecycle of the part?“

2. Not Handling Shared References Properly

Mistake: Modifying aggregated objects without considering that other containers might reference them.

# WRONG: Modifying shared object state carelessly
class Team:
    def promote_player(self, player):
        player.level += 1  # Affects the player in ALL teams!

# BETTER: Be explicit about shared state
class Team:
    def promote_player(self, player):
        # Document that this affects the player globally
        player.level += 1
        print(f"Warning: {player.name} promoted globally, affects all teams")

Why it matters: Shared aggregation means changes to parts are visible everywhere. This can cause bugs if not understood. Always consider: “Is this part shared? Will my change affect other containers?“

3. Creating Circular References Without Cleanup

Mistake: Implementing bidirectional aggregation without proper cleanup logic.

# WRONG: Circular reference without cleanup
class Department:
    def add_employee(self, emp):
        self.employees.append(emp)
        emp.department = self  # Circular reference
    # No remove method to break the cycle!

# RIGHT: Provide cleanup methods
class Department:
    def add_employee(self, emp):
        self.employees.append(emp)
        emp.departments.append(self)
    
    def remove_employee(self, emp):
        self.employees.remove(emp)
        emp.departments.remove(self)  # Break the cycle

Why it matters: In languages without garbage collection (C++), circular references cause memory leaks. Even in Python/Java, they complicate object lifecycle. Always provide symmetrical add/remove operations.

4. Storing Copies Instead of References

Mistake: Creating copies of objects instead of storing references, breaking the aggregation pattern.

# WRONG: Creating a copy
import copy

class Playlist:
    def add_song(self, song):
        self.songs.append(copy.deepcopy(song))  # Creates a copy!

# RIGHT: Store the reference
class Playlist:
    def add_song(self, song):
        self.songs.append(song)  # Stores reference to the same object

Why it matters: Aggregation means sharing objects, not duplicating them. If you copy, changes to the original won’t reflect in the container, and you lose memory efficiency. This is a common interview trap question.

5. Not Checking for Duplicates in Collections

Mistake: Adding the same object multiple times to an aggregated collection.

# WRONG: No duplicate check
class Team:
    def add_player(self, player):
        self.players.append(player)  # Could add same player twice

# RIGHT: Check before adding
class Team:
    def add_player(self, player):
        if player not in self.players:
            self.players.append(player)
        else:
            print(f"{player.name} is already on the team")

Why it matters: Unless duplicates are intentional (like a song appearing twice in a playlist), you should prevent them. This shows attention to data integrity in interviews.

Interview Tips

Explaining Aggregation Clearly

When asked to explain aggregation: Use the “independent lifecycle” test. Say: “Aggregation is a has-a relationship where the parts can exist independently. If I delete the container, the parts survive. For example, a library has books, but if the library closes, the books still exist.”

Contrast with composition immediately: Interviewers often ask you to distinguish these. Have a ready example: “In composition, the whole owns the parts—like a house and its rooms. In aggregation, parts are independent—like a university and its students.”

Recognizing Aggregation in Design Questions

Look for these keywords in problem statements:

  • “can be shared between”
  • “exists independently”
  • “can belong to multiple”
  • “references to existing”

Example: “Design a system where courses have students” → This is aggregation because students exist independently and can take multiple courses.

Ask clarifying questions:

  • “Do the parts exist before the whole?”
  • “Can parts be shared between multiple containers?”
  • “What happens to parts when the container is destroyed?”

These questions show you’re thinking about object lifecycles, which impresses interviewers.

Coding Aggregation in Interviews

Always demonstrate independent creation:

# Show parts created first
student1 = Student("Alice")
student2 = Student("Bob")

# Then added to container
course = Course("CS101")
course.add_student(student1)
course.add_student(student2)

Mention memory implications: “I’m using aggregation here because students are shared across courses. This is memory-efficient—we store references, not copies, so each student object exists only once in memory.”

Discuss thread safety (for senior roles): “Since multiple containers can reference the same aggregated object, we need to consider thread safety if multiple threads modify shared parts. I might use locks or immutable objects depending on requirements.”

Common Interview Scenarios

Scenario 1: “Design a shopping cart system”

  • Products are aggregated (they exist in inventory independently)
  • Cart items might be composition (cart-specific quantity/price)
  • Show you understand the difference

Scenario 2: “When would you use aggregation vs. composition?”

  • Answer: “Use aggregation when parts have independent meaning and lifecycle. Use composition when parts are integral to the whole and have no purpose outside it. For example, a car’s engine is composition, but passengers are aggregation.”

Scenario 3: “How do you handle cleanup in aggregation?”

  • Answer: “In aggregation, the container doesn’t delete parts. I’d implement a clear() method that removes references but doesn’t destroy objects. In Python, garbage collection handles the rest. In C++, I’d use shared_ptr for shared ownership.”

Red Flags to Avoid

  • Don’t say “aggregation is just a weak composition” (shows shallow understanding)
  • Don’t ignore the shared reference implications
  • Don’t forget to mention UML notation if asked (empty diamond)
  • Don’t implement aggregation with deep copies (breaks the pattern)

Bonus Points

Mention real-world examples: “This is like how Spotify handles playlists—songs are aggregated because the same song appears in millions of playlists, but only one copy exists in their database.”

Discuss trade-offs: “Aggregation gives us flexibility and memory efficiency, but we need to be careful about shared state. If one container modifies an aggregated object, all containers see the change. Sometimes we need immutable objects to prevent this.”

Key Takeaways

  • Aggregation is a has-a relationship with independent lifecycles: The container references parts, but parts can exist before, during, and after the container’s lifetime. Deleting the container doesn’t destroy the parts.

  • Parts can be shared across multiple containers: The same object can be aggregated by multiple containers simultaneously (like a song in multiple playlists), making aggregation memory-efficient for shared resources.

  • Aggregation differs from composition in ownership: Composition means strong ownership (whole creates and destroys parts), while aggregation means weak ownership (whole uses existing parts). Use the “independent existence” test to distinguish them.

  • Implement aggregation by accepting existing objects: Create parts independently, then pass them to the container. Store references (not copies) in collections, and provide add/remove methods that don’t destroy the parts.

  • Consider shared state implications: When parts are shared, modifications are visible to all containers. Document this behavior, check for duplicates when appropriate, and maintain bidirectional consistency if implementing two-way navigation.