LESSON 3.2
Class relationships and UML
Association, aggregation, composition and dependency are four distinct ways classes relate. Getting the relationship right prevents the wrong lifetime and the wrong owner.
7 min read
Class diagrams answer one question: who owns what, and for how long? Getting the relationship wrong produces memory leaks, orphaned records, and classes that cannot be tested without their entire object graph.
The four relationships
Dependency (uses): the most transient. A method receives an object as a parameter and uses it. The caller owns the object; the called class holds no reference after the call.
Association (knows): a long-lived reference stored in a field. Order holds a reference to Customer. Both exist independently; neither creates or destroys the other.
Aggregation (has): a stronger association where the whole contains parts, but parts can exist without the whole. A Team contains Player objects. Players exist before and after the team.
Composition (owns): the strongest relationship. The whole creates the parts; the parts cannot exist without the whole. An Order creates its OrderLine objects. Deleting the order deletes all lines.
In code
// Dependency: Discount is used only inside calculateTotal.
class Discount {
apply(amount) { return amount * 0.9; }
}
// Composition: OrderLine is created by Order and meaningless outside it.
class OrderLine {
constructor(item, price) { this.item = item; this.price = price; }
}
// Association: Customer is passed in and outlives any single Order.
class Customer {
constructor(name) { this.name = name; }
}
class Order {
#lines = [];
#customer;
constructor(customer) {
if (!(customer instanceof Customer)) throw new Error("Customer required");
this.#customer = customer;
}
addLine(item, price) {
this.#lines.push(new OrderLine(item, price)); // Composition: Order creates the line.
}
calculateTotal(discount) { // Dependency: discount is transient.
const raw = this.#lines.reduce((s, l) => s + l.price, 0);
return discount ? discount.apply(raw) : raw;
}
get customer() { return this.#customer; }
}
const alice = new Customer("Alice");
const order = new Order(alice);
order.addLine("book", 50);
order.addLine("pen", 10);
console.assert(order.calculateTotal(new Discount()) === 54);
console.log(order.calculateTotal(new Discount()), "for", order.customer.name);public class Example {
record Customer(String name) {}
record OrderLine(String item, double price) {}
static class Discount { double apply(double amount) { return amount * 0.9; } }
static final class Order {
private final Customer customer;
private final java.util.List<OrderLine
Choosing the right relationship
| Question | Answer → Relationship |
|---|---|
| "Does the part make sense without the whole?" | No → Composition |
| "Can the part belong to multiple wholes?" | Yes → Aggregation |
| "Does the class hold a long-term reference?" | Yes → Association |
| "Is the reference only during one method call?" | Yes → Dependency |
The most common mistake is using composition when aggregation is correct. Department that deletes its Employee objects on close is a bug. The second most common is hiding a dependency inside a global instead of making it an explicit parameter.
Where you have already seen it
JPA @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) expresses composition. A React component that unmounts its children models composition. A Redux store associates state with selectors without owning them. Mongoose populate() resolves an association by reference.
Try it
Ticket Booking LLD
Draw the class diagram before coding. State each relationship: does the seat exist without the booking?