Home › Low-Level Design › Class Relationships: Association, Aggregation, Composition
C

Class Relationships: Association, Aggregation, Composition

Most LLD rounds are not lost on the big patterns. They are lost on the arrows between two boxes. I have sat on maybe 200 LLD loops, and the failure is boring: candidates recite Strategy and Observer, then draw a filled diamond where a hollow one belongs and quietly tank the round. If you can tell me why an Order owns its line items but only references a Customer, you already design better than half the people I interview. Here is how to reason about it so you never guess.

Low-Level DesignMedium9 min read

If you remember five things

All three are "has-a." They differ only in how tight the grip is. Everything below defends these five calls.

  1. Lifecycle decides it. Ask one question: if I delete the whole, should the part die with it? That answer, not vibes, picks the relationship.
  2. Composition owns. The part cannot exist without the whole and belongs to exactly one whole. Filled diamond. The whole creates it with `new` and destroys it.
  3. Aggregation shares. The part has its own identity, outlives the whole, and can be held by many. Hollow diamond. You inject it, you do not build it.
  4. Association just talks. Two objects collaborate, neither owns the other. Plain arrow. Most of your dependency-injected wiring lands here.
  5. Inheritance is a different axis. `is-a`, not `has-a`. Treating it as one of these three is the classic blunder (looking at you, `Stack extends Vector`).

1The problem it solves#

The second you have two classes, you owe an answer: how do they relate? "Connected" is lazy, and no interviewer lets it slide.

Picture a parking lot:

  • A ParkingLot, a few thousand ParkingSpot objects, a Ticket per car that drives in.
  • They are clearly connected. The interviewer wants the exact kind of connection, because it pins down three concrete things.

The three questions a relationship answers:

  • Lifecycle. Tear down the lot and the spots vanish too (a spot means nothing without its lot). A car leaving does not delete the lot.
  • Ownership. Who creates and destroys the other object? Exactly one class holds that job, or nobody does and you leak.
  • Sharing. Can one object be pointed at by many, or does it belong to exactly one owner?

Association, aggregation, and composition are the three answers. Get them right and your diagram tells the truth about the system. Get them wrong and you ship code where deleting a cart cascades a delete into the user who owned it. I have watched that exact bug reach production because the ORM mapping blindly copied a bad class diagram.

The whole topic in one sentence

These three relationships are just three answers to "when the whole dies, what happens to the part?" Composition says it dies too, aggregation says it lives on and can be shared, association says they were never really parent and child. Reason from lifecycle and the UML takes care of itself.

2The idea (mental model)#

All three are "has-a." They only differ in how tight the grip is. Here is how I keep them straight at the whiteboard.

  • Association (loosest). Two objects know each other and talk, neither owns the other. A Doctor treats a Patient: both existed before they met, both keep living after. It is a phone number in a contact list. Deleting the number does not delete the person.
  • Aggregation (shared part). A "has-a" where the part can live on its own and be shared. A Team has Player objects. The team folds, the players still exist and join another team tomorrow. The team holds references to players it did not create and will not destroy. A playlist and its songs: delete the playlist, the songs stay, and ten playlists can hold the same song.
  • Composition (owned part). A "has-a" where the part cannot exist without the whole and belongs to exactly one whole. A House has Room objects. No house, no rooms. You do not move a room to another house. The whole creates the parts and destroys them. This is ownership in the strong sense, a cascade delete in database terms.

The one-line test I use in interviews:

If I delete the container, should the contained thing die with it? Yes, and it belongs to only one container, that is composition. No, it can live on and be shared, that is aggregation. They just talk and neither owns the other, that is plain association.

And one thing that is not on this axis at all:

  • Inheritance (is-a) is a different question entirely.
  • I will draw it for contrast, but it is not one of the three, and treating it like one is a top-three mistake I cover below.

3Structure#

