OOP · Designing with Objects
Relationships: Association, Aggregation & Composition
Who knows whom. Who owns whom.
Sign in to track your score
What you'll learn
- Look at two classes and name the relationship between them.
- Separate aggregation from composition using the lifetime question.
- Explain why C++ has no keyword for any of these relationships.
- Choose between is-a and has-a without guessing.
How to use this module. Wherever you see THINK, PREDICT or SPOT THE PROBLEM, stop and answer before reading on. The answer always follows immediately — cover it with your hand if you need to. Every code sample here compiles and was run to confirm its output.
Open any food delivery app and four things are going on at once.
A Customer browses a Restaurant. They place an Order. A DeliveryRider picks it up.
Four objects. None of them lives inside another one — a rider is not part of a restaurant, and a customer is not part of an order. They are separate things that need to know about each other.
Think — The previous modules were about inheritance: one class built on another.
Look at those four again. Is any of them a kind of any other?
No. So how are they related at all?

Most of a real program looks like this. A few inheritance trees, and an enormous amount of objects simply relating to each other.
And those relationships are not all the same. Some objects just know each other. Some contain others that could get along fine on their own. Some own their parts completely.
Those three have names — association, aggregation, composition — and this module is about telling them apart, plus the design question that follows from them: when to reach for inheritance instead.
Watch out — C++ has no
association,aggregationorcompositionkeyword. You will not type these words in code.They describe how you designed two classes to relate. The code expresses that design using ordinary things: member variables, references, pointers, and decisions about who creates and destroys what.
Because these relationships live in the design rather than in the syntax, software teams draw them. The standard way of drawing them is UML — a notation you will meet in design documents, architecture diagrams and interviews for the rest of your career.
Each relationship in this module comes with its UML symbol, and by the end you will be able to read a class diagram and say what it claims.
6.1 Association: objects that just know each other
A delivery rider arrives at a restaurant and collects an order.
For those few minutes the rider knows about that restaurant. Then the delivery is done and the rider moves on to a different restaurant entirely.
Look at what is not happening:
- The rider did not create the restaurant.
- The restaurant did not create the rider.
- Shut the restaurant down and the rider still has a job.
- The rider quits and the restaurant still serves food.
Neither one owns the other. They interact, and that is the whole relationship.
The name for that is association.

In code it can be as light as passing one object to the other:
class Restaurant {
public:
void prepareOrder() { std::cout << "Restaurant is preparing the order\n"; }
};
class DeliveryRider {
public:
void pickUp(Restaurant& restaurant) {
restaurant.prepareOrder();
std::cout << "Rider collected it\n";
}
};
Restaurant joes;
DeliveryRider rider;
rider.pickUp(joes);
Restaurant is preparing the order
Rider collected it
What is happening: DeliveryRider has no restaurant stored inside it. It receives one when it needs one, uses it, and forgets it.
Why it matters: the two classes stay independent. DeliveryRider works with any restaurant, and neither class has to worry about the other's lifetime.
Association does not mean "not stored"
Here is where beginners often draw the wrong conclusion. A relationship can be remembered and still be a plain association.
A passenger is on a train right now. Later they will be on a different train, or on none.
class Passenger {
public:
void board(Train& t) { current = &t; }
void getOff() { current = nullptr; }
void whereAmI() const {
if (current) std::cout << "On train " << current->code << "\n";
else std::cout << "Not on a train\n";
}
private:
Train* current = nullptr;
};
Not on a train
On train RED-19
On train BLU-04
Not on a train
The passenger stores a pointer to a train. That is still association: the passenger did not create the train, does not destroy it, and can let go of it entirely. Trains run whether anyone boards them or not.
Quick check — Two classes, and one holds a pointer to the other. Does that make it association, aggregation or composition?
Answer — you cannot tell yet A pointer alone says nothing. What decides it is the design: does one of them group the other as its parts, and who controls how long the pointed-to object lives? Those questions come next.
Drawing it: the UML symbol
UML draws a class as a box in three parts — the name on top, the data underneath, the operations at the bottom. A minus sign marks something private, a plus sign something public.
Two classes joined by a plain solid line is an association.

