Composition Over Inheritance

Composition Over Inheritance

A gentle, thorough walk through one of the most useful ideas in software design — what it means, why experienced engineers keep repeating it, and how to actually use it in your own code, explained with everyday pictures instead of jargon.

01

The Big Idea, in One Breath

Picture a box of building blocks. Not the kind that only fit together one specific way, but the kind where a wheel clips onto almost anything, a little engine snaps onto almost anything, and a seat snaps onto almost anything. You can build a race car today, take it apart in ten seconds, and build a spaceship tomorrow using half the same pieces. Nothing is glued together. Everything can be swapped, reused, and rearranged.

Now picture a different toy: a large plastic figure that came pre-molded from the factory. It already has arms, a face, and a paint job baked in. If you want a version with a jetpack instead of a cape, you cannot just clip one on — you need an entirely new figure, molded from scratch, that happens to share most of its shape with the old one. Any change, however small, means going back to the factory.

Those two toys represent two very different ways of building software. The first box of clip-together blocks is what programmers call composition. The second, pre-molded figure is what programmers call inheritance. Both are valid ways to build things. But after decades of building large, long-lived software systems, most experienced engineers have learned a hard-won lesson: reach for the clip-together blocks first, and only use the pre-molded figure when you are very sure you will never need to reshape it. That lesson has a name, and it is the whole subject of this guide: favor composition over inheritance.

This is not a rule that says inheritance is bad or forbidden. It is more like a piece of hard-earned travel advice: “when in doubt, pack light.” Packing light does not mean you can never bring a suitcase — it means you check, every single time, whether you truly need everything you are about to carry, because carrying too much makes the whole trip heavier later on. Software design works the same way. Every relationship you bake into your code is something you will be carrying for the life of that project, and the goal of this guide is to help you tell, ahead of time, which relationships are worth carrying and which ones will start to weigh you down.

i
Everyday Analogy

Think about how you get dressed in the morning. You do not own one single outfit that is permanently sewn together — shirt, jacket, shoes, and scarf all fused as one rigid object. Instead, you own separate pieces, and you combine them differently depending on the weather and the occasion. A rainy day swaps in a raincoat. A cold day swaps in a scarf. Nothing about your shirt needs to change just because the weather changed. That is composition: separate, swappable pieces working together, rather than one giant fused object that tries to be everything at once.

By the end of this guide, you will be able to look at almost any two pieces of code — or even two objects in real life — and quickly decide whether the honest relationship between them is “this is a kind of that” or “this simply comes with that as one of its parts.” That one small habit of thinking turns out to save an enormous amount of pain months or years down the road, long after the original code was written and the original reasons have been half-forgotten.

It is worth mentioning why this particular idea has survived so long in the industry, outliving countless other pieces of advice that came and went. Software is rarely written once and left alone — it is read, extended, and modified by people who were not in the room when the original decisions were made. A design choice that is easy for a stranger to understand six months later is worth far more than a design choice that only made sense to the person who wrote it on the day they wrote it. Composition tends to age well precisely because each small piece explains itself, on its own, without requiring anyone to trace a family tree to understand what is really going on.

2 Ideas
is-a vs has-a — the whole test in one line
10+
Worked, everyday-language examples in this guide
Since 1994
The Gang of Four’s original wording of the principle
02

What Is Inheritance, Really?

Inheritance is a tool that almost every object-oriented programming language provides. It lets you create a new class by starting from an existing class and adding or overriding a few things, instead of writing everything from zero. The new class is called a child class or subclass, and the class it borrows from is called the parent class or superclass.

Say you are building a small game. You start with a class called Animal that knows how to eat, sleep, and make a sound. Then you create a Dog class that inherits from Animal. The Dog class automatically gets eating, sleeping, and sound-making for free, and you only need to add the things that make a dog special, like fetching a ball. This feels efficient, and for a first pass, it usually is.

The technical description of this relationship is “is-a.” A dog is an animal. A car is a vehicle. A savings account is an account. Whenever you can honestly complete that sentence and have it hold true in every situation, forever, inheritance is describing something real about the world.

Three Things Inheritance Actually Gives You

It helps to be precise about what you are really getting when you write “extends” in your code, because people often reach for inheritance to get one of these three benefits, without noticing that composition could quietly deliver the same benefit with fewer downsides.

  • Code reuse. The subclass does not have to retype the parent’s logic. This is the benefit most beginners reach for first — but it is also, ironically, the weakest reason to choose inheritance, because composition can reuse code just as easily by holding a helper object.
  • Polymorphism. Many different subclasses can be treated as “just another instance of the parent type” by code that does not care about their specific differences. This is a genuinely powerful benefit, and it is one composition can also deliver, usually through a shared interface rather than a shared parent class.
  • Specialization. A subclass can override a piece of behavior to do something slightly different from its parent, while keeping everything else the same. This is the benefit that causes the most trouble, because “slightly different” has a way of becoming “completely different” as a project matures.
