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.
Introduction
Composition is a strong has-a relationship defined by lifecycle ownership. The key question is whether a part can outlive its whole. In composition, the whole owns the part: it establishes the part, controls access to it, and the domain model treats the part as ending when the whole is removed. If the ownership boundary is wrong, a design can either retain objects that should be scoped to the whole or remove objects that another component still needs.
A House composes Room objects. Demolish the house and the rooms are gone. They have no meaning, no address, and no purpose outside the house that contains them. That tight binding is what separates composition from aggregation and is a key distinction in UML class relationships.
TL;DR
Composition is exclusive whole-part ownership. The whole controls the part's lifecycle, the part is not shared with another whole, and the part has no independent meaning in the model. UML uses a filled diamond (*--) on the whole side. Use composition when those lifecycle rules are true; the presence of new or dependency injection alone does not decide the relationship.
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.
A compact mental model is: "I create these things, they belong only to me, and they end with me."
Mental Model: An Ownership Tree
Model composition as an ownership edge in a domain object tree:
- Each part has one whole, rather than several competing owners.
- The whole is responsible for creating, replacing, and removing the part.
- A part is reached through its whole and has no independent business meaning.
- Removing the whole removes the part from the domain model as well.
In Java, "destroyed with it" is usually a domain-lifecycle statement, not a call to a destructor. If no other references remain, the part becomes unreachable and eligible for garbage collection. In a database or service boundary, the same ownership rule may be enforced with a transaction or cascade delete.
Definitions and boundaries
Composition is a semantic and domain decision, not just a Java constructor pattern. A whole can receive a part through dependency injection and still compose it when the domain says the part belongs exclusively to that whole. Conversely, calling new Part() is only a clue: a factory or helper may create an object that is later shared or managed elsewhere.
The UML filled diamond describes the ownership claim between a whole and a part. It does not require immediate memory reclamation, a package-private constructor, or a particular persistence mapping; those are implementation techniques that may help enforce the boundary.
Concrete Example
Consider an Order and its OrderLine objects. The order accepts product details, creates each line internally, and exposes the lines for reading without exposing a mutable collection:
final class Order {
private final List<OrderLine> lines = new ArrayList<>();
void addLine(String product, int quantity, double price) {
lines.add(new OrderLine(product, quantity, price));
}
List<OrderLine> getLines() {
return Collections.unmodifiableList(lines);
}
}
OrderLine is a part of this order: it is created for one order, is not shared with another order, and is removed from the domain model when its order is removed. If line items were catalog entities that could be reused across orders, the relationship would be an association or aggregation instead.
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, making it one of the tightest has-a bindings 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 reader that the part survives independently, which changes the stated lifecycle semantics.
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), or through a factory it owns. Parts are not passed in as independently owned objects.
- Exclusive ownership. Each part belongs to exactly one whole. No sharing.
- Removal. When the whole is removed from the domain model, the parts are removed with it. In a garbage-collected runtime, they become eligible for collection once no references remain. 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 creation to removal.
- 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.
A practical test is: 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.
Design Implications and Trade-offs
Composition gives the model a clear ownership boundary, but it also couples the part to the whole:
- Encapsulation: keep creation and mutation behind the whole so it can enforce invariants such as valid quantities or a consistent total.
- Construction: the whole may create parts directly, use an owned factory, or receive a factory through dependency injection. The domain lifecycle—not the constructor syntax—determines whether the relationship is composition.
- Reuse and reassignment: exclusive ownership makes the model easier to reason about, but a part cannot be shared or moved freely to another whole. If reuse or reassignment is a requirement, aggregation or association may fit better.
- Persistence: a composed child often uses an identity scoped to its parent and is deleted in the same transaction.
ON DELETE CASCADEcan enforce that rule in a relational database, but it should reflect the domain decision rather than substitute for it. - Runtime semantics: Java has garbage collection rather than deterministic destructors. Composition still requires the domain model to remove parts with their whole, while the runtime decides when unreachable objects are reclaimed.
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 is not given a construction or mutation API for the parts; it can observe read-only views, while the whole retains control of their ownership.
The package-private constructors on Room and OrderLine are another useful boundary. By restricting who can instantiate the part, the code helps 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.
Implementation clue: who creates the part?
If the whole calls new Part() inside its own methods, that is a useful composition clue. If the whole receives a pre-built part from outside, the relationship may be aggregation. Treat both as implementation signals, then verify the domain lifecycle and ownership semantics.
Composition vs Aggregation
Composition and aggregation are both "has-a" relationships. The key difference is 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 can map to ON DELETE CASCADE on the foreign key when the persistence model matches the domain rule. Deleting the order row then deletes all order line rows. Aggregation may instead use ON DELETE SET NULL or ON DELETE RESTRICT.
30-Second Explanation
Composition is a strong has-a relationship: one whole exclusively owns its parts and controls their lifecycle. The part belongs to one whole, has no independent business meaning, and leaves the domain model when the whole is removed. UML shows this with a filled diamond on the whole side. For example, an Order creates its own OrderLine objects and returns them through a read-only view. If a part can be shared, reassigned, or survive independently, use aggregation or association instead.
5-Minute Explanation
Start with the lifecycle question: can this part exist independently of this whole? If the answer is no, and the part is exclusively owned, composition is a candidate. Then verify the boundary: the whole creates or establishes the part, owns the part's mutations, and removes the part when the whole leaves the domain model.
Use Order and OrderLine as a concrete walk-through. Order.addLine(product, quantity, price) accepts data, while Order constructs the OrderLine. The caller cannot attach that line to a second order, and getLines() should not expose a mutable internal list. This protects invariants such as positive quantities and a correct order total.
Contrast that with an Employee and a Department: an employee can exist without a department, can be reassigned, and may be shared by other organizational views. That is not composition merely because the department has an employee reference. Constructor injection, a factory, and Java garbage collection are implementation details; domain ownership and lifecycle semantics decide the relationship. The trade-off is a clearer ownership boundary in exchange for less reuse and more coupling between the whole and its parts.
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. Return Collections.unmodifiableList() or a defensive copy when callers should not mutate the collection.
// 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 often models aggregation because the caller still controls the part references. To maintain composition, accept raw data (strings, primitives) or otherwise ensure the whole receives exclusive ownership and controls the lifecycle.
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, the ownership claims form a cycle that is difficult to define and enforce. Model the circular link as an association, or choose one direction as the actual owner. Composition is normally hierarchical; circular references between classes can still be valid as plain associations.
Records and immutability help enforce composition
Java records make value-like composed parts concise and shallowly immutable. A record OrderLine(String product, int qty, double price) cannot have its components reassigned, although referenced objects can still be mutable. Combined with List.copyOf() in the whole, records can reduce defensive-copying code; they do not replace the ownership design.
Test Your Understanding
Concise answer key
- Car → Engine: Composition.
- ChatRoom → Message: Composition by lifecycle semantics; public construction weakens the boundary.
- Order lines: The getter leaked a mutable collection; return an unmodifiable view or copy.
- House clone: Shallow copying shares parts; deep-copy the composed objects.
- University → Building → Room: Removal cascades through the ownership tree.
- Cart → CartItem → Product: Cart composes
CartItem;CartItemassociates with independentProduct. - Document undo: It remains composition if the document owns the undo history and paragraph references.
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 removed from the domain model 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, a call to
new Part()inside the whole is an implementation signal, not a definitive rule; verify the domain semantics. - 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 can map to
ON DELETE CASCADEwhen that matches the domain rule. Deleting the parent row then deletes all child rows.
Related Concepts
- Association: a general reference between objects without a lifecycle-ownership claim.
- Aggregation: a whole-part relationship where parts can survive independently and may be shared or reassigned.
- Composition vs. inheritance: composition combines owned collaborators; inheritance models an is-a relationship and subtype behavior.
- Encapsulation: the whole protects its invariants by controlling access to composed parts.
- Object composition: assembling behavior from collaborators is broader than UML composition and does not necessarily imply lifecycle ownership.
Related Articles
Learn aggregation in OOP: a has-a relationship where the part can exist independently of the whole, with Java examples and UML notation.
Understand association in OOP: how objects reference each other without ownership, with examples of unidirectional, bidirectional, and multiplicity relationships.
Inheritance models IS-A but couples tightly. Composition models HAS-A and stays flexible. Learn when each is right and how to migrate from one to the other.