There is nothing on either end of that line, and the emptiness is the message. No diamond, no triangle, no ownership claimed by anybody. It says only that these two classes are connected.
6.2 Aggregation: has-a, but separable
A band has musicians.
Ana plays bass, Ben plays drums, Kai sings. The band has a name, a set list, and three members.
Now the question that matters:
Predict — The band breaks up.
Do Ana, Ben and Kai stop existing?
Answer — obviously not They go home. They join other bands. They teach lessons. The band was a grouping of them, never the source of them.
That is the shape of it: one object has other objects, and those objects can carry on perfectly well without it. The name for it is aggregation.

Notice this is stronger than association. The band does not merely know the musicians — it groups them, lists them, treats them as its members. But it does not control how long they exist.
class Musician {
public:
Musician(std::string n) : name(n) {}
~Musician() { std::cout << name << " retired\n"; }
std::string name;
};
class Band {
public:
~Band() { std::cout << "Band broke up\n"; }
void addMember(Musician* m) { members.push_back(m); }
void lineup() const {
for (Musician* m : members) std::cout << m->name << " ";
std::cout << "\n";
}
private:
std::vector<Musician*> members;
};
The musicians are created outside the band, and the band is given them:
Musician ana("Ana");
Musician ben("Ben");
{
Band band;
band.addMember(&ana);
band.addMember(&ben);
band.lineup();
} // the band ends here
std::cout << "After the band: " << ana.name << " and " << ben.name << " are still here\n";
Ana Ben
Band broke up
After the band: Ana and Ben are still here
Ben retired
Ana retired
What is happening: the inner braces end, so band is destroyed and prints its message. Ana and Ben are untouched — they were never the band's to destroy. They only retire when main itself ends.
Why it matters: that output is the definition, made visible. The whole disappeared and the parts kept going.
Watch out —
std::vector<Musician*>does not make this aggregation. A vector of pointers could just as easily hold objects the class created and must delete — that would be ownership, not aggregation.What makes it aggregation is the design decision: the band never creates a musician and never destroys one. The syntax is evidence, not proof.
Drawing it: the hollow diamond

Two things to fix in your memory, because both are easy to get backwards.
The diamond is hollow — an outline with nothing inside it. Hollow means the hold is loose: the part can live without the whole.
The diamond sits on the whole, never on the part. It is drawn against Band because the band is the one that has members. Put it on Musician and the diagram would claim a musician contains bands.
| Association | Aggregation | |
|---|---|---|
| The idea | "we know or use each other" | "I have you as one of my parts" |
| Grouping | none — they just interact | the whole keeps a collection of the parts |
| Lifetime | fully independent | fully independent |
| Example | rider ↔ restaurant | band → musicians |
Both leave lifetimes alone. Aggregation adds the sense that one object gathers the others.
6.3 Composition: owns-a, live-and-die together
Order #1042 has three lines on it: two pizzas, one garlic bread, two juices.
Predict — The order is cancelled and deleted.
What should happen to those three lines?
Answer — they go too They only ever meant anything as part of that order. "Two pizzas" floating around on its own, belonging to no order, is not a thing the system should keep.
Compare that with the band. The musicians existed before the band and outlive it. The order lines were born with the order and die with it.
That difference — tied lifetime — is composition.

In C++ the most direct way to say it is to make the parts actual members:
class OrderItem {
public:
OrderItem(std::string n, int q) : name(n), quantity(q) {}
~OrderItem() { std::cout << " line removed: " << name << "\n"; }
std::string name;
int quantity;
};
class Order {
public:
Order(int id) : orderId(id) { items.reserve(4); }
~Order() { std::cout << "Order " << orderId << " cancelled\n"; }
void addItem(std::string name, int qty) { items.emplace_back(name, qty); }
int lineCount() const { return items.size(); }
private:
int orderId;
std::vector<OrderItem> items;
};
{
Order order(1042);
order.addItem("Pizza", 2);
order.addItem("Juice", 2);
std::cout << "Lines: " << order.lineCount() << "\n";
}
std::cout << "Nothing of that order is left\n";
Lines: 2
Order 1042 cancelled
line removed: Pizza
line removed: Juice
Nothing of that order is left
What is happening: the items are stored by value, not by pointer. They live inside the order object. When the order is destroyed, its own destructor body runs first, then its members are destroyed automatically — which is exactly the reverse order you saw for base and derived parts in Module 4.
Why it matters: nobody wrote code to clean up the items. The lifetime relationship is built into the structure, so it cannot be forgotten.
Watch out — Two things this does not mean.
A member object is a strong signal of composition, but not an automatic label — you still decide what the relationship means in your design.
And composition does not claim an
OrderItemcould never exist anywhere else in any program. It says that in this design, this part belongs to this whole.
Drawing it: the filled diamond