i
In Plain Words

Inheritance means “I am a special version of you, and I automatically do everything you do, unless I say otherwise.”

The trouble starts when that “is-a” sentence is only true most of the time, or only true today, or only true if you squint. Software requirements change constantly, and a relationship that felt permanent on day one can turn out to be temporary, or only partly true, by day two hundred. That is where the story gets interesting, and we will come back to it soon.

There is one more subtle detail worth knowing about inheritance before moving on: a subclass usually cannot fully exist until its parent has finished setting itself up first. Every time you create a Dog, the language quietly builds the Animal part of it first, behind the scenes, before your Dog-specific setup code even runs. This ordering is invisible most of the time, but it becomes a real source of confusion the moment a parent’s setup logic depends on something a subclass was supposed to provide — a small trap that composition simply does not have, since a helper object is fully built on its own before it is ever handed to whoever needs it.

03

What Is Composition, Really?

Composition takes a different approach. Instead of a class borrowing behavior from a parent, a class simply holds other objects inside itself and asks those objects to do the work. Nothing is inherited. Nothing is baked in at birth. Everything is assembled, piece by piece, the same way you might assemble a sandwich by placing bread, cheese, and vegetables next to each other rather than growing a sandwich tree.

Using the same game example, instead of a Dog class inheriting from Animal, you could give Dog a small object called a MovementBehavior, another called a SoundBehavior, and another called a DietBehavior. The Dog class does not know how barking works internally — it just asks its SoundBehavior object to make a sound, and trusts that object to know what to do. Swap in a different SoundBehavior object, and the dog now growls instead of barks, without touching the Dog class at all.

The technical description of this relationship is “has-a.” A dog has a way of moving. A car has an engine. A computer has a hard drive. Notice how much more flexible “has-a” feels compared to “is-a” — an engine can be pulled out and replaced with a different engine without the car stopping being a car.

Two Flavors of Composition

Not all “has-a” relationships behave exactly alike, and it is worth knowing the two common flavors, because the words show up often in professional conversation and in code reviews.

  • Composition (the strict kind). The held object cannot exist meaningfully without its owner. A house has rooms; a room without a house attached to it is a strange, incomplete thing. If the house is torn down, the rooms go with it.
  • Aggregation (the looser kind). The held object can exist perfectly well on its own, before, during, and after the relationship. A team has players, but a player still exists as a person even if the team disbands.

In everyday programming conversation, people often use the single word “composition” loosely to cover both flavors, and this guide does the same — the important shared idea is that one object holds a reference to another and delegates work to it, regardless of exactly how tightly the two are bound together.

In Plain Words

Composition means “I keep a few helpers inside me, and whenever I need something done, I hand the job to the right helper.”

One more thing worth noticing: composition does not require the helper objects to know anything about the class that holds them. A SoundBehavior object has no idea it is currently plugged into a Dog — it simply knows how to make a sound when asked. This one-directional awareness, where the container knows about its parts but the parts know nothing about their container, is a big part of what keeps composed systems easy to rearrange. You can lift a helper object out of one context and drop it into a completely different one, and it will keep working exactly the same way, because it never depended on its surroundings in the first place.

04

The Simplest Test: Is-A vs Has-A

Before writing a single line of code, there is a quick sentence test that catches most design mistakes early. Say the relationship out loud in both directions and see which one sounds natural and stays true under every circumstance you can imagine.

RelationshipSentenceNatural Fit
Dog and Animal“A dog is an animal”Inheritance (is-a)
Car and Engine“A car has an engine”Composition (has-a)
Penguin and FlyingBird“A penguin is a flying bird”Breaks down — not always true
Player and Inventory“A player has an inventory”Composition (has-a)
Rectangle and Square“A square is a rectangle”Mathematically true, often risky in code

Notice the last two rows. They are the classic traps. A penguin genuinely is a bird, but it cannot fly, so if your Bird class assumes every bird can fly, a Penguin subclass is forced to either lie about flying or awkwardly override the behavior with an error. A square genuinely is a mathematical rectangle with equal sides, but if your Rectangle class lets you set width and height independently, a Square subclass has to secretly keep both sides equal, which quietly breaks any code that assumed a rectangle’s width and height were independent. These are not made-up problems — they are two of the most commonly cited real examples in software design writing, precisely because they look so innocent at first glance.

There is a slightly more formal version of this same test that professional engineers refer to often, called the Liskov Substitution Principle, named after the computer scientist Barbara Liskov. Stated simply, it says that anywhere your code expects the parent type, you should be able to hand it any subclass instead, and everything should keep working correctly, with no surprises. The penguin and the square both quietly fail this test — code written for “any bird can fly” or “any rectangle’s sides are independent” breaks the moment a penguin or a square shows up. Whenever a subclass forces you to add a special case, an empty override, or a thrown error just to fit into the parent’s shoes, that is usually this same principle waving a small red flag.