A small e-commerce slice shows all three cleanly, and everyone already has a model of a shopping cart.

  • Order is made of OrderItem objects. The items have no meaning outside their order. Kill the order, kill the items. Composition (filled diamond).
  • Order references a Customer. The customer exists before the order and long after it ships. The order does not own the customer, and many orders point at the same one. Aggregation (hollow diamond), shared.
  • Order uses a PaymentGateway to charge. It calls it once and forgets it. Plain association.
  • PremiumOrder is an Order. Inheritance, shown only for contrast.
classDiagram class Order { -String id -Customer customer -List~OrderItem~ items +addItem(product qty) void +total() Money +checkout(gateway) boolean } class OrderItem { -String productId -int quantity -Money unitPrice +subtotal() Money } class Customer { -String id -String name +email() String } class PaymentGateway { +charge(amount) boolean } class PremiumOrder { +freeShipping() boolean } Order *-- OrderItem Order o-- Customer Order --> PaymentGateway Order <|-- PremiumOrder
Read the diamonds, because the diamond carries the whole meaning. The filled diamond on OrderItem sits on the owner side: "I made this and I will destroy it." The hollow diamond on Customer says "I hold a reference, I did not make it and I will not end it." The plain arrow to PaymentGateway says "I use this for a moment." The open triangle on PremiumOrder is is-a. Four symbols, and that is the entire vocabulary you need at a whiteboard.

4Implementation in Java#

The relationship type shows up directly in how objects get constructed and stored. Watch where new lives, because new is where ownership is decided.

// COMPOSITION: Order creates and owns its items.
// OrderItem is package-private; nothing outside this file builds one.
public class Order {
    private final String id;
    private final Customer customer;              // aggregation: passed in, not created here
    private final List<OrderItem> items = new ArrayList<>();

    public Order(String id, Customer customer) {
        this.id = id;
        this.customer = customer;                 // we hold a reference, we did not build it
    }

    public void addItem(String productId, int qty, Money unitPrice) {
        // The item is born here, inside the order. Pure composition.
        items.add(new OrderItem(productId, qty, unitPrice));
    }

    public Money total() {
        return items.stream()
                    .map(OrderItem::subtotal)
                    .reduce(Money.ZERO, Money::plus);
    }

    // Defensive copy: we own the items, so we never hand out the live list.
    public List<OrderItem> items() {
        return List.copyOf(items);
    }

    // ASSOCIATION: we use a gateway we are handed, just to do one job.
    public boolean checkout(PaymentGateway gateway) {
        return gateway.charge(total());
    }
}

// The part. It is meaningless without an Order around it.
class OrderItem {
    private final String productId;
    private final int quantity;
    private final Money unitPrice;

    OrderItem(String productId, int quantity, Money unitPrice) {
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    Money subtotal() {
        return unitPrice.times(quantity);
    }
}

Three signals you can point at in the code, and I will ask you to point at them:

  • Composition: new OrderItem(...) happens inside Order. The items live in a field no one else constructs. When the Order is garbage collected with no live reference left, the items go with it.
  • Aggregation: Customer arrives through the constructor. The Order stores the reference but never calls new Customer(...). Someone outside owns the customer's life, usually a repository or a session.
  • Association: PaymentGateway comes in as a method parameter, not even a field. The order touches it for one call and does not retain it.
Pause before you read on: a class holds a `Logger logger` field. Field, stored, long-lived. Is that composition?
Reveal the answer
No. It is association. The logger was created somewhere else and will outlive this object (and be shared by many). Storing a reference is not owning it. The deciding question is who called `new` and who destroys it, not whether the pointer happens to live in a field. This exact trap is why "field versus parameter" is a hint, not the rule.

The field-versus-parameter tell:

  • A standing field usually means "I keep a lasting relationship with this."
  • A method parameter usually means "I borrow it for a second."
  • When I grade, I literally scan for where the dependency is stored. It is a strong hint, but lifecycle is the real judge (see the self-check above).

Here is the checkout flow so you can see the borrowed object in action.

sequenceDiagram participant C as Client participant O as Order participant G as PaymentGateway C->>O: checkout(gateway) O->>O: total() O->>G: charge(amount) G-->>O: true O-->>C: success
The gateway is never stored on Order. It arrives as a call argument, does one job, and is gone when checkout returns. That is association in motion: a borrowed collaborator, not an owned part.

5A real-world example#

A music app, because almost everyone has used one, so the intuition is free.

