Programming paradigm comparison

OOP vs Functional Programming

Two fundamental ways of structuring code. Object-oriented programming (OOP) organises code around objects that hold state and expose behaviour. Functional programming (FP) organises code around pure functions that transform immutable data. Most modern languages support both — understanding the trade-offs makes you a better engineer and a more precise communicator in design reviews.

TL;DR

  • OOP — classes, objects, encapsulation, inheritance, polymorphism. State lives inside objects and is mutated through methods. Java, C#, Python, Ruby, and Swift lean OOP by default.
  • FP — pure functions, immutable data, no side effects, function composition. Data is transformed rather than mutated. Haskell, Clojure, Elm, and Elixir are FP-first; JavaScript, Scala, and Kotlin support both.
  • Most production code is pragmatic — use OOP for domain modelling with identity and lifecycle; use FP techniques for data pipelines, concurrent systems, and anywhere determinism matters.

Side-by-side comparison

AspectObject-Oriented (OOP)Functional (FP)
Core abstractionObjects with state and behaviourPure functions that transform data
StateMutable — encapsulated inside objectsImmutable — new data created on each transformation
Side effectsAccepted and managed via encapsulationMinimised or explicitly isolated
Code reuseInheritance and compositionHigher-order functions, function composition
TestingOften requires mocking for dependencies/statePure functions are trivially unit-testable
ConcurrencyTricky — shared mutable state causes race conditionsEasier — immutable data is safe to share across threads
DebuggingState history can be hard to traceDeterministic — same input always gives same output
Best forDomain modelling, GUIs, game entities, business logicData pipelines, compilers, financial calculations, concurrent systems
Language examplesJava, C#, C++, Python, RubyHaskell, Clojure, Erlang, Elm, F#, Elixir

What is Object-Oriented Programming?

OOP models software as a collection of objects — each object bundles data (fields) with the operations that can be performed on it (methods). The four classic pillars are:

  • Encapsulation — hiding internal state behind a public interface. A BankAccount class exposes deposit() and withdraw() but keeps the balance field private.
  • Inheritance — a class can extend another, inheriting its behaviour. SavingsAccount extends BankAccount re-uses the base logic and adds interest calculation.
  • Polymorphism — different classes can share the same interface. A Payment interface can be implemented by CreditCard, BankTransfer, and Crypto — the calling code treats all three identically.
  • Abstraction — exposing only the relevant details. Users of EmailService.send() do not need to know about SMTP headers or retry logic.
# Python OOP example
class BankAccount:
    def __init__(self, owner: str, balance: float = 0):
        self._owner = owner      # encapsulated — private by convention
        self._balance = balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount  # mutates state in place

    def withdraw(self, amount: float) -> None:
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount

    @property
    def balance(self) -> float:
        return self._balance


class SavingsAccount(BankAccount):          # inheritance
    def __init__(self, owner: str, rate: float = 0.02):
        super().__init__(owner)
        self._rate = rate

    def apply_interest(self) -> None:
        self._balance += self._balance * self._rate

What is Functional Programming?

FP treats computation as the evaluation of mathematical functions. The two cornerstones are:

  • Pure functions — given the same input, always return the same output, with no observable side effects (no mutating shared variables, no I/O, no randomness).
  • Immutability — data is never changed after creation. Transformations produce new data structures. This eliminates an entire class of bugs caused by shared mutable state.

Practical FP also relies heavily on higher-order functions — functions that take other functions as arguments or return them. The trio of map, filter, and reduce appears in almost every FP codebase.

// JavaScript functional example — no mutation, no side effects

// Pure function: same input → same output, nothing mutated
const applyInterest = (balance, rate) => balance + balance * rate;

// Higher-order functions with map / filter / reduce
const accounts = [
  { owner: 'Alice', balance: 1000 },
  { owner: 'Bob',   balance: 250  },
  { owner: 'Carol', balance: 800  },
];

const RATE = 0.02;

// map: transform each element, returns a NEW array
const withInterest = accounts.map(acc => ({
  ...acc,                                      // spread — never mutate original
  balance: applyInterest(acc.balance, RATE),
}));

// filter: keep accounts above threshold
const qualifying = withInterest.filter(acc => acc.balance >= 500);

// reduce: collapse to a total
const totalFunds = qualifying.reduce((sum, acc) => sum + acc.balance, 0);

console.log(totalFunds); // deterministic, easy to test

Notice that accounts is never modified. Each step returns a new collection. The logic for applyInterest can be tested independently without any setup or teardown.

Function composition is the FP equivalent of inheritance — small functions are combined into larger ones. In TypeScript, a pipeline of composed functions is easy to type-check and reason about.

State management: the central trade-off

The deepest difference between OOP and FP is how they handle state.