Same diamond shape, same side of the line, one difference: it is filled in. Solid means strong ownership — the part's lifetime is tied to the whole.
That is the entire visual difference between aggregation and composition in UML. Hollow or solid. Everything else about the two diagrams is identical, which is a fair reflection of how close the two ideas are.
The one question that separates them
| Aggregation | Composition | |
|---|---|---|
| Plain English | "I have you, but you can live without me" | "you are part of me" |
| Destroy the whole | parts carry on | parts go with it |
| Who creates the part | someone else, then hands it over | the whole itself |
| Typical C++ | pointer or reference to an object made elsewhere | member object held by value |
| Example | band → musicians | order → order lines |
When you are unsure which one you have, ask one question and answer it honestly:
If the whole disappears, should the part disappear too?
Yes means composition. No means aggregation.

One point of vocabulary before moving on: aggregation and composition are both kinds of association. Association is the general word for "these objects are connected"; the other two are the more specific cases where one object holds the other as a part.
6.4 Inheritance vs composition: which to reach for
Two classes are related. The real question is how.
Module 4 introduced the test, and it is worth repeating because this is where it earns its keep. Say the relationship as a sentence:
"A DeliveryVan is a DeliveryVehicle." True. Inheritance fits.
"A DeliveryVan has a GpsTracker." Also true — but a completely different claim. The van is not a tracker; it carries one. That is composition.

Drawing it: the hollow triangle
UML calls inheritance generalization, and draws it with a hollow triangle rather than a diamond.

The triangle always touches the general class — the base — and never the specific one. Read the line in the direction the triangle points and you get the sentence: DeliveryVan is a DeliveryVehicle.
This is also the only one of the four relationships that C++ has actual syntax for. class DeliveryVan: public DeliveryVehicle says "is a" in the language itself. The other three are expressed through ordinary members and pointers, which is exactly why a diagram is worth drawing.

Watch out — Read that table in one direction only. A UML symbol tells you what the design means, so it maps down to C++ — but C++ does not map back up.
Seeing
std::vector<Musician*>does not let you write a hollow diamond with confidence. The same line could be composition if the class created those musicians and deletes them. The diagram records a decision the code alone cannot tell you.
Where this goes wrong in practice
A music player needs to produce sound, and a speaker produces sound. So someone writes class MusicPlayer: public Speaker.
Read the claim out loud: "A music player is a speaker."
It isn't. A music player has a screen, a playlist, a battery, and a speaker it sends audio to. Inherit and the player suddenly owns every speaker function as part of its own identity — impedance, wiring, driver size — none of which has anything to do with playing music.
The better shape is the true sentence: the player has a speaker.
class Speaker {
public:
virtual void output(std::string track) = 0;
virtual ~Speaker() = default;
};
class BluetoothSpeaker : public Speaker {
public:
void output(std::string track) override {
std::cout << "Streaming \"" << track << "\" over Bluetooth\n";
}
};
class WiredSpeaker : public Speaker {
public:
void output(std::string track) override {
std::cout << "Playing \"" << track << "\" through the cable\n";
}
};
class MusicPlayer {
public:
MusicPlayer(Speaker* s) : speaker(s) {}
void setSpeaker(Speaker* s) { speaker = s; }
void play(std::string track) { speaker->output(track); }
private:
Speaker* speaker;
};
BluetoothSpeaker bt;
WiredSpeaker wired;
MusicPlayer player(&bt);
player.play("Night Drive");
player.setSpeaker(&wired);
player.play("Night Drive");
Streaming "Night Drive" over Bluetooth
Playing "Night Drive" through the cable
What is happening: the same player produced sound two different ways, and the player class did not change at all.
Why it matters: the speaker became a replaceable part. With inheritance that swap is impossible — a MusicPlayer that is a BluetoothSpeaker can never become a wired one, because an object cannot change its type after it is created.