i
Everyday Analogy

Imagine sorting toys into two bins by asking one question about each: “Is this fundamentally a kind of the other toy, forever, no exceptions?” or “Does this simply come with the other toy as one of its parts?” A remote-control car is a kind of car — that is stable and permanent. But a remote-control car has a battery, and batteries get swapped, upgraded, or removed entirely. The moment you catch yourself making an exception to an “is-a” rule, that is usually a signal the relationship was really a “has-a” relationship wearing a disguise.

05

The Building Blocks, Side by Side

It helps to see both approaches drawn out. Below, the left side shows a typical inheritance tree for a small game. The right side shows the same problem solved with composition, where independent behavior pieces are assembled inside each character rather than passed down a family tree.

INHERITANCE TREE Character Warrior Mage FlyingWarrior needs both traits COMPOSITION ASSEMBLY Character AttackStyle MoveStyle SoundStyle any style can plug into any Character — swap freely
Left: a tree that snaps in half the moment one character needs traits from two branches at once. Right: independent pieces that plug into any character, in any mix.

Look closely at the dashed red line on the left. That is a “FlyingWarrior” trying to squeeze into a family tree that was never built to hold a character with two different sets of traits at once. This is one of the most common moments where inheritance quietly starts to hurt — and it happens far more often than beginners expect, because real-world requirements rarely respect the tidy family trees we draw on day one.

The right-hand diagram illustrates something else worth noticing: the Character box on the composition side is small and plain on purpose. It does not know, and does not need to know, anything about how attacking, moving, or making sounds actually works. It just knows it has three named slots, and each slot can be filled with any object that fulfills the right promise. That plainness is exactly the point — a class built around composition tends to stay short, boring, and easy to read, while all the interesting variation lives in small, separately testable pieces around it.

06

Where Inheritance Breaks Down

Inheritance is not a mistake — it is a genuinely useful tool, and every major programming language includes it for good reason. But over many decades of building real systems, engineers have run into the same handful of painful patterns again and again. It is worth understanding each one, because recognizing them early is what saves you from a painful rewrite later.

The Fragile Base Class Problem

When dozens of subclasses all inherit from one shared parent class, that parent class becomes something everyone quietly depends on, even though almost nobody remembers exactly how. Change one line inside the parent to fix a bug, and you might accidentally break twelve unrelated subclasses that were secretly relying on the old, buggy behavior. The parent class becomes fragile — small, well-intentioned edits ripple outward in ways nobody can fully predict.

The Diamond Problem

Some situations call for a class to inherit traits from two different parents at once — a flying warrior that is both a Warrior and something that can Fly. Many languages either forbid this entirely, or allow it but create genuine confusion about which parent’s version of a method should win if both parents define it differently. This tangled shape, when drawn out, looks like a diamond, which is where the name comes from.

Deep, Tangled Hierarchies

A family tree that starts simple — Animal, then Dog, then GuideDog — can quietly grow five or six levels deep as a project matures. Understanding what a GuideDog actually does now means reading through five separate classes, scattered across five different files, mentally merging them together in your head. New engineers joining the project can lose days just tracing where a single method’s real behavior actually lives.

The White-Box Reuse Problem

When a subclass inherits from a parent, it typically gets full, unrestricted access to the parent’s internal details, not just its public promises. This is sometimes called “white-box” reuse, because the box is transparent — you can see and depend on everything inside. That sounds convenient, but it secretly glues the subclass to the parent’s internal implementation, not just its intended behavior. If the parent’s internals ever need to change, every subclass that quietly leaned on those internals can break, even if the parent’s public promises never changed at all.

Rigid at the Wrong Moment

Inheritance relationships are usually locked in when a class is written, not when the program is actually running. If a delivery vehicle needs to switch from “car mode” to “bicycle mode” partway through a trip — say, a courier parks the van and finishes the last block on foot with a handcart — a rigid inheritance tree has no clean way to represent that. The object was born a Car, and short of destroying it and creating an entirely new object, it stays a Car for its whole life.

Difficult to Test in Isolation

Testing a deeply inherited class often means indirectly testing every ancestor above it too, because the subclass’s behavior is really a blend of everyone in its family tree. A bug report that says “the fifth-level subclass behaves oddly” can turn into a scavenger hunt through five separate files before anyone finds the real cause.

!
Watch Out For

A hierarchy that felt clean and obvious on day one, but by month six has an awkward subclass that overrides half its parent’s methods just to throw an error or do nothing. That awkward subclass is usually the clearest sign that “is-a” was the wrong relationship from the start.

