Association, Aggregation and Composition in Java
Association, aggregation and composition are three kinds of relationships between classes in Java that describe the HAS-A ("has") relation. Association is any link between objects; aggregation is a "part–whole" link where the part can live without the whole; composition is a strict "part–whole" link where the part is created by the whole and dies with it. A separate relation, IS-A ("is"), is expressed through class inheritance and interface implementation.
Almost every class in a real application is connected to other classes somehow. In this lesson we look at two classifications of class relationships in Java: the coarse one (IS-A / HAS-A) and the more precise one (association, aggregation, composition), plus the classic rule "favor composition over inheritance".
1. Relationships based on the class hierarchy: IS-A and HAS-A
Let's start with the coarsest classification, the one based on the class hierarchy: the IS-A and HAS-A relationships.

1.1. IS-A relationship ("is a")
In object-oriented programming the IS-A principle is based on class inheritance or interface implementation.
For example, if the class HeavyBox extends Box, we say that HeavyBox is a Box (HeavyBox IS-A Box):

class Box {
private double width;
private double height;
private double depth;
…
}
class HeavyBox extends Box {
private int weight;
…
} Another example: the class Lorry (a truck) extends the class Car:

class Car {
…
}
class Lorry extends Car {
…
} Here Lorry IS-A Car.
One more example, the classes Person and Driver:

class Person {
…
}
class Driver extends Person {
…
} Driver IS-A Person.
The same applies to interfaces. If the class Transport implements the interface Moveable, they are in a Transport IS-A Moveable relationship:

interface Moveable {
…
}
class Transport implements Moveable {
…
} You can verify an IS-A relationship at runtime with the instanceof operator: the expression transport instanceof Moveable returns true, because Transport IS-A Moveable.
1.2. HAS-A relationship ("has a")
A HAS-A relationship is based on usage: one class keeps a reference to an object of another class in a field.
For example, the class Car holds a variable of type Engine:

class Car {
Engine engine;
…
}
class Engine {
…
} We say: Car HAS-A Engine.
Or a Shop that holds an array of Category:

class Shop {
Category[] categories;
…
}
class Category {
…
} This is a HAS-A relationship too: Shop HAS-A Category.
A quick way to pick the right relationship
Say the two classes out loud in a sentence. If "A is a B" sounds right, you have IS-A and inheritance. If "A has a B" sounds right, you have HAS-A: association, aggregation or composition. A lorry is a car, but a car has an engine — so extending Car from Engine would be nonsense.
2. A finer classification: association, aggregation and composition
The HAS-A relationship is too broad on its own, so in design we split it into three more precise kinds of link: association, aggregation and composition.

2.1. Association
Let's start with association. In this relationship objects of two classes may refer to each other. An association can be unidirectional (only one class knows about the other) or bidirectional (both keep references to each other).
For example, the class Person holds a variable of type Car, which means there is an association between Person and Car:

class Person {
private String name;
private Car car;
…
}
class Car {
…
} An association also has a multiplicity: one-to-one (a person owns one car), one-to-many (a shop has many categories), many-to-many (students and courses). In code, the "many" side is expressed with a collection or an array.
Aggregation and composition are special cases of association. Aggregation is a relationship where one object is part of another but can exist on its own. Composition is an even tighter link: the part not only belongs to the whole, it cannot belong to anyone else and does not outlive its owner. The code below makes the difference obvious.
2.2. Aggregation
A Keyboard object is created outside and passed into the PC constructor to establish the link. If the PC object is discarded, the Keyboard object can still be used — as long as something else keeps a reference to it:

public class PC {
private Keyboard keyboard;
public PC(Keyboard keyboard) { // the object comes from outside
this.keyboard = keyboard;
}
} Keyboard keyboard = new Keyboard();
PC pc = new PC(keyboard);
pc = null; // we don't need the computer any more
keyboard.press('A'); // the keyboard is still alive and usable A real-life example of aggregation: a department and its employees. Disband the department and the employees do not vanish — they are simply moved to another one.
public class Department {
private final List<Employee> employees;
public Department(List<Employee> employees) {
this.employees = employees; // employees exist independently
}
} 2.3. Composition
Now let's look at composition. The Keyboard object is created inside the constructor, which means a much tighter link between the objects. Such a Keyboard was never meant to be a standalone object and does not outlive the PC that created it:

