Skip to content
Ameen Abozaid
PublishedBEG-025-L02

Association, aggregation, composition

  • Lesson 2 of 5
  • 9 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

In the last lesson you saw that an object knows another through a reference kept in a field, a parameter, or a local variable. A field link is the interesting one, and it comes in different strengths. In the family of contacts picture: you know your neighbour (association), your football team has players who also exist outside the team (aggregation), and your body has organs that do not exist without it (composition).

This lesson teaches the three "has" relationships, how to spot each one in code, and how to store the "many" side of a one-to-many relationship with a list.

Why this matters: the choice decides who creates the parts, who can delete them, and whether the whole may hand out its internals. Getting it wrong is how a bank ends up with transaction histories that anyone can edit.

This lesson builds on "How objects know each other", on encapsulation (private fields, defensive copies) and on final.

Learning Objectives

By the end of this lesson you will be able to:

  • Define association, aggregation, and composition, and say how they differ in ownership and lifetime.
  • Recognise each one in a short piece of Java code.
  • Implement a one-to-many relationship with a List field.
  • Implement composition: the whole creates the parts and does not expose its internal list.
  • Explain why a getter that returns the live list breaks composition.
  • Argue for a classification in a borderline case.

Written Explanation

Three kinds of "has"

RelationshipMeaningParts live without the whole?Bank example
AssociationOne object knows another; the general caseYes, both are independentA customer uses an account
AggregationA whole holds parts that exist on their ownYesA bank has customers
CompositionA whole owns parts that make no sense without itNoAn account has transactions

All three are "has-a" and all three are usually a field. What differs is the ownership story. Many sources treat aggregation and composition as special cases of association. In practice the line between association and aggregation is blurry, and some teams skip the distinction. We keep the three names because they teach the question to ask: who creates the parts, and what happens to them when the whole goes away?

Aggregation: the whole receives existing parts

An aggregate gets its parts from outside. The parts were created elsewhere, may be held by other objects too, and survive when the whole is discarded.

Composition: the whole creates and owns its parts

In a composition the whole creates the parts, keeps them private, and when the whole is discarded the parts go with it. A transaction only has meaning inside its account's history. Three practical signs in code:

  1. The whole calls new for the parts (the caller does not hand them in).
  2. The list is private final and initialised in the declaration or constructor.
  3. The class never returns the live list.

The "many" side: a list (preview)

One-to-many needs a field that can hold any number of references. A single field only holds one. The standard choice is a List. The List type is taught properly in a later collections module; here is the minimum you need, which is a preview:

java
import java.util.ArrayList;
import java.util.List;

private final List<Transaction> transactions = new ArrayList<>();
transactions.add(t);      // append
transactions.size();      // how many
List.copyOf(transactions) // an unmodifiable snapshot

List<Transaction> means "a list of Transaction references"; ArrayList is a ready-made implementation; for (Transaction t : list) visits each element, just as it did for arrays. The Java API documentation says List.copyOf returns an unmodifiable list holding the elements, and that later changes to the original do not appear in the copy.

Why not hand out the list?

If the getter returns the field itself, any caller can add or remove elements, bypassing the whole's rules. The whole should expose the contents in a safe form: a copy (as above), or methods like deposit that make the changes. This is the defensive-copy idea from the immutability lesson, applied to a collection.

Examples

Example 1: aggregation, a bank and its customers (simple)

java
class Bank {
    private final List<Customer> customers = new ArrayList<>();   // aggregation
    void add(Customer customer) { customers.add(customer); }      // receives existing objects
    int customerCount() { return customers.size(); }
}

In main:

java
Customer amal = new Customer("Amal");
Bank north = new Bank();
Bank south = new Bank();
north.add(amal);
south.add(amal);
System.out.println(north.customerCount() + " " + south.customerCount());
north = null;                                  // the bank object is gone
System.out.println(amal.getName() + " still exists");

Output (JDK 21):

text
1 1
Amal still exists

The same Customer sits in two banks and survives when one bank is dropped. The customer was created outside Bank and passed in: that is aggregation.