Notice this example uses both relationships, each where it fits. BluetoothSpeaker is a Speaker — inheritance, correctly. MusicPlayer has a Speaker — composition, correctly.
Remember — Inheritance is not the bad option. It is the wrong option when the is-a sentence is false.
EmailNotificationgenuinely is aNotification, so inheriting there is right. The mistake is reaching for inheritance because two classes happen to share some code.
A decision guide

SPOT THE RELATIONSHIP Decide each one, and say why in a sentence:
LibraryandBookInvoiceandInvoiceLineDoctorandAppointmentPremiumUserandUser- Aggregation. The library has books, but a book that leaves the collection is still a book.
- Composition. A line exists as part of one invoice. Delete the invoice and the line means nothing.
- Association. They are connected through a booking, but neither contains or owns the other.
- Inheritance. "A premium user is a user" is true — the same thing with extra privileges.
Answer
Common Misconceptions
❌ Association means one object owns another. Why it sounds right: "related" and "connected" sound like a strong link. What is true: association is the loosest connection there is. The objects know or use each other and nothing more. Neither creates the other and neither controls how long the other lives.
❌ Aggregation and composition are the same thing. Why it sounds right: both are "has-a", and both often look like a collection inside a class. What is true: the difference is lifetime. In aggregation the parts survive the whole — a musician outlives the band. In composition they do not — an order line dies with its order.
❌ Aggregation means the parts are stored as pointers. Why it sounds right: aggregation examples usually are written with pointers, so the syntax starts to look like the definition. What is true: a pointer is evidence, not proof. A class could hold pointers to objects it created and must delete, which is ownership. What makes it aggregation is that the whole never creates or destroys the parts.
❌ Composition just means one class has another class as a field. Why it sounds right: member objects are the usual way to express it, so the two get treated as one idea. What is true: the meaning is the tied lifetime and the part belonging to the whole. A member object gives you that lifetime automatically, which is why it is the usual choice — but the relationship is the design decision, not the field.
❌ If an object is stored as a member, the relationship is always composition. Why it sounds right: storing by value really does tie the lifetimes, so the conclusion feels safe. What is true: the lifetime tie is real, but you still decide what the design means. A class can hold a small configuration object as a member purely to use it, with no sense that it is a part of the whole. Look at the intent, not just the declaration.
❌ If two classes share code, one should inherit from the other. Why it sounds right: inheritance does remove the duplication, and it works. What is true: inheritance also claims "is a kind of", and the rest of the program relies on that claim. Shared code with a false is-a sentence is exactly the case for composition instead.
❌ Composition is always better than inheritance. Why it sounds right: it is repeated so often that it starts to sound like a rule. What is true: they answer different questions. When the is-a sentence is genuinely true — EmailNotification and Notification — inheritance is the honest, clearer design. Composition is better when the sentence is false, which happens to be most of the time.
❌ "Has-a" and "is-a" are two ways of saying the same thing. Why it sounds right: both describe a connection between classes, and both get drawn as a line on a diagram. What is true: they make opposite claims. Is-a says the child can be used anywhere the parent is expected. Has-a says one object contains or uses another and can substitute for nothing.
Interview Corner
Q1. What is association, in your own words? Two objects that know about or use each other, with no ownership either way. A delivery rider collects an order from a restaurant — they interact, but neither creates the other and neither controls how long the other exists.
Q2. What relationship does this suggest, and why?
class Document {
private:
std::vector<Page> pages;
public:
void addPage(std::string t) { pages.push_back(Page(t)); }
};
Composition. The pages are stored by value inside the document, so they are created by it and destroyed with it. A page here has no existence apart from its document.
Q3. Why is Band ** → **Musician aggregation rather than composition?
class Band {
public:
void addMember(Musician* m) { members.push_back(m); }
private:
std::vector<Musician*> members; // never created here, never deleted here
};
Because the musicians outlive the band. They existed before it formed, they join other bands afterwards, and the band never created or destroyed them — it only grouped them for a while. Composition would require the parts to die with the whole.
Q4. Does this class own the book?
class Library {
public:
void shelve(Book* b) { held = b; }
private:
Book* held = nullptr;
};
No. It stores a pointer to a book created elsewhere, and it never deletes it. Destroying the Library leaves the Book untouched, so this is aggregation. The pointer by itself proves nothing — what decides it is that the library neither creates nor destroys what it points at.
Q5. What single question separates aggregation from composition? If the whole is destroyed, should the part be destroyed too? Yes means composition; no means aggregation. Everything else — pointers, vectors, member objects — is just how that answer gets expressed in code.
Q6. A MusicPlayer uses a Speaker . Why is inheritance the wrong relationship? Because "a music player is a speaker" is false. Inheriting would give the player every speaker function as part of its own identity, and would lock in one kind of speaker forever, since an object cannot change type after creation. Holding a Speaker* keeps the sentence honest and lets the speaker be swapped.
Q7. What does this print, and what does the order tell you?
class OnlineForm {
public:
OnlineForm() : email("Email"), phone("Phone") {}
~OnlineForm() { std::cout << "form gone "; }
private:
FormField email;
FormField phone;
};
{ OnlineForm f; }
It prints form gone field gone field gone. The form's own destructor body runs first, then its member objects are destroyed automatically. Nobody wrote cleanup code for the fields — the composition relationship is built into the structure, which is the practical benefit of expressing it with member objects.
Q8. A Playlist holds Song objects. Should that be aggregation or composition? Aggregation, in almost any real design. The same song appears on many playlists and stays in the library after a playlist is deleted, so the playlist must not own it. If you need per-playlist data such as position or date added, introduce a PlaylistEntry that the playlist does own by composition, and let that entry point at the shared Song.
- 1.