public class PC {
private final Keyboard keyboard;
public PC() {
this.keyboard = new Keyboard(); // the part is born with the PC
}
} The textbook example of composition is a house and its rooms: demolish the house and the rooms are gone with it.
public class House {
private final List<Room> rooms = new ArrayList<>();
public House(int roomCount) {
for (int i = 0; i < roomCount; i++) {
rooms.add(new Room(i)); // the house creates its own rooms
}
}
public List<Room> getRooms() {
return List.copyOf(rooms); // hand out a copy, not the real list
}
} Important
In Java, composition is a design convention, not a language guarantee. If a getter leaks a reference to the internal object, that object outlives its owner and the link silently degrades into aggregation. That is why parts in a composition are kept in private final fields and exposed only as copies or immutable views.
3. Aggregation vs composition: the difference
In one line: aggregation means "the part can live without the whole and may belong to several wholes", while composition means "the part is created by the whole, belongs only to it and dies with it". Association is the umbrella term — both aggregation and composition are flavours of it.
| Criterion | Association | Aggregation | Composition |
|---|---|---|---|
| Meaning of the link | "knows about", uses | "part–whole", weak | "part–whole", strong |
| Who creates the part | Does not matter | External code; passed into a constructor or setter | The whole itself, inside its constructor |
| Lifecycle | Independent | The part outlives the whole | The part dies with the whole |
| Can the part belong to several wholes | Yes | Yes | No |
| UML notation | Plain line (with an arrow if unidirectional) | Hollow (white) diamond at the whole | Filled (black) diamond at the whole |
| Example | Person — Car | Department — Employee, PC — Keyboard | House — Room, Human — Heart |
3.1. One more link: dependency
Interviewers often add a fourth option to the list — dependency. It is the weakest link of all: the class does not store the object in a field, it only receives it as a method parameter or creates it locally:
class ReportService {
// No Printer field - just a dependency on it
void print(Printer printer) {
printer.print(buildReport());
}
} Ordered by coupling strength, the relationships line up like this: dependency → association → aggregation → composition → inheritance.
4. Composition over inheritance
Object-oriented design has an important rule: favor composition over inheritance. It was stated back in Design Patterns by the Gang of Four and repeated by Joshua Bloch in Effective Java.
4.1. Why too much inheritance hurts
Inheritance is a powerful tool, but overusing it creates problems:
- Fragile code. A change in the base class can quietly break dozens of subclasses.
- Broken encapsulation. A subclass often depends on the implementation details of its parent, not just on its contract.
- Rigid hierarchy. You cannot swap the parent at runtime: the link is fixed at compile time.
- No multiple inheritance. Java does not let a class extend two classes at once. If you need the abilities of both a printer and a scanner, you cannot build an all-in-one device through inheritance — but composition handles it easily.
4.2. Example: an employee and their role
Imagine we are building an HR system.
The weak approach (inheritance):
class Employee {}
class Driver extends Employee {} // what if the driver becomes a manager?
// we would have to create a new object The better approach (composition): instead of saying that an employee is a driver, say that an employee has a role.
class Role {}
class DriverRole extends Role {}
class ManagerRole extends Role {}
class Employee {
private Role role; // composition: an employee "has a" role
public void setRole(Role role) {
this.role = role; // the role can be changed at any moment
}
} 4.3. When to choose which
- Inheritance — only when the subclass is a full substitute for its parent (the Liskov substitution principle) and you control both classes. If you extend a class merely to reuse someone else's methods, use composition instead.
- Composition — when you assemble complex objects out of simple building blocks and want to change behaviour on the fly by plugging in different implementations.
Watch out
Deep inheritance trees (more than three levels) make code hard to read and hard to test. The shorter the family tree of your classes, the more stable the system. Composition plus delegation almost always gives a more flexible design than one more level of extends.
5. Where developers get it wrong
- Assuming every object field means composition. A field on its own only proves an association. Composition is decided by who controls the lifecycle of the part.
- Breaking composition with a getter.
return rooms;instead ofreturn List.copyOf(rooms);hands out internal state — and the part starts living a life of its own. - Bidirectional association without synchronisation. If
Personpoints toCarandCarpoints back toPerson, it is easy to end up with inconsistent data, infinite recursion intoString()orequals(), and memory leaks in caches. - Extending a class just to reuse code. The classic anti-example is
class Stack extends ArrayList: the stack inheritsadd(int, E)and stops being a stack. Keeping the list in a field is the right call. - Confusing IS-A with HAS-A. "A car is an engine" sounds wrong, so
extendshas no place here.
6. Key takeaways
- IS-A — inheritance or interface implementation (
extends,implements). - HAS-A — one object is stored in a field of another object.
- Association — a general link between objects, unidirectional or bidirectional.
- Aggregation — the part comes from outside and survives without the whole (hollow diamond in UML).
- Composition — the part is created by the whole and dies with it (filled diamond in UML).
- Composition over inheritance — flexibility and loose coupling instead of a rigid hierarchy.
Frequently asked questions
Are aggregation and composition IS-A or HAS-A?
Both are HAS-A: the object keeps a reference to another object in a field. IS-A appears only when a class extends another class or implements an interface. So aggregation and composition are refinements of HAS-A by coupling strength and by the lifecycle of the part.
What is the difference between association and dependency?
With an association the object is stored in a field, so the link lasts as long as the owner lives. With a dependency the object arrives only as a method parameter, a local variable or a return value, so the link exists only for the duration of the call. Dependency is the weakest of all the relationships.
Can you break composition by accident in Java?
Yes. Java does not stop you from returning a reference to an internal object: one getter that hands out the actual list or the actual object is enough for the part to outlive its owner, turning composition into aggregation. To avoid it, keep parts in private final fields and return copies, immutable views or plain values.
How do you implement a many-to-many association?
Each class keeps a collection of references to the other one: a student holds a list of courses, a course holds a list of students. Add the link in a single method that updates both sides, otherwise the data goes out of sync. In JPA such a relationship is mapped with the ManyToMany annotation and a join table.
Comments