Back to the map

Module 22 · OOP in practice · Lesson 4 of 4

OOP vs functional styles

Two ways to organise programs: objects that bundle state with behaviour, and pure functions over immutable data. The expression problem that shows what each makes easy, where each shines, and the hybrid most modern codebases use.

DeepNice-to-knowSenior 13 min Updated Nov 2026

Why it matters

Modern languages are multi-paradigm: Java has records, sealed types, lambdas and streams; Kotlin, C#, Scala, TypeScript, Python and Rust mix objects and functions freely. That means the choice between an object-oriented and a functional design is made constantly, often implicitly, inside one codebase. Knowing what each style makes easy and hard turns that choice into a decision rather than a habit, and it is a frequent senior-level discussion in design interviews.

The idea

The object-oriented style organises a program around objects that bundle state with the behaviour that changes it; variation is expressed by subtypes overriding methods. The functional style organises it around pure functions that transform immutable data; variation is expressed by data types and functions that handle each case.

Object-orientedFunctional
Unit of designObject: state plus methodsFunction over data
StateEncapsulated and mutated in placeImmutable; new values are returned
VariationSubclasses and dynamic dispatchData variants and pattern matching
ReuseInheritance and composition of objectsComposition of functions; higher-order functions
Side effectsAnywhere, guarded by encapsulationPushed to the edges; core is pure
StrengthModelling entities with identity and lifecycleTransformations, pipelines, concurrency, testing

The expression problem

Picture a grid: rows are data variants (Circle, Square, Triangle), columns are operations (area, draw, serialise). Every program fills that grid. The two styles fill it in different directions.

areadrawserialiseCircleSquareTriangleclass Circle: area · draw · serialiseclass Square: area · draw · serialiseclass Triangle: area · draw · serialiseOOP: new shape = one class; new operation = edit every classFP: new operation = one function; new shape = edit every function
OOP groups the grid by row: each class implements every operation, so a new variant is one new class. Functional code groups it by column: each function handles every variant, so a new operation is one new function.

Neither is better in general; the right choice depends on which axis grows. A plugin system where third parties add new kinds of things wants the object-oriented direction. A compiler or a rules engine where the set of node types is fixed but new analyses keep appearing wants the functional direction. Modern languages make the functional direction safe with sealed types and exhaustive pattern matching: add a variant and the compiler lists every function that must handle it.

sealed interface Shape
data class Circle(val r: Double) : Shape
data class Square(val side: Double) : Shape

fun area(s: Shape): Double = when (s) {   // compiler checks every case is covered
    is Circle -> Math.PI * s.r * s.r
    is Square -> s.side * s.side
}

What functional habits buy, even in OOP code

Pure functions (output depends only on input, no side effects) are trivial to test, cache and run in parallel. Immutable data can be shared across threads without locks and never changes behind your back. Higher-order functions (map, filter, reduce, passing behaviour as an argument) replace many small strategy classes. These habits do not require abandoning objects; they make objects better.

The common hybrid: functional core, imperative shell

Many well-designed systems put business rules in a pure core of immutable value objects and functions (calculate a price, decide whether an order can ship, apply a state transition) and wrap it in a thin imperative shell of objects that do I/O: read from the database, call services, write results. The core is fast to test with no mocks; the shell is small enough to test with a few integration tests. Domain entities with identity and lifecycle (an Order) remain objects, while their calculations become pure functions or methods on immutable values.

Trade-offs

Object-oriented design models stateful entities and extensible families of types naturally, and makes adding operations across those types expensive. Functional design makes transformations, concurrency and testing simple and adding new variants expensive without sealed types. Pure-functional purity in a language not built for it fights libraries and the team; heavy object hierarchies fight change. Choose per part of the system by asking which axis will grow and where the side effects belong.

In an interview

Show the reasoning, not the allegiance: "Pricing rules are a pure function from cart and promotions to a price breakdown, with immutable inputs, so they are trivially unit-tested and safe to run concurrently. Orders are objects with identity and a lifecycle. Payment providers are an interface because new providers will keep arriving, which is the axis that grows."

Check yourself

A team builds a document analyser. The set of node types (heading, paragraph, table, image) is fixed by a spec, but product keeps asking for new analyses: word counts, accessibility checks, link validation. Which organisation fits, and why?

OOP vs functional styles · Backplane