All posts

OOP vs FP in JavaScript

Composition vs inheritance, the fragile base class problem, and when to use object oriented programming vs functional programming — a complete breakdown of the two paradigms.


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

FPOOP
StructureMany operations on fixed dataFew operations on common data
StateImmutable — create new versionsMutable — modify in place
FunctionsPure, no side effectsMethods with side effects
ParallelismSafe — no shared stateRisky — shared state conflicts
StyleDeclarativeImperative
First-classFunctionsObjects

JavaScript supports both. Most real-world codebases use both. The question isn’t “which is better?” — it’s “which fits this problem?”