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 returns5, 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,reduceare 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.
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
| Situation | Better fit |
|---|---|
| Modelling real-world entities (User, Order, Car) | OOP |
| Data pipelines and transformations | FP |
| Concurrent or parallel code | FP |
| UI components with lifecycle management | OOP |
| Event processing, stream handling | FP |
| Large system with many collaborating actors | OOP (with care) |
| ETL, analytics, batch jobs | FP |
| GUI frameworks, game entities | OOP |
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.
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,reduceinstead offorloops 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
- Functional programming vs object-oriented programming (OOP) — CircleCI
- Functional Programming Paradigm — GeeksforGeeks
- What is Functional Programming? Explained in Python, JS, and Java — Educative
- Functional programming vs OOP: comparing paradigms — Imaginary Cloud
- Functional Programming vs Object-Oriented Programming in Data Analysis — DataCamp
- Harnessing the Power of OOP and FP Paradigms in Software Development — DEV Community
- Top 5 Functional Programming Languages — Coursera
- Combining Object-Oriented and Functional Programming in Large Projects — DEV Community