Composition: strong ownership with shared lifecycles
Understand composition in OOP: a strong has-a relationship where the whole owns its parts and controls their lifecycle, with Java examples and UML notation.
Most candidates draw a diamond on a UML diagram and call it "has-a." They never clarify whether the part can outlive the parent. That distinction is the entire point. Composition means the whole owns the part completely: it creates it, it controls it, and when the whole is destroyed, the part goes with it. Get this wrong and your domain model leaks objects that should not exist, or deletes objects that something else still needs.
A House composes Room objects. Demolish the house and the rooms are gone. They have no meaning, no address, no purpose outside the house that contains them. That tight binding is what separates composition from aggregation, and it is the single most testable concept in UML class relationships.
What Is Composition
Composition is a "has-a" relationship where the whole owns the parts and controls their lifecycle. The whole creates the parts (typically at construction time), and when the whole is destroyed, the parts are destroyed too. Parts do not exist independently and are not shared between multiple wholes.
Think of an invoice and its line items. The line items are born when the invoice is created. They have no identity outside of that invoice. You cannot take a line item and attach it to a different invoice. If you void the invoice, the line items are voided with it. The invoice is the whole; the line items are parts bound to its lifecycle.
For your interview: composition means "I create these things, they belong only to me, and they die with me."
Composition is the strongest has-a relationship
In UML, the relationship hierarchy goes: dependency (weakest) β association β aggregation β composition (strongest). Each step adds tighter coupling. Composition adds lifecycle ownership on top of aggregation's whole-part semantics. It is the tightest binding two classes can have short of inheritance.
UML Notation
Composition uses a filled (solid) diamond on the "whole" side of the relationship. The diamond sits on the class that owns and controls the parts. The line points toward the part.
Compare all three relationship types side by side:
| Symbol | UML Name | Meaning | Diamond |
|---|---|---|---|
--> | Association | Objects reference each other, no ownership | No diamond |
o-- | Aggregation | Whole groups parts, parts survive deletion | Hollow (open) |
*-- | Composition | Whole owns parts, parts die with whole | Filled (solid) |
The filled diamond is the visual cue that says "this part has no independent existence." If you draw a hollow diamond when you mean composition, you are telling the interviewer that the part survives independently, which changes the domain model entirely.
Notice the pattern: Order composes OrderLine, Car composes Engine. An order line without its order is meaningless. An engine without its car is scrap metal (in this domain). Neither part has an identity or purpose outside its whole.
Lifecycle Ownership
The defining feature of composition is lifecycle control. The whole is responsible for three things:
- Creation. The whole creates its parts internally (often in the constructor). Parts are not passed in from outside.
- Exclusive ownership. Each part belongs to exactly one whole. No sharing.
- Destruction. When the whole is destroyed, the parts are destroyed with it. No orphans.
This lifecycle contract has real consequences in code:
- No setter for the part. If external code can swap out the engine, the car does not truly own it. Composition means the whole controls the part from birth to death.
- No sharing. If two houses referenced the same room object, deleting one house would destroy a room that the other house still uses. Composition requires exclusive ownership.
- Deep copy on access. If you return a direct reference to an internal part, external code can mutate it. That breaks encapsulation and weakens the ownership contract.
The practical test I use: can the part exist in isolation? Can you hand it to another whole? If the answer to both is "no," you are looking at composition.
Injection does not automatically mean aggregation
Dependency injection frameworks often pass parts into constructors for testability. A Car receiving an Engine via constructor injection does not automatically make it aggregation. The real test is domain intent: does the engine have meaning outside this specific car? In a factory simulation, no. In a salvage yard system, possibly yes. Code mechanics are a hint, not a verdict.
Implementation
// Room is a composed part. It is created by the House and has
// no meaningful existence outside of it. Note: package-private
// constructor prevents external instantiation.
public class Room {
private final String name;
private final int areaSqFt;
// Package-private: only House (in same package) can create rooms.
Room(String name, int areaSqFt) {
this.name = name;
this.areaSqFt = areaSqFt;
}
public String getName() { return name; }
public int getAreaSqFt() { return areaSqFt; }
@Override
public String toString() {
return name + " (" + areaSqFt + " sq ft)";
}
}The key observation: House receives room names (strings), not Room objects. It calls new Room() internally. Order receives product details and creates OrderLine objects itself. External code never holds a direct reference to a composed part that could outlive the whole.
I also want to highlight the package-private constructors on Room and OrderLine. By restricting who can instantiate the part, you enforce the composition contract at compile time. If some other class could call new Room("Kitchen", 200) and hand it around, the room would exist outside a house, which breaks the model.
Interview signal: who calls new?
If the whole calls new Part() inside its own methods, it is probably composition. If the whole receives a pre-built part from outside, it is probably aggregation. This heuristic is right the vast majority of the time.
Composition vs Aggregation
This is the comparison interviewers care about. Both are "has-a" relationships. The difference is one word: ownership.
| Dimension | Composition | Aggregation |
|---|---|---|
| UML diamond | Filled β | Hollow β |
| Lifecycle | Part dies with the whole | Part exists independently |
| Who creates the part? | The whole creates it internally | External code creates it, passes it in |
| Shared ownership? | No, exactly one whole per part | Yes, part can belong to multiple wholes |
| Deletion cascade | Cascade (part is destroyed) | No cascade (part survives) |
| Java signal | Constructor calls new Part() | Constructor receives Part as parameter |
| Real example | House β Room (room dies with house) | Department β Employee (employee survives) |
The single question to ask: can the part exist without the whole?
- A
Roomwithout aHouse? No. Composition. - An
OrderLinewithout anOrder? No. Composition. - An
Employeewithout aDepartment? Yes, they can be unassigned. Aggregation. - A
Songwithout aPlaylist? Yes, it lives in the catalog. Aggregation.
In database terms, composition maps to ON DELETE CASCADE on the foreign key. Deleting the order row automatically deletes all order line rows. Aggregation maps to ON DELETE SET NULL or ON DELETE RESTRICT.
Common Mistakes
Mistake 1: Exposing mutable references to composed parts.
Returning the internal List<Room> directly lets external code add, remove, or replace rooms. That breaks the ownership contract because now something outside the house controls the parts. Always return Collections.unmodifiableList() or a defensive copy.
// BAD: leaks internal reference
public List<Room> getRooms() {
return rooms; // caller can do rooms.clear()
}
// GOOD: unmodifiable view
public List<Room> getRooms() {
return Collections.unmodifiableList(rooms);
}
Mistake 2: Accepting pre-built parts from outside.
If the constructor takes a List<Room> created by the caller, the caller still holds a reference to those rooms. They can mutate the list or share the rooms with another house. That is aggregation, not composition. To maintain composition, accept raw data (strings, primitives) and build the parts internally.
Mistake 3: Forgetting deep copy when cloning the whole.
If you implement clone() or a copy constructor for the whole but shallow-copy the parts list, both the original and the clone share the same part objects. Mutating a room through one house affects the other. Composition requires deep copy: create new Room instances for the cloned house.
// BAD: shallow copy shares parts
public House(House other) {
this.address = other.address;
this.rooms = other.rooms; // same list, same Room objects
}
// GOOD: deep copy creates new parts
public House(House other) {
this.address = other.address;
this.rooms = new ArrayList<>();
for (Room r : other.rooms) {
this.rooms.add(new Room(r.getName(), r.getAreaSqFt()));
}
}
Mistake 4: Modeling everything as composition.
Not every "has-a" is composition. A Car has a Driver, but the driver exists independently. A Library has Book objects, but in most domains, books survive the library closing. Over-applying composition creates rigid models where parts cannot be reused or reassigned. Use the lifecycle test before committing to a filled diamond.
Mistake 5: Circular composition.
If A composes B and B composes A, destroying either object requires destroying the other first, creating an impossible cycle. Composition is strictly hierarchical. The whole sits above the part, period. Circular references between classes are fine as plain associations, but never as composition.
Records and immutability help enforce composition
Java records make composed parts naturally immutable. A record OrderLine(String product, int qty, double price) cannot be mutated after creation. Combined with List.copyOf() in the whole, you get composition guarantees without writing defensive copies by hand. Prefer records for value-like parts.
Test Your Understanding
Quick Recap
- Composition is a "has-a" relationship where the whole owns the parts and controls their entire lifecycle. Parts are created by the whole and destroyed with it.
- UML notation: filled (solid) diamond
βon the whole side. Hollow diamond is aggregation, not composition. - The lifecycle test: if the part cannot exist independently and is not shared between wholes, it is composition.
- In Java, the signal is that the whole calls
new Part()internally rather than receiving pre-built parts from outside. - Composed parts must not be exposed as mutable references. Return
Collections.unmodifiableList()or defensive copies. - Cloning a composed whole requires deep copy. Shallow copy silently converts composition into shared-reference aggregation.
- In database design, composition maps to
ON DELETE CASCADE. Deleting the parent row deletes all child rows.