Aggregation (HAS-A)
Aggregation is a way of building objects out of other objects — one class HAS-A reference to another. Where inheritance says “a Dog IS-A Animal”, aggregation says “a Car HAS-A Engine”. It is one of the most practical tools in object-oriented design, and understanding when to use it (instead of inheritance) will make your code significantly more flexible.

The HAS-A Relationship
Aggregation describes a whole-part or uses-a relationship between two classes. The outer (containing) class holds a reference to the inner (contained) class, but neither class is a specialisation of the other.
Classic examples:
| Outer class | Inner class | Relationship |
|---|---|---|
Car | Engine | Car HAS-A Engine |
Library | Book | Library HAS-A Book |
Employee | Address | Employee HAS-A Address |
Order | Product | Order HAS-A Product |
The key question to ask yourself is: can the inner object exist independently of the outer one? If yes (an Address can exist without an Employee), the relationship is aggregation. If the inner object cannot survive on its own (a Room rarely makes sense without a House), it leans toward composition — a stricter form of aggregation.
Note: Java does not have separate keywords for aggregation vs composition. Both are implemented as a reference field. The distinction is a design concept, not a language construct.
A Simple Aggregation Example
Here an Employee HAS-A Address. Both classes can exist independently — you can create an Address object and later assign it to multiple employees.
class Address {
String street;
String city;
String country;
Address(String street, String city, String country) {
this.street = street;
this.city = city;
this.country = country;
}
@Override
public String toString() {
return street + ", " + city + ", " + country;
}
}
class Employee {
String name;
int id;
Address address; // HAS-A relationship
Employee(String name, int id, Address address) {
this.name = name;
this.id = id;
this.address = address;
}
void display() {
System.out.println("ID : " + id);
System.out.println("Name : " + name);
System.out.println("Addr : " + address);
}
}
public class Main {
public static void main(String[] args) {
Address addr = new Address("12 Oak Street", "Toronto", "Canada");
Employee emp = new Employee("Priya Sharma", 101, addr);
emp.display();
}
}
Output:
ID : 101
Name : Priya Sharma
Addr : 12 Oak Street, Toronto, Canada
The Address object is created independently and then passed in. Nothing stops you from assigning the same addr to a second Employee — the objects are loosely coupled.
Aggregation vs Inheritance
This is one of the most important design decisions you will make. A popular guideline is: “Favour composition over inheritance” (from the Gang of Four design patterns book).
| Aspect | Inheritance (IS-A) | Aggregation (HAS-A) |
|---|---|---|
| Relationship type | Specialisation | Ownership / usage |
| Keyword | extends | None — just a field |
| Coupling | Tight (subclass depends on superclass internals) | Loose (outer class depends only on public API) |
| Flexibility | Low — locked at compile time | High — reference can be swapped at runtime |
| Code reuse | Implicit (fields/methods inherited) | Explicit (delegation) |
| Example | Dog extends Animal | Car has an Engine field |
Tip: If you can say “B IS-A A” and it makes real-world sense in every context, use inheritance. If you find yourself forcing an IS-A that doesn’t hold universally, switch to aggregation.
Aggregation with Multiple Parts
A single class can hold references to several others — this is both natural and encouraged.
class Engine {
int horsepower;
Engine(int hp) { this.horsepower = hp; }
void start() { System.out.println("Engine started (" + horsepower + " hp)"); }
}
class Tyre {
String brand;
Tyre(String brand) { this.brand = brand; }
void describe() { System.out.println("Tyre brand: " + brand); }
}
class Car {
String model;
Engine engine; // HAS-A Engine
Tyre[] tyres; // HAS-A four Tyres
Car(String model, Engine engine, Tyre[] tyres) {
this.model = model;
this.engine = engine;
this.tyres = tyres;
}
void assemble() {
System.out.println("Assembling: " + model);
engine.start();
for (Tyre t : tyres) t.describe();
}
}
public class Main {
public static void main(String[] args) {
Engine v8 = new Engine(450);
Tyre[] wheels = {
new Tyre("Michelin"), new Tyre("Michelin"),
new Tyre("Michelin"), new Tyre("Michelin")
};
Car car = new Car("Mustang GT", v8, wheels);
car.assemble();
}
}
Output:
Assembling: Mustang GT
Engine started (450 hp)
Tyre brand: Michelin
Tyre brand: Michelin
Tyre brand: Michelin
Tyre brand: Michelin
Replacing Behaviour at Runtime
Because aggregation stores a reference, you can swap the contained object at runtime — something impossible with inheritance. This pattern underpins the Strategy and Dependency Injection design principles.
interface PaymentStrategy {
void pay(double amount);
}
class CreditCard implements PaymentStrategy {
public void pay(double amount) {
System.out.printf("Paid %.2f via Credit Card%n", amount);
}
}
class UPI implements PaymentStrategy {
public void pay(double amount) {
System.out.printf("Paid %.2f via UPI%n", amount);
}
}
class ShoppingCart {
private PaymentStrategy payment; // HAS-A PaymentStrategy
// Swap strategy at any time
void setPayment(PaymentStrategy payment) {
this.payment = payment;
}
void checkout(double total) {
payment.pay(total);
}
}
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPayment(new CreditCard());
cart.checkout(1299.99);
cart.setPayment(new UPI());
cart.checkout(499.00);
}
}
Output:
Paid 1299.99 via Credit Card
Paid 499.00 via UPI
Changing payment behaviour required zero changes to ShoppingCart — that is the power of aggregation combined with interfaces.
Under the Hood
Memory layout
When you write Address address; inside Employee, the JVM allocates a reference slot (4 or 8 bytes, depending on the JVM’s compressed-oops setting) inside the Employee object on the heap. The Address object itself lives at its own separate heap location.
Heap
├── Employee object ──address ref──► Address object
│ [id: 101] [street: "12 Oak Street"]
│ [name ref] [city: "Toronto"]
│ [address ref] ─────────────────► [country: "Canada"]
Because both objects are independently allocated, the garbage collector treats them separately. The Address remains reachable as long as at least one live reference points to it — even after the Employee is collected. This is fundamentally different from value embedding in languages like C++.
No bytecode magic
Aggregation compiles to ordinary getfield / putfield bytecode instructions. There is nothing special about it at the JVM level; it is simply a field that holds an object reference. The JIT compiler can inline short delegating methods, so calling emp.address.getCity() is often as fast as a direct field read after optimisation.
Warning: Avoid creating deeply nested aggregation chains (e.g.,
a.b.c.d.method()). This violates the Law of Demeter (“talk to your immediate friends only”), makes code harder to test, and increases coupling.
Common Mistakes
- Using inheritance when aggregation fits better. Ask “IS-A” strictly. A
Loggeris not aHashMap, even if it stores entries — it HAS-A map. - Null references. If the contained object is optional, guard with a null check or use Optional to make the absence explicit.
- Circular references. If
Aholds a reference toBandBholds a reference back toA, be careful withtoString(),equals(), and serialization — they can loop infinitely.
Related Topics
- Inheritance — understand IS-A before choosing between it and HAS-A
- Types of Inheritance — single, multilevel, hierarchical, and why Java avoids multiple class inheritance
- Interfaces — combine aggregation with interfaces for maximum flexibility
- Polymorphism — how swapping aggregated objects enables runtime polymorphism
- Classes & Objects — the building blocks you compose in aggregation
- Design Patterns — Strategy, Decorator, and many others are built on aggregation