Association, aggregation, composition
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
Listfield. - 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"
| Relationship | Meaning | Parts live without the whole? | Bank example |
|---|---|---|---|
| Association | One object knows another; the general case | Yes, both are independent | A customer uses an account |
| Aggregation | A whole holds parts that exist on their own | Yes | A bank has customers |
| Composition | A whole owns parts that make no sense without it | No | An 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:
- The whole calls
newfor the parts (the caller does not hand them in). - The list is
private finaland initialised in the declaration or constructor. - 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:
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 snapshotList<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)
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:
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):
1 1
Amal still existsThe 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)
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:
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());DEPOSIT 100.0
WITHDRAW 30.0
balance 70.0Callers 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:
acc.transactions().add(new Transaction("DEPOSIT", 1_000_000));With List.copyOf in the getter, the copy cannot be changed, so Java refuses:
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:
3 transactions, balance 70.0There 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 asdeposit.
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.)
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:
1 SamiThe 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.)
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:
3 x Pen
1 x NotebookIt is composition: the order creates its lines and keeps the list private.
B3. This method leaks. Fix it and say what the fix prevents:
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
- What is the difference between aggregation and composition?
- Which Java feature implements the "many" side?
- Who creates the parts in a composition?
- Why return
List.copyOf(...)rather than the field? - Is a Bank-Customer relationship composition? Justify.
Show answers
- In aggregation the parts exist independently of the whole; in composition they are owned by the whole and do not outlive it.
- A collection, such as
List. - The whole object does, usually inside its own methods.
- It stops callers changing the internal history without going through the whole's rules.
- 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.copyOfreturns an unmodifiable list that does not reflect later changes to the source, thataddappends, andsizereturns the number of elements. - Oracle, ArrayList (Java SE 21 API). Supports that
ArrayListimplementsList, 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.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.