None of these problems are theoretical worries invented for the sake of this guide. They are exactly the failure patterns that pushed the original Gang of Four authors to write down their advice in the first place, back when object-oriented programming was still relatively new and many teams were leaning on deep inheritance trees by default, simply because the tool was new and exciting. Decades of hindsight across countless real projects are really what this section is summarizing.

07

Why Composition Often Wins

Composition solves each of the problems above in a fairly direct way, and it does so by shrinking how much any one piece of code needs to know about any other piece.

Flexibility

Swap parts at any time

Because behavior lives in separate, interchangeable objects, you can change what something does while the program is even running, not just at the moment a class was first written.

Black-Box Reuse

Depend only on promises

A class using composition only ever talks to the public interface of its helper objects, never their private internals. This is called “black-box” reuse, and it means internal changes to a helper rarely ripple outward.

No Deep Trees

Flat, shallow structure

There is no five-level family tree to trace. A class either has a helper object or it does not — one level, easy to scan, easy to reason about.

Mix and Match

Combine freely

Need a character that is both fast and stealthy? Just plug in a SpeedBehavior and a StealthBehavior together. No diamond problem, no awkward multi-parent hierarchy required.

There is also a quieter, longer-term benefit: composition tends to keep each class small and focused on one job, because there is no tempting shortcut of “just inherit it from the parent.” Classes that stay small and focused are, on average, easier to test in isolation, easier to explain to a new teammate in a sentence or two, and easier to delete cleanly when a feature is no longer needed.

This connects to a broader idea in software design called the Single Responsibility Principle — the notion that a class should have exactly one reason to change. Deep inheritance trees tend to violate this quietly, because a subclass ends up changing whenever any of its many ancestors change, for reasons that may have nothing to do with the subclass’s actual job. Composition naturally keeps each piece narrow, because a helper object that only knows how to calculate speed has exactly one reason to ever be edited: the way speed is calculated.

i
In Plain Words

Inheritance asks “what am I?” Composition asks “what can I do, and who’s helping me do it?”

None of this means composition is free of trade-offs — it simply trades a different, and usually more manageable, set of costs for the ones inheritance carries. Instead of worrying about a fragile shared ancestor, you occasionally have to worry about correctly wiring several small helper objects together. Most engineering teams find that trade worthwhile, because wiring problems tend to be loud and easy to spot — the program simply will not compile or will fail immediately — while fragile base class problems tend to be quiet and easy to miss until they cause damage somewhere unexpected, weeks after the actual change was made.

08

Composition in Action: A Worked Example

Let’s walk through the same small problem twice — once leaning entirely on inheritance, and once leaning on composition — so the difference stops being abstract and starts being something you can actually feel in the code.

Attempt One: Inheritance

Imagine a delivery app that needs to model different kinds of vehicles: a bicycle, a car, and a drone. All three need to report their speed and whether they can carry heavy packages. The first instinct is often to build one shared parent class.

Leaning on inheritanceclass Vehicle {
    int speed() { return 20; }
    boolean canCarryHeavyPackage() { return true; }
    void announceArrival() {
        System.out.println("Vehicle has arrived.");
    }
}

class Bicycle extends Vehicle {
    // Bicycles are slower and cannot carry heavy packages.
    // We have to override BOTH methods just to correct the parent's assumptions.
    int speed() { return 15; }
    boolean canCarryHeavyPackage() { return false; }
}

class Drone extends Vehicle {
    // Drones fly, so "announceArrival" doesn't really fit — but we inherited it anyway.
    int speed() { return 40; }
    boolean canCarryHeavyPackage() { return false; }
}

This already shows the early cracks. Both Bicycle and Drone spend their entire existence correcting assumptions the parent class made on their behalf. And if tomorrow the app adds a Boat, or needs a vehicle that can both fly and carry heavy packages, the tree starts to strain in exactly the ways described in the previous section.

Attempt Two: Composition

Now let’s rebuild the same feature using small, swappable helper objects instead of a shared parent.

Leaning on compositioninterface SpeedProfile {
    int speed();
}

interface CargoProfile {
    boolean canCarryHeavyPackage();
}

class SlowSpeed implements SpeedProfile {
    public int speed() { return 15; }
}

class FastSpeed implements SpeedProfile {
    public int speed() { return 40; }
}

class LightCargoOnly implements CargoProfile {
    public boolean canCarryHeavyPackage() { return false; }
}

class HeavyCargoCapable implements CargoProfile {
    public boolean canCarryHeavyPackage() { return true; }
}

class DeliveryVehicle {
    private SpeedProfile speedProfile;
    private CargoProfile cargoProfile;

    DeliveryVehicle(SpeedProfile speedProfile, CargoProfile cargoProfile) {
        this.speedProfile = speedProfile;
        this.cargoProfile = cargoProfile;
    }

