Part 01 · Low Level Design

Class Relationships and UML

AssociationAggregationCompositionComposition over inheritanceUML

Knows-about, has-a and owns: association, aggregation vs composition, why has-a usually beats is-a, and UML class diagrams quick enough to draw on a whiteboard.

⏱ 2-3 hours · Station 3 of 80

The batch that took a teacher with it

A coaching centre's app has batches, teachers and students. When the admin cancels the HSC-Physics batch, the code deletes the batch row "and everything inside it": its class sessions, its enrolments, and its teacher.

Karim sir also teaches HSC-Math. The next morning his Math batch shows "teacher: unknown".

The bug isn't in the delete query. It's in a question nobody asked when designing the classes: does a batch own its teacher, or just point at him? Sessions belong to the batch and should go with it. A teacher doesn't.

This chapter is about the four ways classes can be connected, how to tell them apart, and how to draw them so the next developer doesn't have to guess.

The problem: "has a" means four different things

"A batch has a teacher", "a batch has sessions", "a batch has students", "the report printer has a batch". Same words, four different relationships:

RelationshipWhat happens when the batch is deleted?
Batch has sessionsSessions go too: they make no sense alone
Batch has a teacherTeacher stays: he exists on his own and teaches other batches
Batch has studentsStudents stay, and their list of batches must be updated
Report printer has a batchNothing: the printer only borrowed it for one call

If the code treats all four the same way, either things get deleted that shouldn't be (the teacher), or things are left dangling (enrolments pointing at a batch that no longer exists).

Think of a flat and its furniture

You rent a flat. The rooms belong to the flat: when the building is demolished, the rooms are gone. That's composition.

Your furniture is in the flat, but it's yours: when you move out, the sofa goes with you to the next flat. That's aggregation.

You and your neighbours know each other, and either of you could move away without the other disappearing. That's an association.

The plumber comes for an hour, uses your kitchen and leaves. He doesn't live there. That's a dependency.

Batch- name: string- teacher: *Teacher- sessions: []Session- students: []*Student+ Cancel()Teacher+ Name: stringSession+ Date: stringStudent+ Name: stringReportPrinter+ Print(b *Batch)teachersessionsstudentsusesaggregation ◇: the teacher outlives the batchcomposition ◆: sessions live inside the batchassociation →: a batch knows its studentsdependency ⇢: uses a batch for a moment, keeps nothingcancel(): the sessions go with the batchthe teacher and the students stay
Press play. The four relationships around a Batch light up one at a time. Then the batch is cancelled: its sessions go with it, while the teacher and the students stay.

Where the comparison breaks down. In real life ownership is obvious. In code it's just a field holding a reference, and every kind of relationship looks the same at the syntax level. The difference lives in who creates the object, who deletes it, and who else may hold it. You have to decide that on purpose and write it down.

Predict first

A Batch is cancelled. Which objects should disappear with it?

Four relationships, and why has-a usually beats is-a

  1. Association ("knows about"): two objects are linked, and each can exist without the other. Students and batches: many-to-many, and both sides may need to find each other. Drawn as a plain line, often with numbers like * to *.
  2. Aggregation ("has, but shares"): a whole holds parts that live on their own and may be shared. A batch has a teacher; the teacher has other batches too. Drawn with a hollow diamond on the whole's side.
  3. Composition ("owns"): the whole creates its parts and they die with it. Nobody else holds them. A batch owns its sessions. Drawn with a filled diamond. In code, the owner usually builds the part itself and never hands out a reference that outlives it.
  4. Dependency ("uses for a moment"): one class uses another as a parameter or local variable and keeps no reference. PrintSummary(batch) depends on Batch. Drawn as a dashed arrow. It's the weakest link and the cheapest one.
  5. Inheritance ("is a"): TeachingAssistant is a Teacher. It's the strongest link of all: the child gets every public method of the parent, forever. That's why composition over inheritance is the default advice: give a class a field holding the behaviour it needs, instead of becoming a subclass to get it. A person who is both a student and a teaching assistant is easy with composition (a person with roles) and awkward with inheritance (which class do you extend?).
  6. UML class diagrams are just boxes (name, key fields, key methods) and these lines. In an interview, a quick sketch with the right line types answers "who owns what" before you write any code.

Cancelling a batch, step by step

Step through it

