Why Use Functional Programming When OOP Exists?

Why Use Functional Programming When OOP Exists?

OOP is 50+ years old. Classes, objects, inheritance — it works, everyone knows it, almost every popular language supports it. So why are people talking about functional programming like it’s some revelation? Because OOP is great at modelling things. FP is great at modelling transformations. Most real software has both, and conflating the two is where the confusion starts.

What Is Functional Programming, Actually?

Not “functions inside a class.” That’s just OOP with functions.

Functional programming is a paradigm where you build software by composing pure functions — functions that, given the same input, always return the same output and never touch anything outside themselves [1].

Three ideas define it:

  • Pure functions — no side effects, no hidden state changes. add(2, 3) always returns 5, no matter what.
  • Immutability — once data is created, it is not modified. New data is created instead [2].
  • Higher-order functions — functions that take other functions as arguments or return them. map, filter, reduce are the textbook examples [3].

The rest — monads, functors, currying — are built on top of these three. You don’t need to understand all of them to benefit from FP. Most developers never touch monads and still write clean functional code every day.

OOP Does Not Fail. It Just Has Blind Spots.

Let me be direct: OOP is not bad. I use it every day. But there are specific scenarios where it actively makes your life harder.

Shared mutable state is the enemy

In OOP, objects carry state. That UserService instance has a currentUser field. Your CartManager has an items array. Multiple parts of the app read and write these. And then at some point a bug appears that you cannot reproduce reliably — because the state was mutated from two places at the same time [4].

This is not a hypothetical. It’s the everyday reality of large OOP codebases. The shared state is hard to track, and modifying one object from two different places is a recipe for race conditions in any concurrent or async code. FP’s immutability removes this entire class of bug — you pass data in, you get data out, nothing gets mutated behind your back [5].

Testing OOP code is more work than it should be

To test a method that depends on this.db, this.cache, and this.config, you need mocks. Three mocks minimum for one test. And when the class has 20 methods and 8 dependencies, your test setup ends up longer than the actual test.

A pure function needs none of that. Pass the input, check the output. That is the whole test [1].

Where Each Paradigm Actually Shines

SituationBetter fit
Modelling real-world entities (User, Order, Car)OOP
Data pipelines and transformationsFP
Concurrent or parallel codeFP
UI components with lifecycle managementOOP
Event processing, stream handlingFP
Large system with many collaborating actorsOOP (with care)
ETL, analytics, batch jobsFP
GUI frameworks, game entitiesOOP

This is not a “FP wins” table. OOP is the right call in plenty of situations [6].

Yes, You Can Use Both — and Most Codebases Already Do

JavaScript, Python, Scala, Kotlin, Java (since Java 8), even C# — all of these are multi-paradigm languages [7]. They do not force you to pick one side.

Look at a typical JavaScript codebase:

// OOP for structure
class UserRepository {
  constructor(db) {
    this.db = db;
  }

  async findById(id) {
    return this.db.query(`SELECT * FROM users WHERE id = ?`, [id]);
  }
}

// FP for transformation
const formatUsers = (users) =>
  users
    .filter(u => u.isActive)
    .map(u => ({ id: u.id, name: u.name.trim().toLowerCase() }));

UserRepository is classic OOP. formatUsers is pure FP — no side effects, same input always gives the same output. Both live in the same file, same project, no conflict [8].

This is not mixing paradigms out of confusion. It is using the right tool for each job.

fp vs oop paradigm split

How to Actually Mix Them Without Making a Mess

The rule I follow: use OOP to define your structure and boundaries, use FP for the logic inside those boundaries.

In practice:

  • Classes for services, repositories, and components that need lifecycle management.
  • Pure functions for business logic, data transformations, and validation rules.
  • Avoid putting heavy transformation logic inside class methods — extract it to pure functions that can be tested independently.
  • Reach for map, filter, reduce instead of for loops that mutate a variable in place.

Python example:

# OOP for the service boundary
class OrderService:
    def __init__(self, db):
        self.db = db

    def get_pending_orders(self, user_id):
        rows = self.db.fetch(user_id)
        return process_orders(rows)  # delegate to a pure function


# FP for the logic — trivial to test in isolation
def process_orders(orders):
    return [
        {**o, "total": o["price"] * o["quantity"]}
        for o in orders
        if o["status"] == "pending"
    ]

process_orders has zero dependencies on the database, the service, or anything external. Testing it is one function call, no mocks needed [8].

The Languages That Already Made This Call

Scala was literally designed for this — every value is an object and a function at the same time [7]. React’s shift from class components to hooks was, functionally speaking, a move toward FP for UI logic. Redux is pure FP inside a JavaScript app. Java added Stream, Optional, lambdas, and Function<T,R> in Java 8 specifically to bring FP patterns into an OOP language [3].

The industry already voted. Multi-paradigm is the default now, not the exception.

One Thing to Watch Out For

Mixing paradigms is fine. Mixing them without a clear convention is how you end up with code nobody can follow. I have seen codebases where some files are fully OOP, some are fully functional, and some are both for no apparent reason. The team spends more time decoding the style than reading the logic.

Pick a convention: OOP for the shell, FP for the fill. Write it down once in your team’s guidelines. That is enough.

End

Sources

  1. Functional programming vs object-oriented programming (OOP) — CircleCI
  2. Functional Programming Paradigm — GeeksforGeeks
  3. What is Functional Programming? Explained in Python, JS, and Java — Educative
  4. Functional programming vs OOP: comparing paradigms — Imaginary Cloud
  5. Functional Programming vs Object-Oriented Programming in Data Analysis — DataCamp
  6. Harnessing the Power of OOP and FP Paradigms in Software Development — DEV Community
  7. Top 5 Functional Programming Languages — Coursera
  8. Combining Object-Oriented and Functional Programming in Large Projects — DEV Community