Clarity in Architecture: Not OOP vs FP

Clarity in Architecture: Not OOP vs FP As a polyglot full stack engineer who's worked across both frontend and backend — in Java, Kotlin, Python, Go, JavaScript, TypeScript, Ruby, Lua, and PHP — I’ve worked through both fully object-oriented and fully functional phases in my career — and I’ve found that architecture debates often miss the real problem. The tension isn’t between paradigms. It’s between clarity and ambiguity. Too often, discussions get stuck in ideological traps — OOP means classes, DI is bad, FP isn’t practical, SOLID is sacred — or on the other end of the spectrum, some advocate for having no structure at all. These positions end up dominating the conversation instead of leading to a productive exchange. The best systems I built — or helped evolve — weren’t purely object-oriented or strictly functional. They were clear. Their boundaries made sense. And once those boundaries were in place, it became much easier to reason about the system, understand how everything fit together, and extend it without fear of unexpected consequences. This first article kicks off a series that doesn’t argue for a paradigm — it argues for architectural clarity. It’s about how we reason through systems, how we approach problems, and how we make structural decisions that help code grow cleanly. I’m not offering a universal formula — just sharing patterns and principles that have consistently helped me evolve real systems with less friction and more confidence. The Real Question: Where Does This Go? Forget OOP vs FP — or any single paradigm. What I kept running into was the difficulty of seeing the big picture when small decisions piled up. The real challenge is simplifying the system enough to reason about it clearly — and that often starts with asking: Where does this code belong? A factory function? A class? A context object? A singleton? A reducer? When we make the wrong call, it spreads ambiguity: unclear ownership, unpredictable effects, and brittle testability. When we get it right, the system flows. So instead of taking sides in OOP vs FP, this series focuses on practical boundaries: When does a class help? When is internal state okay? What belongs in dependency injection — and what doesn’t? How do you keep functional principles in an effectful system? Let’s reframe the discussion. Classes for Behavior The word “class” triggers a lot of baggage — inheritance, over-engineering, Java boilerplate. But at its core, a class (or in Go, a struct paired with interfaces) is just a way to encapsulate behavior with shared internal logic. I emphasize composition over inheritance — the value of classes comes not from hierarchies, but from the ability to clearly group related behavior. Use a class when: You have multiple methods that coordinate shared state or logic You need a clear lifecycle or ownership boundary You want to separate dependency construction from behavior Avoid it when: You’re just grouping utilities or data There’s no internal cohesion You only need a couple of flat methods You have persistent data that belongs in a database, API, or external system Classes make it easy to separate the act of wiring dependencies (in the constructor) from the internal business logic (in the methods). Personally, I avoid storing long-lived data in classes. Instead, I treat them as coordinators — not sources of truth. When I need to persist data like user info or logs, I reach for a proper database, API, or storage layer. I’m more interested in how classes compose than how they inherit — and I often pair them with interfaces to keep things flexible. We’ll unpack that further in the next part of the series. Internal State: Not Evil, Just Misplaced Functional programming taught us to fear state. But the truth is, state is fine — when it’s owned. The key is cohesion. State should live in objects that: Have a clear lifecycle Own their behavior Don’t leak into the rest of the system Are managing ephemeral data, not persistent records In game development, for example, many objects manage both internal state and behavior tied to a well-defined lifecycle — a natural fit for class-based encapsulation. Ephemeral or short-lived data belongs inside classes that directly control that lifecycle. Long-lived or persistent data should reside in a database, cache, or external API. Classes can read and write that data, but shouldn’t try to own or retain it. Contextual values — like session data, request scope, or frontend state — are also better constructed where they’re needed. They’re often tied to a specific user, time, or flow, and don’t belong in global DI containers. Treating them as contextual inputs keeps the boundaries clean and avoids accidental state leaks. We’ll dive into practical examples of these patterns in the next part of the series. Dependency Injection Should Be Explicit Some might argue that global singletons are fine for th

Apr 9, 2025 - 18:43
 0
Clarity in Architecture: Not OOP vs FP

Clarity in Architecture: Not OOP vs FP

As a polyglot full stack engineer who's worked across both frontend and backend — in Java, Kotlin, Python, Go, JavaScript, TypeScript, Ruby, Lua, and PHP — I’ve worked through both fully object-oriented and fully functional phases in my career — and I’ve found that architecture debates often miss the real problem. The tension isn’t between paradigms. It’s between clarity and ambiguity. Too often, discussions get stuck in ideological traps — OOP means classes, DI is bad, FP isn’t practical, SOLID is sacred — or on the other end of the spectrum, some advocate for having no structure at all. These positions end up dominating the conversation instead of leading to a productive exchange.

The best systems I built — or helped evolve — weren’t purely object-oriented or strictly functional. They were clear. Their boundaries made sense. And once those boundaries were in place, it became much easier to reason about the system, understand how everything fit together, and extend it without fear of unexpected consequences.

