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.
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-oriented | Functional | |
|---|---|---|
| Unit of design | Object: state plus methods | Function over data |
| State | Encapsulated and mutated in place | Immutable; new values are returned |
| Variation | Subclasses and dynamic dispatch | Data variants and pattern matching |
| Reuse | Inheritance and composition of objects | Composition of functions; higher-order functions |
| Side effects | Anywhere, guarded by encapsulation | Pushed to the edges; core is pure |
| Strength | Modelling entities with identity and lifecycle | Transformations, 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.
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?