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
| Aspect | Object-Oriented (OOP) | Functional (FP) |
|---|---|---|
| Core abstraction | Objects with state and behaviour | Pure functions that transform data |
| State | Mutable — encapsulated inside objects | Immutable — new data created on each transformation |
| Side effects | Accepted and managed via encapsulation | Minimised or explicitly isolated |
| Code reuse | Inheritance and composition | Higher-order functions, function composition |
| Testing | Often requires mocking for dependencies/state | Pure functions are trivially unit-testable |
| Concurrency | Tricky — shared mutable state causes race conditions | Easier — immutable data is safe to share across threads |
| Debugging | State history can be hard to trace | Deterministic — same input always gives same output |
| Best for | Domain modelling, GUIs, game entities, business logic | Data pipelines, compilers, financial calculations, concurrent systems |
| Language examples | Java, C#, C++, Python, Ruby | Haskell, 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
BankAccountclass exposesdeposit()andwithdraw()but keeps the balance field private. - Inheritance — a class can extend another, inheriting its behaviour.
SavingsAccount extends BankAccountre-uses the base logic and adds interest calculation. - Polymorphism — different classes can share the same interface. A
Paymentinterface can be implemented byCreditCard,BankTransfer, andCrypto— 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 contract — polymorphism 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 implementsPayment, 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.
maptransforms each element;filterkeeps elements matching a predicate;reducecollapses 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.
Can OOP and functional programming be combined?
Absolutely — and most modern codebases do exactly this. Scala, Kotlin, and Swift blend OOP with FP features natively. JavaScript and Python support both paradigms freely. In practice you might use classes to model domain entities with identity and lifecycle, and use pure functions with map/filter/reduce for data transformation pipelines. The dichotomy is a teaching device; real code is pragmatic.
Which paradigm is better for testing?
Functional programming tends to be easier to test because pure functions are inherently unit-testable — call with input, assert output, no mocking required. OOP code with shared mutable state often requires mocking frameworks and elaborate setup to isolate objects. That said, well-designed OOP (small classes, dependency injection) is also highly testable.
What languages are primarily functional?
Haskell and Erlang are purely functional — they enforce immutability and no side effects at the language level. Clojure, F#, Elm, and Elixir are functional-first. Scala and Kotlin are hybrid OOP+FP. JavaScript, Python, Ruby, and modern Java all support functional programming but do not enforce it.
Does functional programming mean no classes at all?
Not necessarily. FP is about how you think about data and transformation, not about banning classes from the file. In JavaScript you can write perfectly functional code using only functions and plain objects. In Scala you use case classes heavily alongside FP. The distinction is whether you mutate state in place or return new values.