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.

Your problem Mostly transforming data or pure computation? YES GO FUNCTIONAL pipelines Β· reducers NO Entities with identity, lifecycle, or invariants? YES GO OBJECT- ORIENTED NO FUNCTIONAL CORE + 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

LanguageDefault styleNotes
JavaScript / TypeScriptHybridBoth are idiomatic β€” choose per module
PythonHybrid (OOP-leaning)Classes everywhere, but map/filter/reduce and dataclasses support FP
JavaOOP-firstRecords and streams add FP flavor
KotlinHybridData classes + functional collection extensions
C#HybridLINQ is functional programming
RustHybridTraits + iterators; ownership enforces discipline
Haskell / Elixir / ClojureFunctionalNo real OOP β€” purity or immutability by default
C++Multi-paradigmEverything available, everything dangerous

Final Advice

  1. 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.
  2. Let the problem decide. Don't fight your ecosystem, and don't force a paradigm onto a problem that fits the other.
  3. Consistency beats purity. A codebase that's uniformly one style is easier to maintain than one that's "pure" but incoherent.
  4. 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?

Further Reading