    int speed() { return speedProfile.speed(); }
    boolean canCarryHeavyPackage() { return cargoProfile.canCarryHeavyPackage(); }
}

// Assembling a bicycle:
DeliveryVehicle bicycle = new DeliveryVehicle(new SlowSpeed(), new LightCargoOnly());

// Assembling a cargo drone, mixing traits freely:
DeliveryVehicle cargoDrone = new DeliveryVehicle(new FastSpeed(), new HeavyCargoCapable());

Nothing here is forced to correct anyone else’s assumptions. A DeliveryVehicle simply holds a speed helper and a cargo helper, and asks each one to do its job. Adding a Boat tomorrow means picking a SpeedProfile and a CargoProfile for it — no tree to restructure, no diamond problem, no inherited method that quietly does not apply. If the business later needs a “medium speed” option, you write one new small class implementing SpeedProfile, and every existing vehicle can use it immediately without a single line of existing code being touched.

Notice

Composition often leans on interfaces to describe a promise (“something that can report a speed”) separately from any specific way of keeping that promise. This separation of “what” from “how” is a big part of why composition stays flexible even as a project grows for years.

09

A Step-by-Step Refactoring Recipe

Suppose you already have a working inheritance hierarchy that has started to hurt — some subclasses override methods just to disable them, or a new requirement needs traits from two different branches at once. Here is a calm, repeatable way to migrate toward composition without breaking everything at once.

  1. Identify the varying behavior

    Look at what the subclasses actually override. Each overridden method is a hint that the behavior it controls wants to become its own separate, swappable object.

  2. Name an interface for that behavior

    Give the varying behavior a clear name and a small interface, describing only the promise — for example, “something that can calculate a price” — not any specific way of doing it.

  3. Move each variant into its own class

    Turn each subclass’s unique override into a small class that implements the new interface. These classes should be short, focused, and easy to name in a sentence.

  4. Replace inheritance with a held reference

    Give the original class a field holding the new interface type, instead of extending a subclass. Pass in the right implementation through the constructor.

  5. Delete the now-empty subclasses

    Once every caller has been updated to assemble the object with the right helper instead of instantiating a subclass, the old subclasses can usually be removed entirely.

  6. Verify with existing tests, then add new ones

    Run the existing test suite to confirm behavior did not change. Then add small, focused tests for each new helper class, since they can now be tested completely on their own.

This recipe rarely needs to happen in one giant leap. Most teams migrate one behavior at a time — first the pricing logic, a few weeks later the shipping logic, and so on — so the codebase is never in a broken state for long, and the team can pause the migration at any point without regret.

i
In Plain Words

Find what changes, give it a name, put it in its own small box, and hand that box to whoever needs it — instead of baking the change into a family tree.

10

Design Patterns Built on Composition

Once you start looking for it, composition shows up as the quiet backbone of several of the most well-known, battle-tested design patterns. None of these require inheritance to work — they all lean on objects holding and delegating to other objects.

The Strategy Pattern

This is essentially the delivery vehicle example above, generalized. A class holds a reference to an interchangeable “strategy” object, and the exact algorithm or behavior can be swapped in or out, even while the program is running. A navigation app choosing between “fastest route,” “shortest route,” and “avoid tolls” is a textbook use of this pattern.

The Decorator Pattern

Rather than creating a new subclass for every possible combination of features, a decorator wraps an object with another object that adds a little extra behavior, and that wrapped object can itself be wrapped again. A coffee shop’s ordering system is the classic teaching example: instead of a subclass for every combination of milk, sugar, and syrup, each add-on is a thin wrapper around the drink underneath it.

Delegation

Delegation is composition in its purest, simplest form: an object receives a request, and instead of handling it itself, it simply forwards the request to a helper object that knows how to handle it properly. It is such a fundamental building block that the Gang of Four’s own 1994 book on design patterns spends real time discussing it right alongside the “favor composition” advice itself, describing it as an especially direct example of object composition in practice.

Dependency Injection

This is composition applied at the level of an entire application. Instead of a class creating its own helper objects internally, those helper objects are handed to it from the outside — “injected” — usually by a framework or a small piece of setup code. This means the exact same class can be wired up differently in a test environment versus a production environment, without changing the class itself at all.

The Observer Pattern

An object that changes state holds a list of other objects interested in hearing about that change, and notifies each one when something happens. Nothing here relies on a shared parent class — any object that fulfills the small “listener” interface can be added to the list, which is composition quietly doing the work of keeping things loosely connected.

The Adapter Pattern

Sometimes two pieces of code want to work together but their interfaces do not quite match, the way a travel plug adapter lets a device from one country plug into a socket from another. An adapter object wraps the mismatched piece and translates calls between the two shapes, again relying on holding a reference rather than inheriting from either side.