This first article kicks off a series that doesn’t argue for a paradigm — it argues for architectural clarity. It’s about how we reason through systems, how we approach problems, and how we make structural decisions that help code grow cleanly. I’m not offering a universal formula — just sharing patterns and principles that have consistently helped me evolve real systems with less friction and more confidence.

The Real Question: Where Does This Go?

Forget OOP vs FP — or any single paradigm. What I kept running into was the difficulty of seeing the big picture when small decisions piled up. The real challenge is simplifying the system enough to reason about it clearly — and that often starts with asking:

Where does this code belong?

A factory function? A class? A context object? A singleton? A reducer?

When we make the wrong call, it spreads ambiguity: unclear ownership, unpredictable effects, and brittle testability. When we get it right, the system flows.

So instead of taking sides in OOP vs FP, this series focuses on practical boundaries:

  • When does a class help?
  • When is internal state okay?
  • What belongs in dependency injection — and what doesn’t?
  • How do you keep functional principles in an effectful system?

Let’s reframe the discussion.

Classes for Behavior

The word “class” triggers a lot of baggage — inheritance, over-engineering, Java boilerplate. But at its core, a class (or in Go, a struct paired with interfaces) is just a way to encapsulate behavior with shared internal logic. I emphasize composition over inheritance — the value of classes comes not from hierarchies, but from the ability to clearly group related behavior.

Use a class when:

  • You have multiple methods that coordinate shared state or logic
  • You need a clear lifecycle or ownership boundary
  • You want to separate dependency construction from behavior

Avoid it when:

  • You’re just grouping utilities or data
  • There’s no internal cohesion
  • You only need a couple of flat methods
  • You have persistent data that belongs in a database, API, or external system

Classes make it easy to separate the act of wiring dependencies (in the constructor) from the internal business logic (in the methods).

Personally, I avoid storing long-lived data in classes. Instead, I treat them as coordinators — not sources of truth. When I need to persist data like user info or logs, I reach for a proper database, API, or storage layer.

I’m more interested in how classes compose than how they inherit — and I often pair them with interfaces to keep things flexible. We’ll unpack that further in the next part of the series.

Internal State: Not Evil, Just Misplaced

Functional programming taught us to fear state. But the truth is, state is finewhen it’s owned.

The key is cohesion. State should live in objects that:

  • Have a clear lifecycle
  • Own their behavior
  • Don’t leak into the rest of the system
  • Are managing ephemeral data, not persistent records

In game development, for example, many objects manage both internal state and behavior tied to a well-defined lifecycle — a natural fit for class-based encapsulation.

Ephemeral or short-lived data belongs inside classes that directly control that lifecycle. Long-lived or persistent data should reside in a database, cache, or external API. Classes can read and write that data, but shouldn’t try to own or retain it.

Contextual values — like session data, request scope, or frontend state — are also better constructed where they’re needed. They’re often tied to a specific user, time, or flow, and don’t belong in global DI containers. Treating them as contextual inputs keeps the boundaries clean and avoids accidental state leaks.

We’ll dive into practical examples of these patterns in the next part of the series.

Dependency Injection Should Be Explicit

Some might argue that global singletons are fine for things like config files, loggers, or telemetry clients. After all, they’re used everywhere and often initialized once. But in practice, they’re rarely as simple or harmless as they seem.

These so-called singletons almost always involve side effects:

  • Config often pulls from dynamic environments or remote sources.
  • Logging requires runtime setup, formatting, and transports.
  • Feature flags often start as a simple in-memory key-value store, but over time evolve into complex rule engines that evaluate conditions dynamically — sometimes even requiring server-side calls.
  • Telemetry clients rely on side-loaded credentials and context.

Wiring dependencies manually — whether via DI frameworks or localized injection — makes them explicit and traceable. You can see what’s being used, where, and why. This isn’t about evangelizing any one tool — it’s about reinforcing clarity and testability through visibility.

Dependency Injection (DI) tends to provoke strong reactions — it's either misused as a magic context grab-bag, or religiously avoided altogether. But when used intentionally, DI gives you a system-wide service map, not an ambient variable soup.

Use DI for:

  • Long-lived shared services (Logger, DB, API clients)
  • Config or feature flags
  • Composable business logic

Avoid DI for:

  • Anything contextual (Session data, request-specific data)

A good way to think about DI is as a system-wide service map — not a place to store request- or user-specific context.

The rule of thumb: inject services, construct context.

Functional Principles Still Matter

The best systems I worked on still lean heavily on functional ideas:

  • Immutable definitions
  • Composability
  • Strong typing
  • Isolation of effects

But real-world applications need side effects — logging, config loading, HTTP calls, and more. DI and OOP give you structure around those effects without making the system impure everywhere.

The goal isn’t functional purity — it’s having clear, controlled side effects with minimal confusion about where they happen and why.

Wrapping Up

This series is about clarity in architecture — not purity, not paradigm loyalty. If you’ve ever felt unsure where a piece of logic belongs, or struggled to keep code testable and understandable as it grows, this series is for you.

Next up: we’ll look deeper at when to use a class, when not to, and how to make the boundary between dependencies and behavior sharp.