Appearance
9.2.8 — Composition over Inheritance, Mixins and Delegation
The delivery app has three vehicle types and three pricing zones. Somebody models it with inheritance:
typescript
class DeliveryChannel { /* shared bits */ }
class BikeDelivery extends DeliveryChannel { /* … */ }
class UrbanBikeDelivery extends BikeDelivery { /* … */ }
class SuburbanBikeDelivery extends BikeDelivery { /* … */ }
class RuralBikeDelivery extends BikeDelivery { /* … */ }
class DroneDelivery extends DeliveryChannel { /* … */ }
class UrbanDroneDelivery extends DroneDelivery { /* … */ }
// … and six moreNine leaf classes for three vehicles and three zones. Then the business adds a fourth zone, and you write three more classes. Then it adds "priority" as a third axis, and nine classes become twenty-seven.
This is class explosion, and it has one cause: inheritance can only express one axis of variation, and the world usually has several. A class has one parent, so the moment your problem varies along two independent dimensions, the hierarchy has to encode every combination as a separate leaf.
Composition does not have that limit, and this page is about the three techniques that follow from it.
1. Composition: hold it instead of being it
The fix is to stop asking "what kind of channel is this" and start asking "what parts does this channel have".
typescript
class DeliveryChannel {
constructor(
private readonly vehicle: Vehicle, // (1) axis one
private readonly pricing: ZonePricing, // (2) axis two
private readonly tracker: TrackingService, // (3) a shared service
) {}
async deliver(order: Order): Promise<DeliveryResult> {
const cost = this.pricing.quote(order.distance, this.vehicle.costPerKm); // (4)
const id = await this.vehicle.dispatch(order);
await this.tracker.record(order.id, id);
return DeliveryResult.dispatched(id, cost);
}
}Lines (1) and (2) are the two axes, each now a field instead of a place in a hierarchy. Line (3) is an ordinary collaborator. Line (4) combines them.
Count what you need to write: three vehicle classes plus three pricing classes is six small classes, and they produce all nine combinations. Add a fourth zone and it is one class, not three. Add a third axis with three values and it is three more classes covering twenty-seven combinations, where inheritance needed twenty-seven classes.
typescript
const urbanBike = new DeliveryChannel(new Bike(), new UrbanPricing(), tracker);
const ruralVan = new DeliveryChannel(new Van(), new RuralPricing(), tracker);The arithmetic is the whole argument. Inheritance multiplies classes as axes multiply. Composition adds them. Three axes of four values each is 64 classes with inheritance and 12 with composition.
2. What else composition buys
The class count is the headline, and four other benefits matter as much in practice.
You can change behaviour while the program is running. A subclass is fixed the moment the object is created. A field can be replaced:
typescript
channel.usePricing(new SurgePricing(1.8)); // during a rainstorm, then back afterwardsDoing that with inheritance means creating a different object and migrating whatever state it held.
Coupling stays visible and thin. A collaborator is reached through an interface with a few named methods, and that is the entire surface. A parent class exposes its protected members and its internal call structure, which is the fragile base class problem from 9.2.4. The compiler can check the first kind of coupling and cannot check the second.
Each piece can be understood, replaced and reasoned about on its own. UrbanPricing is a small class with one job, readable in a minute, with no parent to consult.
The parts are reusable outside this hierarchy. UrbanPricing can be used by the quoting engine and the estimate widget, neither of which is a delivery channel. A base class's behaviour is only available to its descendants.
The cost, stated honestly so you are not surprised by it: more objects to wire up. Somebody has to create the vehicle, create the pricing, and pass them in. With inheritance, new UrbanBikeDelivery() is one call. That wiring cost is real, and the answer is to collect it in one place — the composition root from 9.2.3 section 6 — or behind a factory (9.4.2) that hands back ready-made combinations.
3. Delegation: passing the work along
Delegation is composition with a specific shape: your object receives a call, and hands it to an object it holds.
typescript
class AuditedWallet implements WalletOperations {
constructor(private readonly inner: Wallet, private readonly log: AuditLog) {}
spend(amount: Money): void {
this.inner.spend(amount); // (1) pass it along
this.log.record("spend", amount, this.inner.balance); // (2) add your own bit
}
get balance(): Money { return this.inner.balance; } // (3) pure forwarding
}Line (1) hands the real work to the wrapped wallet. Line (2) is the value this class adds. Line (3) forwards without adding anything, and it is the price you pay: with delegation you must write a method for everything you want to expose, whereas inheritance would have given you all of them free.
That trade — write the forwarding methods, get control over exactly what is exposed — is delegation's whole character, and there is a real advantage hiding in it. With inheritance you get every parent method whether it makes sense or not, which is how refused bequest happens. With delegation you expose only what belongs, so AuditedWallet can decide not to expose an internal method that would let callers bypass the audit.
Where you have already met this shape. Wrapping something to add behaviour while keeping the same interface is the Decorator pattern (9.4.8). Wrapping to change the interface into one your code already speaks is the Adapter pattern (9.4.7). Wrapping to control access — permissions, caching, laziness — is the Proxy pattern (9.4.10). All three are delegation with different intents, which is why they look so similar in code and why knowing the intent is what tells them apart.
The one real difference from inheritance, worth being precise about. With inheritance, when a parent method calls another method on itself, the call finds the subclass's override. With delegation it does not:
typescript
class Wallet {
spend(amount: Money): void { this.debit(amount); } // calls its own debit
protected debit(amount: Money): void { /* … */ }
}
class AuditedWallet {
constructor(private inner: Wallet) {}
spend(a: Money) { this.inner.spend(a); }
debit(a: Money) { /* audited version — never called by inner.spend */ } // (1)
}Line (1) never runs when inner.spend calls this.debit, because inside inner, this is inner. Inheritance would have routed it to your override.
That difference is exactly why inheritance is fragile and delegation is not. The routing that makes inheritance powerful is the same routing that lets a parent's internal change break you silently. Delegation gives up the power and gets predictability, and predictability is worth more in code you have to maintain.
4. Mixins: sharing behaviour without a parent
Sometimes several unrelated classes need the same small capability — timestamps, soft deletion, serialisation — and none of them should share a parent. A mixin adds behaviour to a class without a permanent inheritance relationship.
In TypeScript the standard form is a function that takes a class and returns an extended class:
typescript
type Constructor<T = {}> = new (...args: any[]) => T; // (1)
function Timestamped<TBase extends Constructor>(Base: TBase) { // (2)
return class extends Base { // (3)
createdAt = new Date(); // (4)
private updatedAt = new Date();
touch(): void { this.updatedAt = new Date(); } // (5)
get lastModified(): Date { return this.updatedAt; }
};
}
class Order { constructor(readonly id: OrderId) {} }
class TimestampedOrder extends Timestamped(Order) {} // (6)
const o = new TimestampedOrder("ord_1");
o.touch();
console.log(o.createdAt, o.lastModified); // → both work, fully typedLine (1) names the type "anything that can be called with new". The any[] is needed because the mixin cannot know what constructor arguments the base takes.
Line (2) is the mixin: a function taking a class and returning a class. Line (3) returns an anonymous class extending whatever came in. Line (4) adds state and line (5) adds behaviour.
Line (6) applies it. Timestamped(Order) produces a new class at runtime, and TimestampedOrder extends that. TypeScript infers the combined type, so o.id, o.createdAt and o.touch() are all known.
Mixins stack, which is the feature:
typescript
class AuditedSoftDeletableOrder extends Auditable(SoftDeletable(Timestamped(Order))) {}What you gain: capabilities compose without a single-inheritance slot being spent, and the same Timestamped works on orders, customers and invoices, which share no parent.
What you pay, and this is why mixins stay a specialist tool:
Name collisions are silent. If Auditable and SoftDeletable both define touch, the outer one wins and nothing warns you. In a deep stack that is genuinely hard to debug.
Order matters and it is invisible. Auditable(SoftDeletable(X)) and SoftDeletable(Auditable(X)) can behave differently, and nothing in the code says which order is correct.
Types get complicated fast. Error messages from a four-deep mixin stack are long and hard to read, and any mixin needing constructor arguments makes the typing considerably harder.
It is still inheritance underneath, so the fragile base class problem has not gone away — it has been made dynamic.
The rule: reach for a mixin when the capability is small, self-contained, stateless or nearly so, and genuinely needed by unrelated classes. For anything with real logic, hold a collaborator instead. A Timestamped mixin is fine. A Payable mixin containing your payment logic is a class pretending not to be one.
5. The functional spelling
In JavaScript and TypeScript there is a fourth option that is often the simplest, and it is worth knowing because most modern library code uses it.
typescript
type Vehicle = { costPerKm: Money; dispatch(order: Order): Promise<DispatchId> };
function makeDeliveryChannel(vehicle: Vehicle, pricing: ZonePricing, tracker: Tracker) {
return { // (1)
async deliver(order: Order): Promise<DeliveryResult> {
const cost = pricing.quote(order.distance, vehicle.costPerKm); // (2)
const id = await vehicle.dispatch(order);
await tracker.record(order.id, id);
return DeliveryResult.dispatched(id, cost);
},
};
}Line (1) returns an object literal with methods rather than a class instance. Line (2) reaches the parts through the closure — vehicle and pricing are captured by the function, so there are no fields and no this at all (3.6.2 explains the capture).
What this buys. The captured values are genuinely private, more so than # fields, because nothing outside the closure can name them. There is no this, so you can pass channel.deliver as a callback without the binding problem from 9.2.5 section 7. And it is less code.
What it costs. Each object gets its own copy of every method, whereas class instances share one copy on the prototype. For a handful of objects this is irrelevant; for a million it is real memory. There is no instanceof check, so error hierarchies and type narrowing by class do not work. And inheritance-shaped extension is unavailable, which is usually fine and occasionally not.
Which to use. Classes when you have identity and rules to protect, when you need instanceof, or when there will be many instances. Closures when you are wiring up a handful of long-lived services at startup, which is exactly what this example is. Both are composition, and the choice between them is smaller than the choice to compose in the first place (3.6.2 section 7 covers the deeper equivalence).
6. The decision procedure
Everything in this page and 9.2.4 reduces to a short sequence you can run in a review.
Start with composition. Give the class a field, not a parent. This should be the default, and it should require no justification.
Ask whether you want the parent's type or the parent's code. If you want callers to treat the two interchangeably, you want an interface and possibly inheritance. If you only wanted a method the parent already has, you want composition — this is the single most common mistake, and the tell is the sentence "I extended it because it already had X".
Use inheritance only when all three hold: it is a genuine is-a where the child works anywhere the parent does with no surprises, the hierarchy is stable rather than growing sideways every quarter, and the extension points are deliberate and documented.
Use a mixin when the capability is small and shared across unrelated classes, and accept the collision and ordering risks.
Watch for these three signals, each of which says composition: you are overriding a method to do less or to throw; the class count is multiplying as requirements are added; or you cannot describe the relationship without the phrase "sort of a kind of".
7. The larger point about the four pillars
You now have all four of the words people recite when asked about object-oriented programming, and they are worth putting back together, because they are not four separate ideas.
- Encapsulation hides state that changes behind operations that do not.
- Abstraction hides which concrete thing you are talking to behind a role that stays the same.
- Polymorphism routes a call to whatever is playing that role right now.
- Inheritance declares which things are allowed to play it.
All four are the same instruction from 9.1, applied to four different materials: isolate what changes behind what does not. Encapsulation applies it to state, abstraction to contracts, polymorphism to dispatch, inheritance to types. When you can derive each one from that single sentence rather than reciting them from a list, you have stopped memorising and started designing — and the next chapter's principles will read as five more derivations rather than five more slogans.
One last thing worth saying plainly, because both camps get dogmatic about it. Objects and functions are not rivals. The strongest default in modern practice is a functional core — immutable value objects and pure calculations, which are trivial to reason about and to test — surrounded by an imperative shell of objects that own state, effects and roles, such as repositories, notifiers and controllers. Rich entities guard their rules, value objects make data lawful, pure functions compute, and interfaces mark the seams. Neither "everything must be a class" nor "classes are evil" survives contact with a real system, and both camps end up rediscovering the other's tools.
What the next chapter adds. You now have the mechanisms. Chapter 9.3 covers the principles that tell you when to apply which one: DRY, KISS, YAGNI, the five SOLID principles, and the smaller rules that experienced engineers actually use day to day.
Recall
- Class explosion: inheritance has one slot, so two axes of variation force every combination to become a class. Three vehicles × three zones is nine classes with inheritance and six with composition, and a fourth zone costs three classes versus one.
- Composition also buys runtime swapping, thin visible coupling through an interface instead of a parent's internals, parts you can read on their own, and parts reusable outside the hierarchy. It costs wiring, which you collect in a composition root or a factory.
- Delegation is composition where you forward calls to a held object and add your own bit. You must write the forwarding methods, and in exchange you expose only what belongs. Decorator, Adapter and Proxy are all delegation with different intents.
- The key mechanical difference: a parent's internal self-call finds the subclass's override, but a wrapped object's self-call stays inside the wrapped object. That routing is what makes inheritance powerful and fragile at the same time.
- Mixins add a capability without spending the inheritance slot, written as a function taking a class and returning an extended class. They stack. The costs are silent name collisions, invisible order dependence, complicated type errors, and the fact that it is still inheritance underneath. Use for small self-contained capabilities only.
- The closure spelling — a factory function returning an object of methods — gives real privacy and no
thisproblems, at the cost of per-object method copies and noinstanceof. - Decision order: compose by default; ask whether you want the parent's type or its code; inherit only for genuine, stable, documented is-a; mixin only for small shared capabilities. Signals that say compose: overriding to do less, multiplying class counts, and needing the phrase "sort of a kind of".
- The four pillars are one instruction on four materials: isolate what changes behind what does not — state, contracts, dispatch, types.
Self-test: Give the arithmetic argument for composition over inheritance with three axes of four values. What does delegation cost you that inheritance gives free, and what do you get back? Why does a wrapped object's internal call not reach your override, and why is that a feature? Name the three real risks of mixins. State the four pillars as one sentence.
Quiz Bank
FoundationalMake the case for composition over inheritance using the class-explosion example, then steel-man inheritance.
The case. Take delivery channels that vary by vehicle (bike, drone, van) and by pricing zone (urban, suburban, rural). With inheritance, each combination must be its own leaf class, because a class has only one parent and the hierarchy can only encode one axis. That is nine classes, and a fourth zone adds three more, and a third axis multiplies everything again. With composition, the vehicle and the pricing become two fields, so three vehicle classes plus three pricing classes cover all nine combinations, a fourth zone is one class, and a third axis of three values adds three classes rather than eighteen. Inheritance multiplies as axes multiply; composition adds.
Four benefits come with it. Behaviour can be swapped while the program runs, since a field can be reassigned and a superclass cannot. The coupling is a small named interface the compiler checks, rather than a parent's protected members and internal call order, which nothing checks. Each part is small enough to read and test on its own. And the parts are reusable outside this hierarchy, since UrbanPricing also serves the quoting engine.
The steel-man. Inheritance is right where substitutability is the actual point and the taxonomy is stable. Framework extension surfaces, where the framework calls you and a declared skeleton with named holes is the honest shape. Error hierarchies, where catch plus instanceof AppError handling a whole family is exactly what you want and nothing else provides it as cleanly. Abstract classes that own a genuinely shared algorithm and leave small documented holes, which is Template Method. What these share is that you are declaring "these are interchangeable kinds of one thing", not "I needed a method this class already had".
The rules of engagement to close with: inherit for substitution and never for reuse alone, keep hierarchies one or two levels deep, and treat overriding a method to do less than the parent as proof you wanted composition.
AppliedWhat is delegation, how does it differ from inheritance mechanically, and why is the difference a feature rather than a limitation?
Delegation is holding an object and forwarding calls to it, usually adding something on the way — an audit record, a cache check, a permission check. You implement the interface yourself and pass the real work along.
The mechanical difference. With inheritance, when a parent method calls another method on itself, the call resolves through the actual object, so it finds the subclass's override. With delegation it does not: inside the wrapped object, this refers to the wrapped object, so your version is never reached from within it.
Why that is a feature. The routing that makes inheritance powerful is exactly the routing that makes it fragile. Because a parent's internal calls reach subclass overrides, a subclass ends up depending on which internal methods the parent happens to call, and that dependency is written down nowhere. The parent changes its internals legally and the subclass breaks silently, which is the fragile base class problem (9.2.4). Delegation gives up the automatic routing and gets predictability: the wrapped object behaves exactly as it does on its own, always, and your wrapper's behaviour is exactly what you wrote.
The cost is real and worth stating: you must write a forwarding method for everything you want to expose, where inheritance gives you all of them free. The compensation is control. Inheritance hands you every parent method whether it makes sense or not, which is how refused bequest happens; delegation exposes only what belongs, so a wrapper can decline to expose a method that would let callers bypass it.
Where you have seen it: Decorator adds behaviour behind the same interface, Adapter changes the interface to one your code already speaks, and Proxy controls access. All three are delegation, distinguished by intent rather than by shape.
InterviewWhat are mixins, when do they earn their place, and what do they cost?
A mixin adds a capability to a class without a permanent parent relationship. In TypeScript the standard form is a function that takes a class and returns a class extending it, so class TimestampedOrder extends Timestamped(Order) {} produces a class with the base's members plus the mixin's, fully typed. They stack, so several capabilities can be applied to one class.
When they earn their place: the capability is small and self-contained, it is genuinely needed by classes that share no parent, and it carries little or no state. Timestamps, soft deletion and a serialisation helper are good examples. The alternative would be either copying the code into each class or inventing an artificial common ancestor, and both are worse.
What they cost, and these are the reasons they stay a specialist tool. Name collisions are silent: if two mixins in a stack define the same method, the outer one wins and nothing warns you. Order matters and is invisible, so A(B(X)) and B(A(X)) can differ with no indication of which is correct. Type errors from a deep stack are long and hard to read, and mixins needing constructor arguments are considerably harder to type. And it is still inheritance underneath, so the fragile base class problem has not disappeared — it has been made dynamic, which is harder to reason about, not easier.
The rule: if the capability has real logic or real dependencies, hold a collaborator instead. A Timestamped mixin is fine. A Payable mixin containing payment logic is a class that is pretending not to be one, and it should be one.
StaffYour team is split: half want a class hierarchy for a new document-processing feature, half want plain functions and objects. How do you resolve it?
Refuse the framing first. "Classes versus functions" is not the decision — it is a proxy for several real decisions that should be made separately, and settling it as a philosophy question means the losing half will disagree with every future review.
Decision one: where do the rules live? If documents have invariants that must always hold — a processed document always has a checksum, a page count is never negative — those rules need an owner, which means a class or a validated value object. If the data is a payload passing through with no rules, it is data, and functions over plain types are the right answer. Both usually exist in the same feature.
Decision two: how does the behaviour vary? If new document types keep arriving and the operations are stable, that is the axis classes handle well — a role and one class per type (9.2.5). If the types are fixed by a file-format specification and you keep adding new operations — extract text, redact, thumbnail, index — that is the axis a tagged union and functions handle better, and the compiler will list every site to update when a variant is added.
Decision three: what is at the boundary? Anything that touches the outside world — storage, an OCR service, a queue — should sit behind a small role so the core can be exercised without it. That is a shared requirement both camps will agree with once it is stated as testability rather than as architecture.
Then name the shape most teams converge on, which usually ends the argument because both halves recognise their preference in it: a functional core of immutable document values and pure transformations, wrapped in a thin imperative shell of objects that own effects and roles. The people who wanted functions get the pure core, the people who wanted classes get the roles and the invariant guards, and neither is being asked to give up what they were actually protecting.
Finally, put a decision record in writing naming the axis of change you expect, since that is the assumption everything else rests on, and it is the thing to revisit in six months if the feature grew in a direction nobody predicted.