What They Share

The common backbone

Behavior lives in small, focused, swappable objects. New behavior means writing a new small class, not editing old ones. Testing becomes easier, since fake helper objects can stand in for real ones.

What They Cost

The honest trade-offs

More small classes and interfaces to keep track of. Slightly more setup code to wire the pieces together. Can feel like overkill for a genuinely tiny, one-off program.

11

Composition Across Popular Languages

The core idea stays the same everywhere, but the exact tools available to express it differ a little from language to language. A quick tour helps connect this guide to whatever language you happen to work in day to day.

Java and C#

Both languages express composition through fields that hold interface types, exactly like the DeliveryVehicle example earlier. Interfaces describe the promise, concrete classes fulfill it, and constructors or setter methods wire the pieces together. Dependency injection frameworks are extremely common in both ecosystems specifically because composition benefits so much from having a consistent way to wire objects together across a large application.

Python

Python supports composition the same way, but its flexible, dynamic nature also allows a looser style sometimes called “duck typing” — if an object has the right method with the right name, Python is often happy to use it, without requiring a formal interface declaration at all. This makes composition feel especially lightweight in Python, though many teams still choose to define formal interfaces using Python’s abstract base classes, purely to keep large codebases predictable and self-documenting.

JavaScript and TypeScript

Plain JavaScript objects are naturally suited to composition, since you can attach behavior to an object simply by giving it a property that holds a function or another object, with no class hierarchy required at all. TypeScript adds optional interfaces on top of this, giving teams the choice to describe promises formally when a project grows large enough to benefit from that extra structure, while still keeping the underlying composition-friendly nature of the language.

Go and Rust

These two newer, widely respected languages are worth a special mention, because both were deliberately designed without traditional class inheritance at all. Go achieves reuse through a technique called “embedding,” where one struct includes another as a named field and automatically exposes its methods — composition, essentially, built directly into the language’s core design. Rust achieves similar goals through “traits,” small interfaces that any type can implement, combined with structs that hold other structs as fields. The fact that two modern, carefully designed languages chose to skip class inheritance entirely, and lean on composition-like tools instead, is itself a strong real-world data point in favor of the whole idea this guide is exploring.

Java / C#
Interfaces + fields, often with DI frameworks
Python / JS
Loosely typed, composition feels natural and light
Go / Rust
No class inheritance at all — composition by design
12

How Composition Changes Testing

One of the most practical, day-to-day benefits of composition rarely gets mentioned in the abstract — it shows up the moment you sit down to write automated tests. Because a class built with composition only depends on the interfaces of its helper objects, you can hand it a fake, simplified stand-in helper during a test, instead of the real one.

Suppose DeliveryVehicle from earlier needed to call a real GPS service to calculate speed in a live traffic system. Testing that directly would mean waiting on a real network call every time the test runs, which is slow and unreliable. With composition, a test can instead hand DeliveryVehicle a tiny fake SpeedProfile that instantly returns a fixed number, with no network call at all.

A simple test doubleclass FixedTestSpeed implements SpeedProfile {
    public int speed() { return 99; }
}

DeliveryVehicle testVehicle =
    new DeliveryVehicle(new FixedTestSpeed(), new LightCargoOnly());

assert testVehicle.speed() == 99;

This pattern is so common in professional testing that it has its own family of names — test doubles, fakes, stubs, and mocks are all slightly different flavors of the same basic idea: a small, predictable stand-in object used only during a test. Inheritance-heavy code can still be tested, but it usually requires more elaborate tools, because a deeply inherited class often cannot be cleanly separated from its ancestors the way a composed class can be separated from its helpers.

Notice

A good rule of thumb: if writing a test for a class requires setting up several unrelated ancestor classes just to get started, that class is probably leaning on inheritance more than it needs to.

13

When Inheritance Still Makes Sense

“Favor composition over inheritance” is deliberately worded as “favor,” not “always use.” Inheritance remains the right tool in a genuine set of situations, and a skilled engineer reaches for it on purpose, not by default.

True Is-A

The relationship is rock solid

When “is-a” holds true in every situation you can reasonably imagine, and the parent class is small and stable, inheritance is simple and honest.

Shared Framework Code

Built for extension

Many UI frameworks and libraries are explicitly designed around a small, carefully controlled base class meant to be extended — that is the intended, documented use.

Small, Closed Hierarchies

Two or three levels, done

A short, shallow hierarchy that the team fully understands and rarely touches carries little of the risk of a deep, sprawling one.

True Polymorphism

Interchangeable by design

When the entire point is that many types should be usable in exactly the same way through one shared type, inheritance (or an interface) is directly expressing that intent.