Example 2: composition, an account and its transactions (practical)

java
class Transaction {
    private final String type;
    private final double amount;
    Transaction(String type, double amount) {
        this.type = type;
        this.amount = amount;
    }
    String describe() { return type + " " + amount; }
}

class Account {
    private final List<Transaction> transactions = new ArrayList<>(); // composition
    private double balance;

    void deposit(double amount) {
        balance += amount;
        transactions.add(new Transaction("DEPOSIT", amount));   // the whole creates the part
    }
    void withdraw(double amount) {
        balance -= amount;
        transactions.add(new Transaction("WITHDRAW", amount));
    }
    double getBalance() { return balance; }
    List<Transaction> transactions() { return List.copyOf(transactions); }
}

Using it:

java
Account acc = new Account();
acc.deposit(100);
acc.withdraw(30);
for (Transaction t : acc.transactions()) {
    System.out.println(t.describe());
}
System.out.println("balance " + acc.getBalance());
text
DEPOSIT 100.0
WITHDRAW 30.0
balance 70.0

Callers never create Transaction objects themselves; they call deposit and withdraw, and the account records the history.

Example 3: what happens when the list leaks (realistic)

Add one more line to the end of main:

java
acc.transactions().add(new Transaction("DEPOSIT", 1_000_000));

With List.copyOf in the getter, the copy cannot be changed, so Java refuses:

text
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
	at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
	at Main.main(Main.java:60)

Now change the getter to return transactions; and print the size and balance afterwards. The fake deposit goes through:

text
3 transactions, balance 70.0

There are now three transactions but the balance did not change. The history and the balance disagree, and nothing checked the new entry. That is why a composite does not hand out its internal list: the whole must keep control of its parts.

Backend Insight

Entity relationships in backend code are exactly these. An Order that creates its OrderLines is composition, and in a database the lines usually get deleted with the order. A Customer shared by many orders is association. When you meet persistence frameworks later, you will state for each "has" whether the children are removed with the parent: the very question this lesson trains.

Composition also protects consistency. A balance that can be changed only through deposit and withdraw cannot disagree with its history. That is the same protect-the-invariant rule from the encapsulation module, now applied to a group of objects.

Common Mistakes

Mistake 1: Returning the live list

  • Incorrect: List<Transaction> transactions() { return transactions; }
  • Correct: return List.copyOf(transactions);, or expose specific operations such as deposit.

Mistake 2: Creating parts outside and passing them in, for a composition

If callers can build Transaction objects and hand them to Account, the account no longer controls its history. Let the whole create the parts.

Mistake 3: Treating aggregation as ownership

If a Bank deletes customers when it closes, you have composition. If the same Customer object is also held elsewhere, deleting it from one bank is not "destroying" it. Decide who owns the lifetime.

Mistake 4: Using a single field for "many"

Writing Transaction transaction; for an account with a history keeps only the last one (or the first). A "many" side needs a collection.

Mistake 5: Spending hours on the label

Association versus aggregation is often a matter of judgement. State your reason in a sentence. The code and the diagram should agree, and the labelling should help the next reader.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Classify each as association, aggregation, or composition and give one reason: (a) a playlist and the songs it holds (songs can be on other playlists); (b) an invoice and its invoice lines; (c) a driver and the car they currently drive.

Show solution A1

A1. (a) Aggregation: songs exist independently and can be shared. (b) Composition: lines have no meaning outside the invoice. (c) Association: both exist independently and one simply knows the other.

A2. Which relationship says "the parts are created by the whole and die with it"?

Show solution A2

A2. Composition.

A3. Why is Transaction t; the wrong field for "an account has many transactions"?

Show solution A3

A3. It holds one reference, so it can only remember one transaction. A one-to-many link needs a collection such as List<Transaction>.

Level B: Application

B1. Write a Team class that holds Player objects received through add(Player p). Show that after one team variable is set to null, a Player still works. Name the relationship.

Show solution B1

B1. (We compiled and ran this.)

java
class Team {
    private final List<Player> players = new ArrayList<>();
    void add(Player player) { players.add(player); }
    int size() { return players.size(); }
}

