When to Use Which
Lesson 8: When to Use Which
Rule of thumb up front: if the problem is mostly about transforming data, think functionally. If it's mostly about entities with identity and behavior, think OOP. If it's both β and it usually is β use a functional core with an imperative shell.
Reach for FP whenβ¦
- Data pipelines: ETL, parsing, streaming and processing events, API response shaping.
- Business logic that must be bulletproof-testable: finance, healthcare, rules engines, pricing. Pure functions make correctness provable and audits easy.
- Concurrent or parallel systems: servers, batch jobs, message processors. Immutability eliminates entire classes of race conditions.
- State machines and reducers: UI state, workflow engines, undo/redo systems.
- Pure logic libraries: anything other code will call and you want to be impossible to misuse.
Reach for OOP whenβ¦
- Domain entities with identity and lifecycle: Customer, Order, GameCharacter β things that persist and change over time.
- UI frameworks and widget systems: components are stateful objects that react to events.
- Plugin/extension architectures: frameworks that call your code through interfaces (inversion of control).
- The ecosystem dictates it: Java/Spring, C#/.NET, C++ game engines β fighting the house style costs more than it saves.
The Functional Core, Imperative Shell
The most practical pattern in modern software β and the answer to "both, but how?" Keep the heart of your app pure: all decisions, calculations, and transformations as pure functions of explicit state. Wrap it in a thin shell that does the dirty I/O: read input, call the pure core, write output.
// IMPURE shell β talks to the world
function handleCheckout(request) {
const cart = loadCart(request.userId); // I/O in
const result = checkout(cart, request.promo); // pure core
saveReceipt(result); // I/O out
return result;
}
// PURE core β no I/O, trivially testable
function checkout(cart, promo) {
const withDiscount = applyPromo(cart, promo);
const total = computeTotal(withDiscount);
return { items: withDiscount, total };
}
Your database, network, and UI all live in the shell. Your rules, math, and decisions live in the core β and the core is where bugs actually happen, so that's where you want pure testability.
Ecosystem Quick Reference
| Language | Default style | Notes |
|---|---|---|
| JavaScript / TypeScript | Hybrid | Both are idiomatic β choose per module |
| Python | Hybrid (OOP-leaning) | Classes everywhere, but map/filter/reduce and dataclasses support FP |
| Java | OOP-first | Records and streams add FP flavor |
| Kotlin | Hybrid | Data classes + functional collection extensions |
| C# | Hybrid | LINQ is functional programming |
| Rust | Hybrid | Traits + iterators; ownership enforces discipline |
| Haskell / Elixir / Clojure | Functional | No real OOP β purity or immutability by default |
| C++ | Multi-paradigm | Everything available, everything dangerous |
Final Advice
- Learn both. The second paradigm makes you dramatically better at the first β you start seeing OOP's hidden state and FP's unreadable abstractions with fresh eyes.
- Let the problem decide. Don't fight your ecosystem, and don't force a paradigm onto a problem that fits the other.
- Consistency beats purity. A codebase that's uniformly one style is easier to maintain than one that's "pure" but incoherent.
- Default to the functional core pattern for new logic-heavy features β it's the lowest-risk way to get most of FP's benefits in any codebase.
π§ Knowledge Check
1. Which of these problems is the best fit for a functional approach?
2. The "functional core, imperative shell" pattern means:
3. Why does "consistency beats purity" matter in real codebases?