TL;DR;
Object Oriented Programming (OOP) and functional programming (FP) aren’t enemies, but just different tools in your belt. Languages like C#, Java, and JavaScript now encourage a mix of object-oriented and functional features, though developers may not realize it. Whatever you use to structure your code, classes, modules, or functions, keeping logic clean is where functional programming excels. Use what makes sense, bend the rules when you need to.
Written by Cristian M. Zaragoza Gómez, Senior Software Engineer at Zartis.
Introduction – Why Are You Reading This?
If you’re a developer, most likely, you’ve already formed an opinion (maybe even a strong one) about programming paradigms. Whether you’re loyal to classes and inheritance or you swear by pure functions and immutability, the internet is full of “pure” developers claiming about what’s “right”. But the thing is, modern development hasn’t to be one thing or the other.
I remember years ago (time flies), I was working on a REST API in .NET, structuring everything with clean service layers and repositories. I was certain I was doing textbook OOP, until I realized half of my logic was just LINQ chains and stateless helpers. That’s when it hit me: we’re all writing hybrid code.
The “Well-Known” Imperative Styles
Procedural and OOP often get grouped under the same umbrella: imperative programming. Both focus on defining the steps needed to solve a problem, but they approach structure and organization in very different ways.
Procedural programming is all about writing sequences of instructions, just plain logic and functions operating on data. It’s the classic top-down approach and works well for small to mid-sized programs where structure is simple and straightforward.
Here’s a simple procedural example in C#:
string Greet(string name)
{
return $"Hello, {name}!";
}
var greeting = Greet("Carlos");
Console.WriteLine(greeting);
Procedural code may look simple, but it’s still widely used in scripting, embedded systems, and older business systems where straightforward step-by-step logic makes sense.
Object-oriented programming, on the other hand, organizes code around objects, bundling data and behavior together into reusable components. This makes it easier to model complex systems, especially when you need to manage state or represent real-world entities.
Key OOP concepts:
- Encapsulation: keeping data and behavior bundled together
- Inheritance: reusing and extending code
- Polymorphism: making code behave differently depending on context
- Abstraction: hiding implementation details behind a clear interface
We won’t go deep into each of these here, but they are helpful for understanding how OOP organizes and abstracts complexity, especially when we start comparing it with functional approaches later on.
Let’s take that same greeting sample, but written in OOP style:
public class User
{
public string Name { get; set; }
public void Greet()
{
Console.WriteLine($"Hello, {Name}!");
}
}
OOP excels in large, stateful applications. It gives you a mental model that scales, but its popularity in enterprise software is also tied to broader historical and ecosystem factors, like Java’s dominance in the early 2000s and the rise of the .NET Framework shortly after.
The Not-So-Well-Known Functional Programming – “Have We Met Before?”
Functional programming might feel more academic or niche, but very most likely you’ve used it already, maybe without even realizing it.
At its core, functional programming (FP) is about describing what to do with data, not how to do it step by step. You build programs by composing small, reusable, stateless functions that avoid side effects and do not mutate shared state. This mindset often leads to more predictable, testable, and parallel-friendly code.
Key ideas in FP:
- Pure functions: return the same output for the same input, without side effects
- Immutability: data is never modified, only transformed
- Higher-order functions: functions that take or return other functions
- Function composition: building more complex logic by chaining simple functions
Even if you have never worked with functional-first languages like Haskell or F#, you have probably used functional techniques. Languages like JavaScript, Python, C#, Kotlin, and Java all include features such as map, filter, reduce, lambdas, and closures.
Example 1 (JavaScript):
const greet = name => `Hello, ${name}!`;
console.log(greet("Carlos")); // Hello, Carlos!
Even languages known for being OOP-heavy like C# or Java now has functional features out-of-the-box.
LINQ in C# is probably the first introduction to functional thinking for many developers. Once you get used to filtering and mapping over data instead of mutating it, it’s hard to go back.
Hera another sample slightly more expressive using composition :
Example 2 (JavaScript):
const users = [
{ name: "Silvia", age: 2 },
{ name: "Greta", age: 38 },
{ name: "Carlos", age: 4 }
];
const getAdultNames = users =>
users
.filter(user => user.age >= 18)
.map(user => user.name);
console.log(getAdultNames(users)); // ["Greta"]
Stronger Together – Where the Lines Blur
You don’t always have to pick a side. And while many developers still work with OOP, a lot of modern languages and frameworks are evolving toward functional-style features, often treating OOP as a legacy model or fallback. You can see this shift in ecosystems like JavaScript, Kotlin, and even in recent versions of C#, where things like lambdas, immutability, and declarative data transformation are now first-class citizens.
Most modern languages blur the lines between paradigms. For example, C# and Java both support functional constructs like lambdas, records, and pattern matching. These features let you write clean, expressive logic without giving up the structure that classes and interfaces provide.
F# is a great example of a language designed with functional principles at its core. It emphasizes immutability, expression-based syntax, and composition, while still allowing you to use OOP when needed. Since it runs on .NET, it fits naturally into enterprise environments that might already use C# or other Microsoft technologies.
That said, functional programming is not without trade-offs. Concepts like immutability and recursion can introduce performance challenges, particularly in memory usage or allocation. Procedural and object-oriented approaches often allow more direct control over state and memory, which can be a better fit in performance-critical systems. Languages like Haskell can optimize away many of these concerns through advanced compilation techniques, but in more mainstream platforms, you may still need to weigh the impact. In high-performance systems, OOP or procedural code that relies on in-place mutation can still be the more efficient option.
For example, imagine a high-throughput data processing system that handles thousands of large records per second. In a purely functional style, each transformation creates a new copy of the data instead of modifying it in place. This can lead to increased memory usage and pressure on the garbage collector, especially if the data structures are large or deeply nested. In these cases, a more procedural or object-oriented approach with controlled mutation might be more efficient and predictable in terms of performance.
At the end of the day, we are all writing hybrid code. Whether you are organizing logic through objects, modules, or pure functions, you will likely end up mixing paradigms depending on your language, your team, and the problem you are solving.
So, When To Use Which?
There isn’t a silver bullet for all situations. But here’s a few quick guidelines:
Use OOP When:
- You want to encapsulate data and behavior into reusable, stateful objects
- You prefer encapsulating state within objects instead of managing it through external stores or data transformations.
- You’re working in a domain where object-oriented modeling still helps organize behavior and logic, such as traditional enterprise systems.
Use Functional Programming When:
- You’re transforming data or working with streams, and prefer functional-style composition over class-based abstractions.
- You want better testability or predictability.
- You’re writing concurrent or parallel code and want to reduce complexity by avoiding shared state and side effects.
Most of the time, you’ll use a little bit of both. I used to think I had to “choose a side”, but these days I’m more interested in writing code that makes sense, even if that means breaking a few “purist” rules along the way.
That said, there’s another important factor to consider: what does your language and community lean toward? If you’re working in a language that’s adding more functional features, like C# or Kotlin, it often makes sense to follow that direction. Languages and frameworks tend to evolve with certain patterns in mind, and working with those patterns usually leads to more readable, maintainable code. And if that direction really doesn’t fit the way you like to work, it might be worth considering a language that does.
A Real-World Example
Let’s say you’re building a service layer in a C# app.
You might structure things with interfaces and dependency injection (classic OOP). But inside your methods, maybe you’re chaining LINQ operations to filter users and project some data.
Example (C#):
public IEnumerable<string> GetAdultUserNames(List<User> users)
{
return users
.Where(u => u.Age >= 18)
.Select(u => u.Name);
}
OOP gives you structure. Functional gives you elegant, focused logic, and together you get readable and maintainable code. In some languages, like C# or Kotlin, mixing paradigms like this is almost expected. In others, like Haskell or Rust, you may need to lean more heavily into a particular style that the language is designed to support.
What About F#?
If you’re curious about functional programming on .NET, F# is a fantastic place to explore. It’s a functional-first language that promotes immutability, type safety, and concise expression-based syntax, but it doesn’t force you into a corner.
You can write clean, composable logic with pipelines and pattern matching, while still dipping into OOP if your use case needs it. F# is especially popular for domain modeling, data pipelines, financial systems, and anywhere you care about correctness and clarity.
Here’s a quick example of a data transformation in F#:
let getAdultUserNames users =
users
|> List.filter (fun u -> u.Age >= 18)
|> List.map (fun u -> u.Name)
It’s expressive, readable, and easy to test.
If you’re a C# developer looking to dip your toes into functional thinking, F# might be a surprisingly accessible next step.
Conclusion
The debate between procedural (OOP) and functional programming is kind of like arguing whether a hammer is better than a screwdriver. It depends what you’re building.
Modern programming isn’t about picking sides, it’s about understanding the strengths of each paradigm and knowing when to reach for them. The more fluent you are in both, the more versatile and effective you’ll be.
That said, this post focused on the practical, high-level trade-offs that most developers run into in their day-to-day work. There is a lot more depth to explore, including the differences between pure and impure functional languages, how performance and memory usage can vary depending on the paradigm, and how procedural styles are still at the core of many real-world systems.
Languages like F#, Scala, Kotlin, and even modern JavaScript allow you to combine different paradigms, each with its own philosophy and trade-offs. If this sparked your interest, it is worth digging deeper into how different languages and runtimes handle purity, side effects, and resource management.
So next time someone asks “Are you an OOP or FP dev?”, feel free to say: “Yes.” Paradigms are tools not F.C. Barcelona vs Real Madrid hooligans.
—
If you’d like to connect with Cristian to learn more about this topic or have further questions, feel free to start a conversation!
Author:
Cristian M. Zaragoza Gómez is a full-stack developer with a strong focus on backend development. Known for his persistence and analytical mindset, he thrives on solving complex problems and building robust, scalable systems. Whether optimising performance or architecting new features, Cristian brings a pragmatic approach to every challenge.