school.Cancel(HSC-Physics)

SchoolBatchSessionsTeacher and students

Step 1/4 The school decides to cancel

School is where batches are opened and cancelled, so this is where the clean-up rules live.

open
HSC-Physics, HSC-Math

One question per line you draw

For every line between two classes, ask: if this one goes away, what happens to that one?

  • It goes too → composition. The owner creates it and no one else holds it.
  • It stays, and others share it → aggregation.
  • It stays, and both sides keep track of each other → association.
  • Nothing, because no reference was kept → dependency.

And before you draw an inheritance arrow, try saying the sentence out loud with "always": "a teaching assistant is always, in every way, a teacher." If you hesitate, use composition.

Try it: which relationship is this?

Try each case

Name the relationship

order.lines = [line1, line2]
works: Composition. Order lines are created for this order and mean nothing without it. Delete the order, the lines go.

The coaching centre in code

A School opens batches. Each Batch owns its sessions, shares a teacher, and is associated with many students. PrintSummary only depends on a batch. Then HSC-Physics is cancelled.

package main

import (
	"fmt"
	"strings"
)

// Teacher and Student live on their own; batches only point at them.
type Teacher struct {
	Name    string
	batches []*Batch
}

type Student struct {
	Name    string
	batches []*Batch
}

// Session is owned by its batch (composition): it is created inside the batch and
// has no meaning outside it.
type Session struct{ Topic string }

type Batch struct {
	Name     string
	teacher  *Teacher   // aggregation: shared, outlives the batch
	sessions []Session  // composition: owned, dies with the batch
	students []*Student // association: many-to-many, both sides know
}

// School is where batches are created and cancelled.
type School struct{ batches []*Batch }

func (s *School) Open(name string, t *Teacher, topics ...string) *Batch {
	b := &Batch{Name: name, teacher: t}
	for _, topic := range topics {
		b.sessions = append(b.sessions, Session{Topic: topic})
	}
	t.batches = append(t.batches, b)
	s.batches = append(s.batches, b)
	return b
}

func (b *Batch) Enroll(st *Student) {
	b.students = append(b.students, st)
	st.batches = append(st.batches, b)
}

// Cancel removes the batch everywhere it is referenced. Its sessions need no clean-up:
// nothing else holds them.
func (s *School) Cancel(b *Batch) {
	s.batches = without(s.batches, b)
	b.teacher.batches = without(b.teacher.batches, b)
	for _, st := range b.students {
		st.batches = without(st.batches, b)
	}
}

func without(list []*Batch, b *Batch) []*Batch {
	out := []*Batch{}
	for _, x := range list {
		if x != b {
			out = append(out, x)
		}
	}
	return out
}

func names(list []*Batch) string {
	if len(list) == 0 {
		return "(none)"
	}
	n := []string{}
	for _, b := range list {
		n = append(n, b.Name)
	}
	return strings.Join(n, ", ")
}

// PrintSummary only uses a batch for a moment (dependency): it keeps no reference.
func PrintSummary(b *Batch) {
	st := []string{}
	for _, s := range b.students {
		st = append(st, s.Name)
	}
	fmt.Printf("%s: teacher %s, %d sessions, students %s\n",
		b.Name, b.teacher.Name, len(b.sessions), strings.Join(st, ", "))
}

func main() {
	school := &School{}
	karim := &Teacher{Name: "Karim"}
	rahim, nadia := &Student{Name: "Rahim"}, &Student{Name: "Nadia"}

	physics := school.Open("HSC-Physics", karim, "Vectors", "Motion", "Waves")
	math := school.Open("HSC-Math", karim, "Matrices", "Calculus")
	physics.Enroll(rahim)
	physics.Enroll(nadia)
	math.Enroll(rahim)

	PrintSummary(physics)
	PrintSummary(math)
	fmt.Println("Rahim's batches:", names(rahim.batches))

	fmt.Printf("cancel %s: its %d sessions go with it\n", physics.Name, len(physics.sessions))
	school.Cancel(physics)
	fmt.Println("Karim still exists, teaches:", names(karim.batches))
	fmt.Println("Rahim still exists, batches:", names(rahim.batches))
	fmt.Println("Nadia still exists, batches:", names(nadia.batches))
	fmt.Println("open batches:", names(school.batches))
}

All six print exactly this:

HSC-Physics: teacher Karim, 3 sessions, students Rahim, Nadia
HSC-Math: teacher Karim, 2 sessions, students Rahim
Rahim's batches: HSC-Physics, HSC-Math
cancel HSC-Physics: its 3 sessions go with it
Karim still exists, teaches: HSC-Math
Rahim still exists, batches: HSC-Math
Nadia still exists, batches: (none)
open batches: HSC-Math

Reading the output:

  • The first three lines show the links: Karim teaches both batches, Rahim is in both, Nadia only in Physics.
  • Cancel: Physics had 3 sessions. After cancelling, nothing anywhere refers to them; they were never shared.
  • Karim still exists and still teaches Math. Only his link to Physics was removed.
  • Rahim and Nadia still exist. Nadia now has no batches, which is a valid state for a student.
  • What each language changed: in Go, Python, TypeScript and Java the garbage collector frees the sessions once the batch is unreachable. C++ makes ownership explicit: the school holds batches in unique_ptr, sessions are stored by value inside the batch, and teachers/students are raw non-owning pointers, so cancelling destroys the batch and its sessions at that line. Rust stores links as IDs and the school owns everything; setting the batch slot to None drops the batch and its sessions, and the borrow checker won't let anything keep a reference to them.

Getting relationships right in real code

  • The owner creates, the owner deletes. If a class builds its parts in its own constructor or factory method, that's a strong sign of composition, and it should be the only one to remove them.
  • Many-to-many usually becomes its own object. Enrollment{student, batch, joinedOn, fee} soon needs data of its own. In a database it's a join table; in code it's often a class.
  • Keep both sides in sync in one method. Enroll updates the batch and the student together. If two separate calls do it, one will eventually be forgotten.
  • Database foreign keys mirror these choices: ON DELETE CASCADE for composition (sessions), ON DELETE RESTRICT or SET NULL for aggregation (a batch must not delete its teacher).
  • Draw before you code in interviews. Five boxes and the right lines in two minutes make the conversation about the design, not about syntax.

Designs that cause problems later

Try each case

Pick the broken relationship

DELETE batch → also DELETE its teacher
breaks: The teacher is aggregated, not owned. Deleting him breaks every other batch he teaches.

Pause and think

The product team says: "Teachers can also enrol as students in other batches, to learn new subjects." With Teacher and Student as separate classes, how would you model Karim taking an English batch?

Reveal

Don't make Teacher extends Student or the other way round. Pull out a Person (name, phone, ID) and give batches links to a person in a role: a batch has one teaching person and many enrolled persons. Or keep Teacher and Student as role objects that each point at the same Person. Either way, Karim is one person with two roles, which is composition, and his data isn't duplicated.

Quick quiz

1. What's the one-sentence test that separates composition from aggregation?

Answer

If the whole is deleted, does the part go with it? Yes, and nobody else holds it: composition. No, it lives on and may be shared: aggregation.

2. Why is a many-to-many association often turned into its own class?

Answer

Because the link soon needs its own data (join date, fee, status). An Enrollment object holds that, and both sides refer to it.

3. What does "composition over inheritance" mean in practice?

Answer

Give a class a field that holds the behaviour or data it needs, instead of subclassing to inherit it. It keeps classes loosely tied, lets behaviour change at run time, and avoids awkward "is a" claims.

4. In UML, what do a filled diamond and a dashed arrow mean?

Answer

A filled diamond is composition (the whole owns the part). A dashed arrow is a dependency (one class uses another briefly).

Your challenge: a teacher who leaves

Add School.Resign(teacher, replacement): every batch taught by the leaving teacher is handed to the replacement.

  • Add a teacher Salma at the start of the story.
  • At the end, Karim resigns and Salma takes over.
  • Print Salma teaches: and Karim still exists, teaches: after it.

Before you run it, predict both lines.

You've succeeded when the new lines are Salma teaches: HSC-Math and Karim still exists, teaches: (none), Karim still exists as an object, and the batch's sessions and students are unchanged.

In one line

For every link, ask what happens to one side when the other goes away: gone with it is composition, stays and is shared is aggregation, stays and both sides know each other is association, and nothing because nothing was kept is dependency. Prefer has-a over is-a, and let the owner be the only one who creates and deletes its parts.

Station stamp

Finish the chapter

Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.

Go Pro to earn XP →