In fact, many of the design patterns discussed in the previous section still use a small amount of inheritance internally — usually a single interface or a shallow one-level hierarchy — while relying on composition for everything above that. The advice was never “delete inheritance from your toolbox.” It was “reach for the more flexible tool first, and only accept the more rigid one when you are confident the rigidity will not hurt you later.”

It is also worth remembering that the Gang of Four’s original 1994 book, the very source of this advice, is itself full of patterns that use inheritance deliberately and well. Their point was never that inheritance is broken — it was that inheritance is commonly reached for out of habit, to solve reuse problems that composition solves more safely, and that engineers should slow down and ask which relationship is really true before choosing.

A useful habit, once you have internalized the is-a versus has-a test from earlier in this guide, is to treat every new “extends” keyword as a small decision worth pausing on for a moment, the same way you might pause before signing a long-term contract. Ask whether the relationship will still be true a year from now, under requirements nobody has thought of yet. If the honest answer is “probably,” inheritance is a fine choice. If the honest answer is “maybe, but I’m not fully sure,” that uncertainty itself is usually a good enough reason to reach for composition instead.

14

Side-by-Side Comparison

AspectInheritanceComposition
Relationship“is-a”“has-a”
Bond formed atCompile time, mostly fixedCan be assembled or changed at runtime
Access to internalsWhite-box — full internal accessBlack-box — only the public interface
Reuse styleBorrow behavior from an ancestorDelegate work to a held object
CouplingTighter — subclasses depend on parent internalsLooser — depends only on an interface’s promise
Combining traitsAwkward; risks the diamond problemNatural; simply hold more than one helper
TestingOften requires setting up ancestor classesEasily tested with small fake helpers
Best forSmall, stable, genuinely permanent “is-a” factsBehavior that may need to change, grow, or mix over time
1994
Year the Gang of Four’s book popularized the phrase
2nd
It was the second core object-oriented principle in that book
1 Level
Composition usually only needs one — hold and delegate
15

Common Pitfalls When Switching to Composition

Over-Engineering a Tiny Script

Not every program needs interfaces, strategy objects, and dependency injection. A fifty-line script that will be thrown away next week does not need the same architectural care as a system that will run in production for ten years. Composition adds real value on growing, long-lived systems — it can add unnecessary ceremony to something genuinely small and short-lived.

Too Many Tiny Objects

It is possible to swing too far and split behavior into so many microscopic helper objects that following the flow of a single request means jumping through a dozen files. The goal was always readability and flexibility, not fragmentation for its own sake. A helper object should represent one clear, nameable responsibility — not just an excuse to avoid a two-line “if” statement.

Forgetting to Program Against an Interface

Composition delivers most of its flexibility when a class depends on an interface — a promise — rather than on one specific concrete helper class. If a class hard-codes exactly which helper class it holds, swapping that helper later requires editing the class anyway, quietly losing much of the benefit composition was supposed to provide.

Mistaking “No Inheritance” for “No Structure”

Composition is not an excuse to skip planning. A well-composed system still benefits from clear naming, a small number of well-thought-out interfaces, and a shared understanding across the team of which helper object is responsible for what. Composition removes a rigid family tree — it does not remove the need for thoughtful design.

Wiring Fatigue

As an application grows, someone has to actually connect all these small, swappable pieces together — deciding which speed profile goes with which vehicle, and so on. Without a clear, consistent place where this wiring happens, it can spread out across the codebase and become its own confusing puzzle. Many teams solve this with a dedicated dependency injection framework or a single, well-documented “assembly” area of the code, precisely so the wiring itself stays organized.

!
Watch Out For

Rewriting an entire working inheritance hierarchy into composition purely because “composition is the modern best practice,” without a real, felt problem driving the change. Refactor when a hierarchy is actually causing pain — not simply because a guide like this one recommends it in the abstract.

The common thread running through every pitfall in this section is the same: composition is a tool for managing change, not a trophy to be collected for its own sake. Applied thoughtfully, on the parts of a system that genuinely need flexibility, it earns its keep many times over. Applied everywhere, indiscriminately, it can leave a codebase feeling scattered rather than organized — which is precisely the opposite of what the idea was meant to achieve.

16

A Few More Real-World Analogies

Kitchen

A restaurant’s stations

A restaurant does not clone its entire kitchen for every new dish. It has a grill station, a salad station, and a dessert station — separate, reusable stations that combine differently depending on the order. That is composition, plated.

Music

A band, not a soloist

A band assembles a drummer, a guitarist, and a singer — each independently swappable — rather than one performer trying to be a fused drummer-guitarist-singer hybrid who can never take a night off without cancelling the whole show.

Furniture

Modular shelving

Modular shelving units let you add, remove, or rearrange individual cubes as your space changes. A single, permanently welded bookshelf cannot grow with you the same way.

Sports Team