  • Playlist aggregates Song. You add songs you do not own. Delete the playlist and the songs stay in the catalog for everyone. Two playlists hold the same song by reference. Shared, independent lifecycle. Textbook aggregation.
  • Song is composed of AudioSegment chunks (or metadata blocks) that exist only to make up that one song. Delete the song and those chunks are meaningless and go away. One owner, dependent lifecycle.
  • Player associates with an AudioOutputDevice. It sends audio to whatever speaker is connected right now. It does not own your Bluetooth headphones, and unplugging them does not destroy the player.

Why I reach for this one in real interviews:

  • It forces the candidate to defend "shared and independent" versus "owned and dependent" with something they have physical intuition about.
  • If someone tells me a playlist composes its songs, I ask: so when I delete my road-trip playlist, your copy of that song vanishes from your library? Watch them reconsider in real time. That moment is the point.

The same shape repeats everywhere:

  • A University aggregates Student objects.
  • A Document composes its Paragraph objects.
  • A RideRequest associates with a Driver.
  • Once you see the pattern you stop memorizing definitions and start reasoning from lifecycle.

6When to use it, and when not to#

Strong ownership
Composition
Use when
The part is an implementation detail with no identity outside the whole.
Examples
Line items, rooms in a house, the engine in one specific car, cells in a spreadsheet.
Tell
You call `new` inside the owner, and nobody outside should ever hold a reference to the part.
Breaks when
You over-compose: the part cannot be reused or unit-tested in isolation because it cannot even be constructed alone.
Shared part
Aggregation
Use when
The part has its own identity and lifecycle and may be shared.
Examples
Players on teams, students in courses, products stocked in a warehouse.
Tell
You construct the part somewhere else and inject it in.
Breaks when
You actually needed one strict owner and sharing lets the part drift out of sync or outlive its purpose.

Reach for association when two objects collaborate but neither is a part of the other: services calling services, a controller using a repository, an order charging a gateway. Most of your dependency-injected wiring is association, and that is completely fine.

When not to force it:

  • Do not model a relationship as composition because it feels "stronger" or more object-oriented. Over-composing creates rigid designs you cannot reuse or test in isolation.
  • Do not add a relationship at all if the two classes do not genuinely need each other. The best relationship is sometimes no relationship, and a flat design with fewer edges is easier to reason about than a clever one.

7Common mistakes#

Confusing aggregation and composition. The big one, and what most borderline candidates get wrong.

  • The test is lifecycle, not vibes.
  • Ask: can the part outlive the whole? Can it be shared by two wholes at once?
  • Yes to either means aggregation.

Treating inheritance as a has-a. Stack extends Vector from early Java is the classic blunder.

  • A stack is not a kind of vector. It uses a list internally and should hide most of it. That should be composition, not inheritance.
  • When in doubt, prefer composition over inheritance: more flexible, it does not leak the parent's whole public API, and it lets you swap the backing implementation later.

Bidirectional references everywhere.

  • If Order points to Customer and Customer holds a List<Order>, you now have two sides to keep in sync, plus a cycle that bites you in serialization and object equality.
  • Make relationships one-directional unless both sides genuinely need to navigate, and even then question it.

Mistaking a field for ownership.

  • Holding a reference is not owning. A Logger field is association, not composition, even though it lives in a field.
  • Who created it and who destroys it decides the relationship, not where you stashed the pointer.

Leaking composed parts.

