Inheritance vs. Composition is the object-oriented showdown that refuses to die. It's a battle waged since the moment someone thought modeling an Animal
was a good idea, and we've been living with the consequences ever since. Like most things in software, it started as a well-meaning design principle that evolved into a hill people are eager to die on in code reviews and architecture meetings. It's not just a source of pedantic infighting, but the choices you make will significantly affect how systems scale, how they evolve, and how often you end cursing yourself for implementing certain patterns.
Sure, nothing beats a good old-fashioned flame war over inheritance vs. composition. It's right up there with tabs vs. spaces (spaces are betterâfight me) and whether pineapple belongs on pizza. But personally, I prefer solutions that don't require a manifesto to maintain. Use what works, mix what doesn't. Leave the purity tests to Reddit threads and Twitter squabbles.
At first glance, inheritance makes sense. You start with a base class, and every new type adds a little more on top. A few layers deep however, you soon realize you're not building reusable components; you're pouring concrete. If you change the foundation, and everything above it cracks. If you override too much the whole thing will start to sag under its own weight.
Composition takes a different approach. Instead of stacking behavior vertically, it spreads it out in a way that is flat, modular, and easier to reason about. You attach behaviors based on what something does, not what it is. You are not forcing every object into a single, rigid lineage. Instead, you leverage roles, skills, and a little glue code. Composition gives you flexibility, but go too far and you're left with a junk drawer of behaviors loosely taped to something that used to resemble an object.
To make the tradeoffs between composition and inheritance clear, let's start by framing this around a domain that's already overloaded with ambiguity and awkward hierarchy: the workplace.
People, Abstracted
Let's model some people. They have job titles, shifting responsibilities, and a tendency to defy clean type hierarchies just by existing.
Modeling them using inheritance would lead to something like this:
class Person; end
class Engineer < Person; end
class Manager < Person; end
class Designer < Person; end
Seems reasonable. Everyone has a role. Responsibilities are distinct. You assume most shared behavior can live in Person
, and the rest flows down neatly through each subclass.
That is, until someone throws you a curveball: the new hire is an EngineeringManager
. They are just another branch from the Person
class so you try:
class EngineeringManager < Person; end
That doesn't provide you with the necessary context about what they do since they have specialized skills in two domains. So you ask yourself, Are they more of an Engineer or a Manager?
You decide to go with the lesser of two evils and proclaim that they are more Manager
than Engineer
:
class EngineeringManager < Manager; end
Soon after, you realize that even though the majority of their job is managing people, they have suddenly forgotten how to code! Flipping it around doesn't solve the problem either since they lose their management capabilities:
class EngineeringManager < Engineer; end
There's no clean way to inherit from both. Ruby doesn't support multiple inheritance (thankfully), and even if it did, you'd be knee-deep in the infamous diamond of death where shared ancestors introduce conflicting methods and initialization order becomes an interpretive art.
You can't inherit from both, and any attempt to manually stitch the two together duplicates logic or creates a fragile mess of overrides. And once you start aliasing methods to resolve name clashes, you're not modeling roles: youâre playing defense against your own class tree.
Working from the base Person
class doesn't solve this either, as you end up having to duplicate and maintain the implicit coupling between all three of these roles. This creates a myriad of problems and will drive code design into the ground. It's not DRY, not scalable, and most certainly not fun to debug when everything breaks at once.
Inheritance promised structure. What you got was a maze of tradeoffs held together by wishful thinking and method overrides.
Before you reach for the obvious rebuttal, What if we had multiple inheritance?
, let's talk about the diamond of death.
The Diamond of Death
Imagine for a moment a language that allows for multiple inheritance. EngineeringManager
inheriting from both Engineer
and Manager
, and both inherit from Person
. Now EngineeringManager
has two copies of Person
floating around in its method resolution path. Which one does it use? Whose initialize
gets called? What if both redefine schedule_meeting
or report_status
, but with subtly different assumptions? What happens if you need another class that inherits from the EngineeringManager
like a CTO
?
Congratulations! Youâve successfully created a debugging nightmare while giving your class hierarchy an identity crisis. You didn't solve the problem, you just created a new and significantly more annoying one.
Ruby dodges multiple inheritance entirely by not supporting it. If youâre modeling cross-functional roles, inheritance wonât just fight you; itâll schedule a meeting, decline its own invite, and then escalate to architecture review. The hierarchy you started with was supposed to bring clarity, but now it has you pinned in the break room and is asking, Why is the project taking so long?
Where do we go from here? It's become apparent that what we actually need to abstract is behavior as opposed to structure.
Composition
Where inheritance tries to model people by what they are, composition shifts the focus to what they do.
Note: Throughout this piece, composition refers to the design principle. This refers to assembling objects from small, focused behaviors. The term mixin will be used interchangeably in examples, as it's the most common implementation pattern for composition in many languages (including Ruby). If you're working in Rails, you might also encounter these bundled as concerns, which are just namespaced mixins with an opinionated structure.
Instead of enforcing a rigid hierarchy, you can instead model Person
-based classes by the skills or responsibilities that they possess. This makes the code flat, modular, and explicit.
module CanCode
def write_code
"Refactoring something that wasn't broken."
end
end
module CanManage
def schedule_one_on_one
"Meeting scheduled. Emotional support not included."
end
end
class Person
def initialize(name)
@name = name
end
end
class EngineeringManager < Person
include CanCode
include CanManage
end
No superclass politics in sight! No fights over which initialize
runs first, or why it now needs five arguments and a sacrificial ritual involving metaprogramming magic (and apparently a goat sacrifice according to the internal docs). No forgotten parent class quietly injecting bugs like a disgruntled employee.
You want to add mentoring? Plug it in. Need to remove management later because your engineer wants to go back to IC work? Yank the module. The behavior is now a set of discrete concerns, not an implicit grab-bag passed from one generation of objects to the next.
Crucially, composition keeps your objects from pretending to be more specific than they are. You're not saying this is a Manager. You're saying this object can manage. That subtle shift helps alleviate structure that would otherwise collapse under real-world complexity and ambiguity.
When Inheritance Actually Makes Sense
Composition isn't always the right tool. Sometimes, you don't need flexible behavior and need a solid structure. Something with a shared foundation, standardized utilities, and a few layout tweaks on each level.
We already have a Person
class, so what about their work environment? Say, an OfficeFloor
.
Every floor in a commercial building shares the same foundation, plumbing, electrical, elevator access; they're built to spec. The only real difference is the interior layout as some teams like open space, others want cubicles. Structurally, it's all the same skeleton.
This is where inheritance shines.
If the behaviors are predictable, tightly coupled to the structure, and unlikely to change independently, inheritance gives you a clean, centralized place to define them. The base class handles the foundational systems, and each subclass tweaks only the details that differ.
You define a base OfficeFloor
class:
class OfficeFloor
def plumbing
"Standard pipes, maintenance once a year."
end
def electrical
"120V with backup generator access."
end
def fire_safety
"Sprinklers and two stairwells per code."
end
end
Then, you can easily subclass for different layouts:
class EngineeringFloor < OfficeFloor
def layout
"Rows of desks, whiteboards, and at least one bean bag chair."
end
end
class MarketingFloor < OfficeFloor
def layout
"Open space, mood lighting, and a suspicious number of snack stations."
end
end
class FinanceFloor < OfficeFloor
def layout
"Closed offices, silence, and that one person who still prints emails."
end
end
Inheritance gives you a predictable scaffold. It communicates clearly: these things are the same, structurally. The differences are based on variations in data, not behavior.
If you reached for composition here, you'd just be reassembling the same plumbing and fire code compliance every time. Thatâs not flexible, thatâs boilerplate.
Inheritance shines when the framework is solid and shared, and all you're doing is rearranging the furniture.
Abstract Structure, Flexible Behavior
Composition gives you flexibility. Inheritance gives you structure. But real-world systems usually need both. Modern development practices lean toward using composition far more often than inheritance because maintainability is a major concern. Systems that are easy to change are usually better suited for ever-changing business applications. That doesn't mean that inheritance is not useful or practical in many cases, however.
In order to understand when to use inheritance, there are nuances to uncover and explore first. You want consistent scaffolding, but also want room to swap parts out when needed. That's where inheritance works best when paired with well-defined extension points: default behavior in the base class, overridden only when necessary.
class OfficeFloor
def plumbing
"Standard pipes, maintenance every 6 months."
end
def electrical
"120V with emergency backup lighting."
end
def layout
"Empty space. Awaiting tenant chaos."
end
end
class EngineeringFloor < OfficeFloor
def layout
"Whiteboards, standing desks, and at least one pair of noise-cancelling headphones per seat."
end
end
class HRFloor < OfficeFloor
def layout
"Private offices, warm lighting, and a strategically placed candy jar."
end
end
Here, the structure is defined. You don't need to rewire every floor from scratch; you only override what changes. The baseline structure stays untouched, consistent, and centralized.
This isn't about abstract classes in the classic sense. It's about building a stable skeleton and letting each piece of the system fill in the specifics without breaking the interface expectations underneath.
If you find yourself overriding everything, it might be time to refactor toward composition. But if the variation is thin (primarily cosmetic, data-level, or presentation-layer tweaks), inheritance remains a clean, readable solution.
Your project manager just sent you a Slack message that reads, âHey, forgot to mentionâsome floors need extra security. Also a video studio, nap rooms, and, uh⊠a place to keep the IT guy. You know why.â Unfortunately, you do know why. Motivated to get him out of the coworking space, you find inspiration and draft up some pseudocode to support your new mission.
You now have to support floors that can include any number of layout changes. You could subclass each one again, or you could compose that behavior so it can be shared across floors or tailored for shared use-cases:
module HasSecurity
def authorized?(employee)
employee.clearance_level >= :confidential
end
def security_features
"Keycard access, badge logging, and 24/7 surveillance."
end
end
module HasVideoStation
def record_video(subject:)
VideoRecorder.record_for(subject)
end
def video_station_features
"Green screen, props, and a professional camera."
end
end
module HasServerCloset
def administrate!
it_guy = Person.find_by(title: "IT Guy")
it_guy.fester!
tickets = HelpdeskTicket.order(created_at: :desc)
it_guy.work_in_between_lucky_star_episodes if tickets
end
def server_closet_features
"The IT guy has fully melded with his rolling chair, forming a new subspecies of cryptid known only by the orange sheen of his fingertips and the faint scent of melted plastic."
end
end
module HasNapRoom
def rest_on_cot(person:)
person.energy += 10
end
def nap_room_features
"A single cot with questionable back support and even more questionable cleanliness. Serves as a way to escape office chaos rather than sleeping."
end
end
class EngineeringFloor < OfficeFloor
include HasSecurity
include HasNapRoom
include HasServerCloset
def layout
"Whiteboards, standing desks, and a mysterious server closet that the lone IT Helpdesk guy with Cheeto-stained fingers inhabits."
end
end
class MarketingFloor < OfficeFloor
include HasVideoStation
include HasNapRoom
def layout
"Collaborative zones, neon signage, and approximately one ring light per person."
end
end
class LegalFloor < OfficeFloor
include HasSecurity
def layout
"Glass walls, quiet corners, and a bookshelf full of binders no one is allowed to touch."
end
end
These floors coexist without incident, despite their wildly different behaviors and Cheeto-stained IT closets. They work well together because:
Inheritance handles structure:
OfficeFloor
provides a shared foundationâplumbing, electrical, etc.âthat doesnât need to be redefined. Every subclass benefits from the consistent base behavior.Composition provides capabilities:
HasSecurity
,HasVideoStation
, andHasNapRoom
are clear, self-contained behavioral concerns. They're optional, reusable, and donât require you to alter the inheritance tree to add them.Each subclass is expressive: The
layout
definitions retain personality and intent, while the mixins provide an extensible âbehavioral footprintâ for each floor.It avoids subclass explosion: Instead of creating things like
EngineeringFloorWithNapRoomAndSecurity
, you compose the traits you need without bloating the class tree.
Choosing Between Inheritance and Composition
So when do you reach for one over the other? Here's a quick recap to help your code survive contact with reality:
Use Inheritance When:
You're modeling things with a shared, stable structure.
Most behavior is identical, and only small pieces need to be tweaked.
The relationship is truly âis-aâ, not âsort of has some traits of.â
You want predictable method resolution and centralized logic.
Your system benefits from polymorphism (e.g., rendering different
Shape
s, processingCommand
s).
Example: Every office floor has plumbing and power. You just rearrange the desks.
Use Composition When:
You're modeling capabilities, roles, or traits.
Objects need to opt in to behavior, not inherit it blindly.
You want loose coupling and reusable modules.
Youâre mixing behaviors across otherwise unrelated classes.
You need to vary behavior without breaking structure.
Example: Some floors have nap rooms and some don't. This is not structural; it's optional.
Use Both When:
You want a shared backbone with selectively applied features.
Structure is consistent, but optional enhancements vary per object.
You donât want to subclass for every combination of traits.
Your goal is to make your system explicit and adaptable without repeating yourself.
Example: The
OfficeFloor
defines what a floor is. Mixins likeHasSecurity
describe what a floor can do.
When in doubt, ask: is this a blueprint, or is it a behavior?
My Opinion
I'm firmly in the composition camp. It makes code easier to reason about, plays nicely with Rails conventions, and doesn't implode the moment someone changes a business requirement mid-sprint. Behavior modeled through loosely coupled modules is easier to extend, and I don't have to spelunk through five layers of method overrides to see what's going on. Metaprogramming is one of my favorite pastimes, and I'll happily drop everything to build internal tooling. Building modular structure is fascinating to me! So yes, I'm biased, but at least my code doesn't need a family tree to debug.
Inheritance feels great until you try to change anything. Then it's like defusing a bomb where every method override might cut the wrong wire. I prefer modules I can swap in and out, not behaviors baked into a class lineage like genetic baggage. Inheritance limits flexibility where metaprogramming shines: dynamic behavior, internal tools, and code that evolves without collapsing under its own ancestry. It's not that inheritance is evil, it's just that it doesn't scale well when real-world requirements start getting weird. And if there's one constant in this field? It's that the requirements always change. I prefer to hedge my bets and keep the bomb squad on standby.
Wrap-Up
Inheritance and composition aren't enemies. They each come with their own strengths, trade-offs, and very different failure modes.
Inheritance gives you structure. It's ideal when your objects follow the same rules, share the same skeleton, and only need light variation. But it gets brittle fast when real-world complexity starts poking holes in your carefully crafted hierarchy.
Composition gives you flexibility. You build objects from behaviors, not ancestry. It's flatter, more modular, and far more forgiving when your domain starts doing human thingsâlike having conflicting responsibilities or evolving mid-sprint.
Use inheritance when the abstraction is real. Use composition when it's not.
And when you need both? Don't be afraid to mix structure with mixins. A shared foundation with optional behavior is a pattern that scales.
If your mixins are unionizing and demanding clearer contracts, you may have abstracted a little too hard. If your class hierarchy needs a flowchart, an onboarding doc, and an intern named Kyle to explain it, maybe it's not that elegant. If your classes are starting to schedule meetings with each other, congratulations: you've modeled bureaucracy.