Look at fragment A. What relationship does it show?
- 2.

Look at fragment B. What does the hollow diamond tell you?
- 3.

Look at fragment C. Which class is the whole, and what happens when it is destroyed?
- 4.

Look at . Which class is the base class, and how do you know?
The scenario
Design the object relationships for a music streaming app. The classes involved:
MusicPlayer · Playlist · PlaylistEntry · Song · Artist · AudioOutput · User · PremiumUser
No large application. Around fifteen minutes, and the work is deciding how each pair is related and why.
Task 1. For each pair, choose association, aggregation, composition or inheritance:
MusicPlayer↔AudioOutputPlaylist↔PlaylistEntryPlaylistEntry↔SongArtist↔SongPremiumUser↔User
Task 2. Then answer:
- Which of these should be stored as member objects held by value?
- Which should merely point at something owned elsewhere?
- Where does inheritance genuinely fit?
Task 3. A teammate suggests making MusicPlayer inherit from Speaker, since a player uses a speaker to make sound. Do you agree? Explain your answer in two sentences.
Hint — For every pair, ask the lifetime question: delete the first one — should the second disappear?
Watch out for pair 2 and pair 3. They look similar and they are not the same, which is the whole reason
PlaylistEntryexists.
A possible solution
class User {
public:
User(std::string n) : name(n) {}
virtual bool canSkipAds() const { return false; }
virtual ~User() = default;
std::string name;
};
class PremiumUser : public User { // IS-A
public:
PremiumUser(std::string n) : User(n) {}
bool canSkipAds() const override { return true; }
};
A song knows who made it, but never owns the artist:
class Artist {
public:
Artist(std::string n) : name(n) {}
std::string name;
};
class Song {
public:
Song(std::string t, Artist* by) : title(t), by(by) {} // knows its artist
std::string title;
Artist* by;
};
A playlist owns its entries, but an entry only points at a shared song:
class PlaylistEntry {
public:
PlaylistEntry(Song* s, int pos) : song(s), position(pos) {}
Song* song; // points at a shared song
int position;
};
class Playlist {
public:
Playlist(std::string n) : name(n) { entries.reserve(8); }
void add(Song* s) { entries.emplace_back(s, entries.size() + 1); }
void show() const {
for (const PlaylistEntry& e : entries)
std::cout << " " << e.position << ". " << e.song->title
<< " - " << e.song->by->name << "\n";
}
private:
std::string name;
std::vector<PlaylistEntry> entries; // owned outright
};