  • If your Order returns its internal List<OrderItem> by reference, callers mutate it behind your back and your "ownership" is a fiction.
  • Return a copy or an unmodifiable view, which is exactly why the items() method above uses List.copyOf.
Red flag that fails you here

Drawing a filled diamond because composition "feels stronger," then folding the moment I ask "so deleting this whole deletes that part everywhere, even the copy another object is using?" If you cannot defend the diamond with one sentence about lifecycle and sharing, you did not choose it, you guessed it. Guessing on the arrows is exactly what separates the borderline candidate from the hire.

From the interviewer's chair

I am not grading UML purity. I am grading whether your diagram and your code tell the same story. If you drew composition, I expect new inside the owner. If you drew aggregation, I expect injection through the constructor. When those two disagree, I trust neither, and I start wondering what else in your design is decoration rather than decision.

8Where it shows up in interviews#

Almost every LLD prompt lands here in the first ten minutes. Parking lot, elevator, vending machine, library management, ride-sharing: all of them are mostly an exercise in getting these three relationships right across a dozen classes. The patterns you layer on top come later and are easier.

What I am actually grading:

  • Do you draw the correct diamond, and justify it in one sentence about lifecycle, not UML trivia?
  • When I push ("why aggregation and not composition here?"), do you answer with ownership and sharing, or fold and flip?
  • Does your code match your diagram? Composition wants new inside the owner. Aggregation wants injection through the constructor.

What a strong candidate sounds like:

Candidate: The lot composes its spots, because a spot has no meaning without the lot, so tearing down the lot tears down the spots. But it aggregates tickets, since a ticket's data outlives the drive and we archive it for billing.

Interviewer: And if I delete a spot, what happens to the ticket that was parked there?

Candidate: Nothing destructive. The ticket references the spot id, it does not own the spot. That reference is an association, so the ticket survives and we just mark the spot gone.

Draw the diamond, name the lifecycle, defend it in one sentence, move on.

The trap to avoid:

  • Do not spend five minutes philosophizing about UML purity.
  • Interviewers care that your design is correct and defensible, not that you can recite the formal definition.
Follow-ups the interviewer will ask (and your answers)
"Why is Order to OrderItem composition and not aggregation?"
Lifecycle and sharing. An order item has no identity outside its order and is never shared by two orders, so deleting the order should delete its items. One owner, dependent lifecycle: that is composition, which is why `new OrderItem(...)` lives inside `Order` and nobody outside constructs one.
"You store Customer in a field, just like items. Why is one aggregation and the other composition?"
Because who calls `new` differs, not where the pointer lives. `Order` builds its items itself and ends their life, so composition. The customer is constructed elsewhere (a repository or session), injected in, and outlives any single order, so aggregation. A field is a hint, not the deciding factor.
"When would you turn one of these into inheritance instead?"
Rarely, and only for a true `is-a` where the subtype can stand in for the base everywhere (Liskov). A `PremiumOrder` is an `Order`, fine. But `Stack extends Vector` fails: a stack is not a vector, it uses one. When tempted, prefer composition so you do not leak the parent's public API and you can swap the backing implementation.
"Order has a Customer and Customer has a list of Orders. Any problem?"
That bidirectional link is two sides to keep in sync, plus a cycle that bites serialization and equality. I make it one-directional unless both sides genuinely need to navigate. Usually the order needs the customer and the customer's orders can be a query, not a stored back-reference.
"Your Order owns its items. How do you stop callers from mutating them?"
Never hand out the live list. `items()` returns `List.copyOf(items)`, an immutable copy, so a caller cannot add or remove items behind the order's back. If I leaked the internal list by reference, the ownership would be fiction and the encapsulation composition buys me would be gone.

What you can now explain in the room

  • Why lifecycle (not strength or vibes) is the one test that picks composition versus aggregation versus association.
  • How where `new` lives decides ownership, and why a stored field is a hint rather than proof.
  • Why `Order` composes its items, aggregates its customer, and associates with its gateway, each in one sentence.
  • How to read the four UML symbols (filled diamond, hollow diamond, plain arrow, open triangle) and draw the right one.
  • Why inheritance is a separate axis and why `Stack extends Vector` should have been composition.
  • How to defend a composed part with a defensive copy so ownership is real, not decorative.

Next, look at the LLD classics that lean on these relationships: Parking Lot, Library Management, and the Composite and Strategy patterns, where "prefer composition over inheritance" stops being advice and becomes the whole point.

Get a prep plan built around your Class Relationships: Association, Aggregation, Composition interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Class Relationships: Association, Aggregation, Composition prep plan is on the way.