Substitutions

A coach can swap a tired player for a fresh one mid-match without redesigning the entire team. Software built with composition can do the same thing with its “players” — the helper objects — while the game keeps running.

i
Everyday Analogy

Perhaps the clearest of all: a smartphone. A phone has a camera, has a battery, has a screen — none of these are “kinds of phone,” they are parts a phone is built from. When a newer camera comes out, engineers do not invent a brand-new species of phone; they simply put a better camera part inside the same design. That is composition working exactly as intended, and you carry a working example of it in your pocket every day.

Even the toy box from the very first section of this guide is worth revisiting one more time. Notice that the clip-together blocks were never anti-structure — a well-designed set of blocks still has a small number of standard connector shapes that every piece agrees to use, the same way a well-designed set of interfaces gives every helper object a standard shape to plug into. Composition is not the absence of a plan. It is a plan built around small, agreed-upon connection points instead of one rigid, permanent shape.

Taken together, these analogies point at the same underlying truth from a dozen different angles: the world we live in is already full of composed systems, and we navigate them instinctively every day without ever needing a computer science degree to understand why they work so well. Software built the same way tends to feel just as natural to work with, once you get used to looking for the “has-a” relationships hiding in plain sight.

17

Frequently Asked Questions

Does this mean I should never use “extends” in my code?

No. It means “extends” should be a deliberate choice made because the “is-a” relationship is genuinely permanent and true, not the automatic first thing you reach for whenever two classes share a little bit of behavior.

Is composition always more code to write?

Usually a little more upfront — you write an interface plus one or more small implementing classes, rather than one subclass. In exchange, you typically write far less code later, because adding new behavior rarely requires touching existing classes at all.

Can I use both inheritance and composition in the same project?

Yes, and most healthy, real-world codebases do exactly that. A small, shallow inheritance hierarchy for genuinely permanent “is-a” facts, combined with composition for anything that might change, grow, or mix over time, is a very common and very reasonable combination.

How do I know if my hierarchy has already gone too deep?

A helpful early warning sign is needing to scroll through more than two or three classes just to understand what one method actually does. Another is finding a subclass that overrides a method purely to throw an error or do nothing at all.

Does composition slow down my program?

In the overwhelming majority of applications, the difference is far too small to notice, and modern language runtimes are heavily optimized for exactly this style of delegation. The readability, testability, and flexibility benefits almost always outweigh a difference that is not measurable in practice.

Where did this advice originally come from?

It traces back to the influential 1994 book on object-oriented design patterns written by four authors often nicknamed the “Gang of Four.” It appeared as the second of their core object-oriented design principles, discussed at length alongside the closely related idea of delegation.

18

Glossary

TermDefinition
InheritanceA class automatically gaining the behavior of a parent class, expressing an “is-a” relationship.
CompositionA class holding a reference to another object and delegating work to it, expressing a “has-a” relationship.
Subclass / SuperclassThe child class that inherits, and the parent class it inherits from.
InterfaceA description of a promise — what an object can do — without saying exactly how it does it.
DelegationForwarding a request to a helper object rather than handling it directly.
White-Box ReuseReuse where the reusing class can see and depend on the internal details of what it reuses, as with inheritance.
Black-Box ReuseReuse where only the public interface is visible, as with composition.
Fragile Base ClassA shared parent class that becomes risky to change because many subclasses secretly depend on its exact internal behavior.
Diamond ProblemThe ambiguity that arises when a class would need to inherit from two parents that both define the same method differently.
Liskov Substitution PrincipleThe idea that any subclass should be usable anywhere its parent type is expected, without breaking anything.
Test DoubleA small, simplified stand-in object used in place of a real one during automated testing.
19

Key Takeaways

Remember This

  • Inheritance models “is-a” — a genuinely permanent, always-true relationship between a specific thing and a general category.
  • Composition models “has-a” — one object holding and delegating to other, independently swappable objects.
  • Inheritance grants full, “white-box” access to a parent’s internals, which quietly increases coupling and fragility over time.
  • Composition only depends on a helper’s public interface, its “black-box” promise, which keeps classes looser and easier to change.
  • Deep hierarchies, the diamond problem, and the fragile base class problem are the classic warning signs that inheritance has overstayed its welcome.
  • Well-known patterns — Strategy, Decorator, Delegation, Observer, Adapter, and Dependency Injection — are all, at their core, composition in different outfits.
  • Composition also tends to make automated testing simpler, since small fake helper objects can stand in for real ones.
  • Inheritance is still the right call for small, stable, genuinely permanent “is-a” facts — the advice is “favor,” never “forbid.”
  • The simplest test that catches most mistakes early: say the relationship out loud both ways, and trust whichever sentence stays true in every situation.

Leave a Reply

Your email address will not be published. Required fields are marked *