We’ve spent serious time in both paradigms. Now it’s time to put them side by side — compare them, understand where each breaks down, and figure out when to reach for one over the other.
But before the head-to-head, there’s one debate we need to settle first.
Composition vs inheritance
This one gets argued about a lot. To understand why, start with what each one actually means.
Inheritance is a superclass extended to smaller pieces. A parent class with child classes that add or override things.
Composition is smaller pieces combined to create something bigger. Small functions assembled together to give objects their capabilities.
You’ve seen both. Inheritance through class extends. Composition through compose — small functions piped together into a pipeline.
The question is: which approach holds up better as software grows and changes?
Inheritance: structuring code around what things ARE
When you write a class hierarchy, you’re describing identity.
class Character {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() { /* deal damage */ }
}
class Elf extends Character {
constructor(name, weapon, type) {
super(name, weapon);
this.type = type;
}
}
class Ogre extends Character {
constructor(name, weapon, side) {
super(name, weapon);
this.side = side;
}
}
You’re saying: “An Elf is a Character. An Ogre is a Character.”
The structure is organised around what things are. They have data — properties — and methods that operate on that data, bundled together.
The tight coupling problem
The moment you define an inheritance chain, you’ve created a tight coupling between classes. A child class depends entirely on its parent.
Change something in Character — say, modify how attack() works — and that change ripples into every subclass. Maybe you wanted that. Maybe you didn’t know Ogre was doing something special with attack() and you just broke it.
Character (base class)
↓ extends
Elf — gets Character.attack()
Ogre — gets Character.attack()
Watcher — gets Character.attack()
Touch attack() in Character and you’ve touched all three. That’s the tight coupling problem — a small change in one place has unpredictable effects everywhere else.
The coupling between a child class and its parent is the tightest form of coupling there is. It’s the opposite of modular, reusable code.
The fragile base class problem
Tight coupling leads directly here: the base class becomes fragile.
Because everything inherits from it, the base class is incredibly sensitive to change. Modify it carefully and everything is fine. Modify it carelessly and subclasses break in ways that are hard to trace. The more subclasses you have, the more brittle the base class becomes.
Even small, well-intentioned changes — like adding a sleep() method — can cause unexpected conflicts down the inheritance chain.
The hierarchy problem
Now your game grows. You add User, Watcher, BossElf, JuniorElf. The hierarchy gets deeper.
User
│
Character
┌────┴────┐
Elf Ogre
┌──┴──┐
BossElf JuniorElf
And here’s where it starts to hurt.
JuniorElf just needs to sleep. That’s all. But because it inherits from Elf, which inherits from Character, which inherits from User, it gets everything. Every method. Every property. The whole chain.
What if requirements change and JuniorElf needs to sit above BossElf in the hierarchy? You’re not making a small tweak. You’re restructuring something that half your codebase depends on.
The gorilla banana problem
There’s a classic analogy that captures this perfectly:
“I just wanted a banana, but I got a gorilla holding the banana and the entire jungle.”
You asked for sleep(). You inherited a User, a Character, an Elf, and all their state, methods, and dependencies.
This is what deep inheritance does. Every subclass carries the weight of every ancestor. As the program grows and hierarchies get rewritten, the code becomes increasingly tightly coupled — refactoring becomes inevitable, and refactoring breaks things.
Composition: structuring code around what things DO
Composition flips the question. Instead of “what is this thing?”, you ask “what does this thing do?”
function createElf(name, weapon, type) {
return { name, weapon, type };
}
function getAttackAbility(character) {
return Object.assign({}, character, {
attack() { /* deal damage */ }
});
}
function getSleepAbility(character) {
return Object.assign({}, character, {
sleep() { /* restore health */ }
});
}
function getMakeFortAbility(character) {
return Object.assign({}, character, {
makeFort() { /* build a fort */ }
});
}
Each function adds a capability. State isn’t stored internally — it flows in, gets extended with new abilities, and flows back out. getAttackAbility accepts a character and returns it with attack attached. Simple.
Now describe what a character has:
// Elf can attack and sleep
const elf = getSleepAbility(getAttackAbility(createElf("Legolas", "bow", "forest")));
// Ogre can attack, build forts, and sleep
const ogre = getSleepAbility(getMakeFortAbility(getAttackAbility(createOgre("Shrek", "club"))));
// Junior elf just sleeps — and only sleeps
const juniorElf = getSleepAbility(createElf("Pip", "none", "junior"));
JuniorElf gets exactly what it needs. Nothing more.
createElf ──▶ getAttackAbility ──▶ getSleepAbility ──▶ elf
No hierarchy. No tight coupling. No ripple effects. Adding a new ability is just a new function. Removing one is just removing it from the chain.
The verdict on composition vs inheritance
Inheritance works. When your hierarchy is simple and stable, it’s clean and readable. The problems come when things grow and change — and software always grows and changes.
Composition is more flexible long-term. Because you’re building up from small pieces, adding, removing, or swapping capabilities is just a matter of adding or removing a function from the chain. There’s no base class to break, no hierarchy to restructure.
Most experienced developers lean toward composition. That doesn’t mean inheritance is always wrong — it means you should reach for it consciously, knowing its trade-offs.
The two paradigms, properly defined
A programming paradigm is a set of rules that shape how you organise and write code. Not a language feature — a way of thinking.
Object oriented programming says: organise code into units called objects or classes. A class is a box containing state (attributes) and the methods that operate on that state. Group related things together.
Functional programming says: write code as a combination of functions. Data is immutable. Functions are pure — the output depends only on the input, and the function doesn’t affect anything outside itself.
Because data is immutable and functions are pure, FP gives you very strong control over how a program flows. Functions can be reasoned about in isolation. The output is always predictable.
What each paradigm centres on
In OOP, objects are first-class citizens. The core building blocks are classes and the objects you instantiate from them. Four pillars hold the paradigm up:
- Abstraction — hide implementation details, expose only what’s needed
- Encapsulation — bundle related state and behaviour in one place
- Inheritance — share structure and behaviour through class hierarchies
- Polymorphism — the same interface, different implementations depending on the type
In FP, functions are first-class citizens. The core building block is the pure function. The paradigm rests on two ideas:
- Pure functions — no side effects, same input always returns same output
- Function composition — combine small pure functions to build complex behaviour
History and language landscape
Both paradigms have been around since the 1970s — or even earlier for FP, which traces its roots to lambda calculus.
OOP became dominant in the industry. It’s the default in C#, Java, and Python — languages built with classes at the centre.
FP has been around just as long but stayed more niche until recently. Haskell and Clojure are purely or heavily functional languages.
JavaScript is different. It lets you do both.
Head-to-head: the key differences
Operations vs data
FP is about performing many different operations on fixed data. You have data structures and many ways to transform them.
OOP is about a few operations on common data. Objects share behaviour through methods, and state evolves over time.
State
FP is stateless. Data is immutable — you don’t change it, you create new versions of it.
OOP is stateful. Objects hold state. Methods modify it. this.health -= damage is OOP thinking.
// OOP — mutate state directly
class Character {
takeDamage(amount) {
this.health -= amount; // modifying this
}
}
// FP — return new state, leave original untouched
function takeDamage(character, amount) {
return { ...character, health: character.health - amount };
}
Side effects
FP functions are pure — they don’t reach outside themselves. No modifying globals, no logging, no DOM manipulation inside a function.
OOP methods do modify things outside themselves. That’s often the entire point — changing the object’s state is what a method is for.
Parallelism
This is where FP pulls ahead on one dimension.
Because FP functions have no side effects and don’t share state, you can run them across multiple processors simultaneously with no conflicts. Pure functions are inherently safe to parallelise.
OOP doesn’t have that guarantee. Two threads modifying the same object’s state at the same time is a race condition waiting to happen.
This is one of the big reasons functional programming is rising in distributed computing, machine learning pipelines, and high-performance systems.
Declarative vs imperative
FP is declarative — you say what you want, not how to do it.
OOP tends to be more imperative — you describe steps, how state changes, how things get done.
// Imperative — step by step instructions
const alive = [];
for (let i = 0; i < characters.length; i++) {
if (characters[i].health > 0) {
alive.push(characters[i]);
}
}
// Declarative — describe the result
const alive = characters.filter(c => c.health > 0);
The declarative version says what you want. The reader doesn’t have to trace mutations — they see: filter characters to those with health above zero. Done.
When to reach for FP
FP shines when:
- You’re processing large amounts of data — analytics, pipelines, ETL
- You need to run operations in parallel across multiple processors
- You have a small set of well-defined data structures with many operations applied to them
- You want highly testable, predictable, bug-resistant code
Machine learning data pipelines. Analytics engines. Financial calculations. API data transformations. These are natural FP territory.
When to reach for OOP
OOP shines when:
- You have many distinct entities that each need to track their own state
- The operations on those entities are relatively few
- The real-world mental model of “things with behaviour” maps naturally to the problem
A game with characters, items, enemies, and levels — that’s natural OOP territory. Each character is a thing with its own state: health, position, inventory. OOP models that intuitively.
JavaScript does both — so do most real apps
Most languages lean one way. JavaScript lets you choose.
React is a perfect illustration of both paradigms working together.
// Class component — OOP style
class App extends Component {
constructor(props) {
super(props);
this.state = { users: [] };
}
fetchUsers() {
// calls API, modifies this.state
}
render() { /* ... */ }
}
// Functional component — pure function
function Card({ name, email }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
Card is a pure function. Same props in, same output out. No side effects. No state. No surprises.
The App class manages state the OOP way. The Card component transforms data the FP way. Both in the same codebase. Both right for their purpose.
React has moved strongly toward functional components over time — not because classes are wrong, but because pure functions are easier to test, reason about, and compose.
The two components of every program
Here’s the simplest way to frame it.
Every program has two primary things: data and behaviour.
OOP says: bring them together. Data and the behaviour that operates on it belong in the same object, the same class. Encapsulation is the principle. Organise around identity.
FP says: keep them separate. Data is data. Functions are functions. Mixing them creates hidden dependencies and shared state that’s hard to reason about. Organise around transformation.
Neither framing is universally correct. OOP leads to maintainable code. FP leads to maintainable code. The goals are identical — clear, extensible, efficient, DRY code. The approach is different.
The skill is knowing which tool fits the problem in front of you.
The short version
| FP | OOP | |
|---|---|---|
| Structure | Many operations on fixed data | Few operations on common data |
| State | Immutable — create new versions | Mutable — modify in place |
| Functions | Pure, no side effects | Methods with side effects |
| Parallelism | Safe — no shared state | Risky — shared state conflicts |
| Style | Declarative | Imperative |
| First-class | Functions | Objects |
JavaScript supports both. Most real-world codebases use both. The question isn’t “which is better?” — it’s “which fits this problem?”