With sami added to red and blue, then red = null;, the line System.out.println(blue.size() + " " + sami.getName()); prints:

text
1 Sami

The relationship is aggregation: the team receives players that exist outside it.

B2. Write an Order class with addLine(String product, int quantity) that creates OrderLine objects internally and a lines() method that returns them safely. Name the relationship.

Show solution B2

B2. (Compiled and run.)

java
class Order {
    private final List<OrderLine> lines = new ArrayList<>();
    void addLine(String product, int quantity) {
        lines.add(new OrderLine(product, quantity));
    }
    List<OrderLine> lines() { return List.copyOf(lines); }
}

Adding Pen x3 and Notebook x1 and looping over lines() prints:

text
3 x Pen
1 x Notebook

It is composition: the order creates its lines and keeps the list private.

B3. This method leaks. Fix it and say what the fix prevents:

java
List<OrderLine> lines() { return lines; }
Show solution B3

B3. return List.copyOf(lines);. It prevents callers from adding, removing, or replacing lines behind the order's back, because the copy is unmodifiable.

Level C: Challenge

C1. A Customer holds a List<Account> created by openAccount. Argue whether Customer-Account is association or composition. Mention what you would need to know about the bank's rules.

Show solution C1

C1. Both are defensible. If an account can be transferred to another customer, or can outlive its owner (for example, joint accounts), it is association. If an account has meaning only for one owner and is closed when the customer is removed, it is closer to composition. The bank's rules decide, and the point is to say why.

C2. Someone writes new Account() in two different places and passes the same List<Transaction> into both, hoping they share history. Explain why that breaks composition, and what to do instead.

Show solution C2

C2. The two accounts would write into one list, so each account's history contains the other's transactions, and each balance disagrees with its history. A composition means each whole creates and owns its own list. If two accounts must share data, model it as a different relationship, not as a shared internal list.

Mini Challenge

Take the Bank, Customer, Account, Transaction set. On paper, write one sentence for each pair that says the relationship type and why, for example "Account-Transaction: composition, because history is created by the account and deleted with it." Then write the field declaration for each (for example, private final List<Transaction> transactions = new ArrayList<>();) and mark which ones are private final.

Check your work: every "many" side is a collection, no getter returns a live list, and composition parts are created by the whole.

Summary & Next Lesson

Key takeaways

  • Association: one object knows another. Aggregation: a whole holds independent parts. Composition: a whole creates and owns parts that die with it.
  • Ask: who creates the parts, and what happens to them when the whole goes away?
  • One-to-many is a collection field such as List<Transaction> (a preview of the collections module).
  • A composite creates its parts, keeps the list private, and returns a copy or exposes operations.
  • Borderline cases are judgement calls. Give a reason.

Next lesson

The next lesson, "Dependency", covers the weakest relationship: a class that only uses another class briefly, with no field at all.

Self-check

  1. What is the difference between aggregation and composition?
  2. Which Java feature implements the "many" side?
  3. Who creates the parts in a composition?
  4. Why return List.copyOf(...) rather than the field?
  5. Is a Bank-Customer relationship composition? Justify.
Show answers
  1. In aggregation the parts exist independently of the whole; in composition they are owned by the whole and do not outlive it.
  2. A collection, such as List.
  3. The whole object does, usually inside its own methods.
  4. It stops callers changing the internal history without going through the whole's rules.
  5. Normally aggregation: customers exist independently of one bank object and may be held by several.

References

  • Oracle, List (Java SE 21 API). Supports that List.copyOf returns an unmodifiable list that does not reflect later changes to the source, that add appends, and size returns the number of elements.
  • Oracle, ArrayList (Java SE 21 API). Supports that ArrayList implements List, and that its no-argument constructor creates an empty list.
  • Visual Paradigm, UML aggregation vs composition. Supports that both are forms of association and that the difference is lifetime: aggregated parts can exist without the whole, composed parts depend on it.
  • The remark that some teams skip the aggregation distinction is experience-based and not verified from a primary source. All Java code was compiled and run with JDK 21.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.