Object-oriented, functional or service layer?
| Dimension | Object-oriented programming | Functional programming | Service layer |
|---|---|---|---|
| In one line | Objects that own their data and the methods allowed to change it | Pure functions that turn immutable values into new values | Stateless services that run each use case over plain data objects |
| Where state lives | Inside objects, in private fields changed only by their methods | In immutable values passed from function to function; real changes are pushed to the edges | In the database and in plain data objects loaded per request; services keep none |
| Where behaviour lives | In methods on the object that owns the data | In standalone functions that take data and return data | In service methods, one per use case, between controllers and repositories |
| How variation is handled | Several classes behind one interface, picked at run time (dynamic dispatch); subclasses, used sparingly | Pass a different function in; branch over the shapes a value can take | Inject a different implementation of an interface through the constructor |
| Testing | Put the object into the right state, call methods, check the result; mock its collaborators | Call with inputs and compare the output; no setup and no mocks for the pure core | Pass fakes for repositories and gateways to the constructor |
| Concurrency | Shared mutable objects need locks or must stay on one thread; Kay’s message-passing objects avoid that, like actors | Immutable data can be shared freely; parallel pipelines are safe when the functions are pure | Services hold nothing, so you run more copies; the database transaction handles conflicts |
| Typical languages and frameworks | Java, C#, C++, Python, Smalltalk, TypeScript classes; Go’s interfaces without a type hierarchy | Haskell, OCaml, F#; the same style in JavaScript, Java streams, Rust iterators and React | Spring (@Service), NestJS (@Injectable), ASP.NET Core, Rails service objects |
| Common failure mode | Deep inheritance trees and a noun for every verb (AbstractProxyMediator) | Dense chains nobody can read, hidden copying costs, effects leaking into “pure” code | All rules in fat services over bags of getters and setters (the anemic domain model) |
| Pick it when | Long-lived things with rules to protect, and many variants behind one interface | Calculations, transformations and rules you want to test easily and run in parallel | Business backends with several entry points, on a framework that already expects it |
Rule of thumb
These aren’t rival camps; most real backends use all three. A common, healthy mix: a service layer as the entry point for each use case, domain objects that guard their own rules, and pure functions for the calculations in between. Reach for objects where state has rules, functions where data flows, and services where a request has to be coordinated across a database, payments and email.
Sources
- Alan Kay to Stefan Ram: on the meaning of “object-oriented programming” (email, Jul 2003)
- IEEE Milestone: Object-Oriented Programming, 1961–1967
- Artima: Design Principles from Design Patterns, a conversation with Erich Gamma (Jun 2005)
- Go FAQ: Is Go an object-oriented language?
- Steve Yegge: Execution in the Kingdom of Nouns (Mar 2006)
- Python docs: Functional Programming HOWTO
- John Hughes: Why Functional Programming Matters (1990)
- Java SE 21 API: package java.util.stream (parallelism, side-effects)
- MacQueen, Harper and Reppy: The History of Standard ML (HOPL IV, 2020)
- React docs: Keeping Components Pure
- Martin Fowler’s P of EAA catalog: Service Layer (Randy Stafford)
- Martin Fowler: Anemic Domain Model (Nov 2003)
- Spring Framework API: @Service
- NestJS docs: Providers
- Microsoft Learn: Dependency injection in ASP.NET Core
- Bryan Helmkamp, Code Climate: 7 Patterns to Refactor Fat ActiveRecord Models (Oct 2012)