In OOP, state lives inside objects. An Order object might move through states: pending → paid → shipped → delivered. Methods mutate internal fields. This is intuitive for modelling real-world entities — an order really does have identity and lifecycle.

In FP, instead of mutating an Order, you call a function that takes the current order and returns a new one with updated status. Nothing is overwritten. This makes state changes auditable and reversible — the pattern behind Redux in React and the event-sourcing architecture pattern. See also microservices vs monolith for how event-sourcing changes service design.

Composition vs inheritance is a related debate. FP favours composition — building behaviour by combining small functions. OOP traditionally uses inheritance hierarchies, though modern OOP advice ("prefer composition over inheritance") has moved closer to the FP position.

How engineers talk about OOP vs FP

"We encapsulate the validation logic inside the model so callers can't put it in an invalid state."

Context: OOP design review. Encapsulate means bundle logic with the data it protects.

"This is a pure function — no side effects, same input always gives same output. It's trivial to unit-test."

Context: code review. Engineers use pure to mean deterministic and free of hidden dependencies.

"Don't mutate the object — return a new one with the updated field."

Context: FP-style PR feedback. Common in React codebases where immutable state updates prevent rendering bugs.

"We map over the list, filter out nulls, then reduce to a total. The whole pipeline is three lines."

Context: explaining data transformation logic. map/filter/reduce are the FP trio every developer should recognise.

"The interface defines the contractpolymorphism means the calling code doesn't care which implementation is injected."

Context: OOP architecture discussion. Contract and polymorphism are standard vocabulary in Java/C# design conversations.

"We use a higher-order function here — pass in the sorting strategy, so the logic is reusable across contexts."

Context: API design. A higher-order function takes another function as an argument, enabling flexible, composable behaviour.

"This class hierarchy is six levels deep — we're paying the inheritance tax. Let's flatten it with composition."

Context: refactoring discussion. Inheritance tax is informal but widely understood — deep hierarchies are fragile and hard to change.

"Side effects are isolated at the edges — the core business logic is all pure functions."

Context: FP architecture review. The pattern of keeping I/O at the boundaries and pure logic in the centre is called the "functional core, imperative shell."

Key vocabulary

Encapsulation
Bundling data and the methods that operate on it inside a class, hiding internal state from the outside world. External code interacts only through the published interface.
Inheritance
A class can extend another, reusing its behaviour. A subclass can override or extend methods. Overuse leads to rigid hierarchies — modern guidance prefers composition.
Polymorphism
Different types can share the same interface. Code that calls payment.process() works with any class that implements Payment, without knowing the concrete type.
Pure function
A function that always returns the same output for the same input and has no observable side effects. Easy to test, memoise, and parallelise.
Immutability
The property of data that cannot be changed after creation. Modifications produce new data structures rather than altering existing ones.
Side effect
Any observable interaction with the outside world — writing to a database, printing to the console, modifying a global variable. FP isolates side effects; OOP manages them through encapsulation.
Higher-order function
A function that takes other functions as arguments or returns a function — e.g., map, filter, reduce, sort.
Function composition
Combining two or more functions so the output of one becomes the input of the next — e.g., f(g(x)). The FP equivalent of building complexity from small, reusable pieces.
map / filter / reduce
The three canonical higher-order functions. map transforms each element; filter keeps elements matching a predicate; reduce collapses a collection to a single value.

Decision guide

  • Modelling real-world entities with identity and lifecycle (User, Order, Vehicle) → OOP
  • Building a data transformation pipeline (ETL, report generation, stream processing) → Functional
  • Highly concurrent system where shared state causes bugs → Functional
  • Team experienced with SOLID principles and design patterns → OOP
  • Want easy unit tests without mocking → Functional
  • Complex domain logic with many interacting rules → OOP or pragmatic mix
  • Working in React / Redux — state management already FP-flavoured → Lean into FP
  • Working in Java / C# enterprise codebase → OOP with FP techniques for data transforms
  • Starting fresh in a language like Scala or Kotlin → Blend both — they are designed for it

Frequently asked questions

What is the core difference between OOP and functional programming?

OOP organises code around objects — data structures bundled with the methods that operate on them. State is encapsulated inside objects and mutated through method calls. Functional programming treats computation as the evaluation of mathematical functions: data is immutable, functions have no side effects, and new data is created by transforming existing data rather than mutating it in place.

What does "immutability" mean in functional programming?

Immutability means that once a data structure is created, it cannot be changed. Instead of modifying an object in place, you create a new one with the updated values. This prevents bugs caused by shared mutable state — a function can never accidentally modify data that another part of the programme is using, which is especially valuable in concurrent systems.

What is a pure function?

A pure function always returns the same output for the same input and has no side effects — it does not read from or write to external state (no database calls, no console.log, no modifying variables outside its scope). Pure functions are trivial to unit-test (no mocking required) and safe to run in parallel, memoise, or inline.