A programming paradigm is a way of structuring a program around a small set of organising principles: in OOP, objects and messages; in FP, pure functions and immutable data; in concurrent programming, multiple flows of control that happen at once. No single paradigm dominates; each is a lens for a different class of problem, and almost every modern language is multi-paradigm — the engineer’s choice is which paradigm to lean on for which piece.
This topic gives you the vocabulary to make the choice deliberately.
Three Paradigms Through Three Questions
Each paradigm answers a different organising question:
| Paradigm | Question it organises the answer to |
|---|---|
| Object-Oriented | “Who is responsible for which piece of state, and what is the protocol for asking them to act?” |
| Functional | “How do we compute new values from old values without modifying anything?” |
| Concurrent | “How do multiple independent flows of control coordinate without corrupting shared state?” |
The three are not exclusive. A modern backend program has all three: a domain model in OOP shape (entities with encapsulated state and well-defined operations), the data-transformation pipeline in FP shape (stateless parallel map/filter/reduce), and the request handling in concurrent shape (threads or coroutines coordinating to serve many users).
Object-Oriented Programming
The three principles, restated in a way that actually distinguishes good code from cargo-cult:
| Principle | What it actually means | Common corruption |
|---|---|---|
| Encapsulation | The object’s internal state is non-visible to callers; they speak to it via method calls | “Getter/setter for every field” — this is a struct in disguise, not encapsulation |
| Inheritance | Substitutability: a subclass instance can stand in for a parent instance anywhere | Hierarchies 6 levels deep where descendants violate parent contracts; “code reuse” masquerading as subtyping |
| Polymorphism | The same call name resolves to different implementations based on the receiver | if (type == X) ladders everywhere — the polymorphism is in the type-check, not the architecture |
The essence of OOP is decision deferral — when a caller invokes shape.area(), the caller does not know which shape will be in the variable at runtime; the language defers the decision to dispatch. Where to use OOP well: domain models with clear responsibilities and operations that operate on state that wants to be hidden. Where OOP hurts: data pipelines where there’s no state to hide and no behaviour to defer.
Functional Programming
FP organises computation around pure functions — functions whose output depends only on their input and which have no side effects. The two consequences:
- Immutability — data is not modified; new data is returned.
users.filter(active)returns a new list, not a mutated one. - Referential transparency — a function call can be replaced by its return value without changing program behaviour. Optimisation (memoisation, parallel evaluation, dead-code elimination) becomes a compiler concern.
| FP shape | Practical use |
|---|---|
map, filter, reduce | Data transformation pipelines — the foundation of modern data engineering |
| Higher-order functions (functions taking or returning functions) | Strategy abstraction (see craft-design-patterns); effect systems in FP languages |
| Lazy evaluation | Process streams of any size without buffering; only evaluate what’s needed |
| Algebraic data types + pattern matching | Closed enumerations of shapes; exhaustiveness-checked dispatch (Rust enums, Scala enums) |
Where FP wins: data transformation pipelines, parallel pipelines (no shared state, no races), compiler passes, configuration-as-data. Where FP hurts: problems where the whole point is shared mutable state (a database engine, an interactive UI’s session state) — FP forces you to model state explicitly, which is more work than Java’s class members but easier to reason about.
The discipline to internalise: prefer immutability, default to immutability, accept mutation only where profiling shows it cost. This is roughly how Rust, modern C++, and modern JavaScript disciplinaires work today.
Concurrent Programming
Concurrent programming organises multiple flows of control that execute simultaneously. The central problem — and the choice that splits the paradigm — is how those flows share state.
| Approach | Mechanism | Languages | Trade-off |
|---|---|---|---|
| Shared-state concurrency | Multiple threads + locks / atomics / mutexes | C, C++, Java (traditional), Rust | Fast, but the engineer proves correctness: deadlock, race, hazard all must be coded around |
| Message-passing | Independent actors with mailboxes; messages, not shared references | Erlang, Akka (JVM), Go’s goroutines+channels | Easier to reason about (no shared pointers), but the programmer must think in protocols |
| Software transactional memory (STM) | Memory is transactional — like a database, with committed reads/writes | Clojure, Haskell | Composable and deadlock-free; runtime overhead, with contention management |
| Async/await + cooperative scheduling | Coroutines yield control at await points; one thread per (logical) core | JavaScript (Node, browser), Python (asyncio), Rust (async), C# | Single-threaded by default, no shared-state races; flow-control inversion traps await |
The choice is rarely which one; each language’s run-time picks one or two and commits. The engineer’s job is understanding which the runtime chose, and writing the code in that style.
| Runtime | Default concurrency model |
|---|---|
| JVM (Java) | Shared state, locks. synchronized for legacy code; Lock / ConcurrentHashMap for new. |
| Go | Goroutines + channels (message-passing). |
| Erlang / Elixir | Processes with mailboxes; “let it crash” supervision trees. |
| Node.js | Single-threaded event loop; async/await cooperatively. |
| Rust | Shared-state with the borrow-checker preventing data races at compile time; async/await is cooperative. |
Three universal truths apply regardless of model:
- Shared mutable state is the root of concurrency bugs. All three other models eliminate some class of it.
- Backpressure is the engineering problem in every concurrency model. A queue grows too fast → memory blowup; a channel buffers → backpressure up the chain.
- The fine-grained test is in the code under load, not in clean architecture review. Patterns that look correct can deadlock under specific scheduling.
Multi-Paradigm as the Engineering Reality
No production codebase is “pure” anything. The practical engineer reaches for the paradigm that fits the problem:
| Subsystem | Likely dominant paradigm | Why |
|---|---|---|
| Domain entities (bank account, order, user) | OOP | Encapsulated state with well-defined operations; polymorphic dispatch |
| ETL / data pipeline | FP | Pure transformations over immutable streams |
| Request handling + worker pool | Concurrent | Multiple flows coordinating |
| Iterators, callbacks, configuration injection | Higher-order functions | Strategy abstraction without ceremony |
| Capability-sensitive code (parser, cmd dispatch) | Algebraic data types + pattern matching | Closed enumeration + exhaustive dispatch |
The rule of thumb — borrow from each where it makes the next 6 months easier, not where it makes the next 6 weeks fashionable.
Practice Trajectory
- Pick a piece of your own code that feels heavy. Identify whether it is fighting because it’s OOP-shaped code forced to be FP-shaped, or the opposite. The feeling of friction is the diagnosis.
- Take a single function and reimplement it three ways: OOP (with a class), FP (with a higher-order map), and “pure, no abstraction”. Compare readability on a six-month reread.
- Identify one piece of shared mutable state in a system you work on. Apply message-passing: convert the state to an actor with a mailbox. Note what became easier; note what became heavier.
- Take a long class with 6+ methods on the same instance state. Refactor to a module of pure functions that take the state as input and return the mutated state. Profile the difference in testability.
- Audit one async/await codebase for the absence of structured concurrency — orphaned tasks that escape their parents. Most async codebases leak here.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Domain entities with encapsulated state that the team changes together | OOP with single-responsibility classes |
| Data transformation, parsing, compilation passes | FP — pure pipelines over immutable data |
| Multi-user server with shared state | Concurrent — explicit concurrency model (shared state + locks, or message-passing) |
| Closed set of cases (`success | error |
| “We must pick one paradigm for the codebase” | No — a multi-paradigm language (Rust, Scala, modern Python, modern JavaScript) supports all three; the engineer’s discipline is picking per-subsystem |