Last Updated: May 7, 2026 at 10:00

Reactive vs. Object-Oriented: Two Different Ways of Thinking About Code

Why choosing reactive programming is not a library decision — it is an irreversible shift in how you think about time, state, and failure

Most teams adopt reactive programming for the throughput gains and only later discover they have also inherited a completely different mental model for debugging, state management, and error handling — one that cannot coexist peacefully with object-oriented thinking in the same developer's head at two in the morning. This article explains how reactive works, what it costs, where it quietly goes wrong, and why the question is never "is reactive better?" but always "what problem am I actually trying to solve?"

Image

Lets start with a story. A team had built a perfectly respectable e-commerce backend using Spring MVC and traditional object-oriented practices. Everything worked fine. Then Black Friday came, and their thread pool fell over. Not because the database was slow or the code was inefficient, but because every concurrent request held a thread while waiting for downstream responses. The hardware was fine. The network was fine. But the thread-based concurrency model had hit its limit.

Someone on the team had watched a conference talk about Spring WebFlux. It promised non-blocking concurrency and better throughput under load. They spent two months rewriting critical paths to use reactive streams. When they deployed, the system handled the load beautifully. Then a subtle bug appeared. Orders would occasionally disappear from the confirmation page, only to show up again fifteen minutes later in the admin panel.

Debugging took four days. The culprit was a single missing subscribe() call in a complex reactor chain that silently dropped errors instead of logging them. One line. Four days.

That team learned something that no conference talk had warned them about. Reactive programming was not just a different library or syntax. It demanded a different way of thinking about time, state, and debugging. They had underestimated the mental model shift, and it cost them.

Let us understand why.

What these approaches actually mean

Object-oriented programming, the style most of us learned first, thinks in terms of control flow. You have objects with methods. You call a method. It does something, perhaps calls other methods, then returns. The path your program takes is a sequence of explicit steps. When you debug, you set a breakpoint and step through line by line. The program moves because you tell it to move.

Reactive programming flips this. Instead of control flow, you think in data flow. Instead of calling methods, you declare transformations on streams of events. Instead of telling the program when to do something, you describe what should happen whenever data arrives. The program then reacts to data as it comes.

Here is the most important distinction. In object-oriented programming, you pull data by calling methods. In reactive programming, data pushes itself to you.

Consider this with a concrete example. In OOP, you might write:

List<Order> orders = orderService.getRecentOrders();

You are reaching out and pulling data into your variable. Execution stops right there until the data arrives. In reactive, you write something closer to:

orderService.recentOrders().subscribe(orders -> display(orders));

You are not pulling anything. You are saying "when orders arrive, display them" and then you move on. The data will come to you when it is ready.

This difference seems small. In practice, it changes everything about how you debug, how you think about errors, and how you organize your application.

Why debugging reactive code feels so strange

This is where the real cost of reactive becomes visible.

In OOP, when something goes wrong, you ask a sequence of questions. What method was I in? What line are we on? What were the arguments? What called this method? You can recreate the entire timeline of your program by looking at the call stack and the current state of objects. Step-through debugging works beautifully because execution follows a single path through time.

In reactive, you cannot do this. Your program is not a single path. It is a graph of transformations that data flows through, potentially arriving from multiple sources at unpredictable times. When the orders started disappearing, that team could not set a breakpoint on a line number and step through. The error was not happening on a line. It was happening in a stream that had gone silent.

When something breaks in reactive code, you do not ask "what line are we on?" You ask "which stream emitted an error, and what were the last ten values that flowed through these five transformations before it broke?" That is a fundamentally different question. Answering it requires different tools.

Stack traces become less useful in reactive systems because the error often surfaces in a subscriber far away from the original source. A database error inside a flatMap operator produces a stack trace that shows reactor internals rather than your business logic. The context of how data arrived gets lost unless you explicitly carry it forward.

The team eventually learned to debug by adding logging operators into their stream chains, printing values at each stage, and building mental timelines from the logs. They used doOnNext in Reactor to peek at values without transforming them. They added error logging before their error recovery logic. This worked — but a debugging session that would take ten minutes in OOP took two hours in reactive.

Most developers who struggle with reactive are not struggling with the operators. They are struggling with the fact that their intuitions about program execution no longer apply.

How state thinking changes

The second major shift involves how you think about state.

Before we talk about reactive programming's approach to state, let's make sure we agree on what "state" even means. State is just data that can change over time. A counter that goes from 0 to 1 to 2 is state. A username that a user can update is state. Whether a button is disabled or enabled is state.

In the kind of code most developers learn first — object-oriented programming — state lives inside objects as variables. You have a counter object. It has a number stored inside it. You call a method to increment it. You read the variable whenever you need the current value. It is direct and intuitive: the data sits somewhere, you go and get it, you change it when you need to.

Reactive programming asks you to think about this completely differently. Instead of storing a value and changing it directly, you describe a stream of events over time and derive the current value from those events.

Think of it like a bank account statement versus a current balance. Your bank doesn't just store "you have £500." It stores every transaction — deposit £200, withdraw £50, deposit £350 — and your balance is calculated from that history. Reactive programming thinks like the transaction log, not the balance field. Nothing is stored directly. The current value emerges from the events that have flowed through the stream.

This feels awkward at first, and that's normal. The awkwardness is actually a useful signal. If you find yourself constantly fighting to grab the current value of a stream as a one-off snapshot — the way you'd just read a variable — reactive is working against you in that part of your code. That's often a sign you're using the wrong tool there.

Hot and cold observables: the concept that catches everyone out

This is the idea that most developers from an OOP background have never encountered, because there's simply no equivalent in that world. Understanding it is what separates people who use reactive programming confidently from people who use it and occasionally get mysterious bugs.

In reactive programming, a stream can behave in two very different ways depending on whether it is cold or hot.

A cold observable starts fresh for every subscriber.

Imagine a Netflix show. Every person who clicks play watches it from episode one. You watching it has no effect on somebody else watching it. Everyone gets their own independent copy, starting from the beginning.

An HTTP request works this way in reactive programming. Every time a new piece of your application starts listening to it, a brand new network request goes out. If three parts of your app are all listening to the same HTTP stream, three separate requests fire. Most developers find this deeply surprising the first time they discover it.

A hot observable is already running, regardless of who is listening.

Imagine a live radio station. It broadcasts continuously. If you tune in at 9pm, you hear whatever is playing right now. You don't get to rewind to this morning's programme. You joined a stream that was already in progress, and anything that played before you arrived is simply gone.

A WebSocket connection works this way. The server is pushing events continuously. When your code starts listening, it receives events from that moment forward. Anything that arrived before you started listening is already gone — there's no going back.

Why this caused the missing orders bug

The team had a stream of incoming orders. Two separate parts of the UI needed to react to each new order: the confirmation screen and the dashboard.

They wired both up to the same stream. What they didn't realise was that their stream was cold — meaning each listener got its own independent execution of the stream rather than sharing a single one. The confirmation screen started listening first and saw the order arrive. The dashboard started listening a fraction of a second later. Because the stream was cold, it created a completely fresh execution for the dashboard — but the order event had already passed through the first execution. The dashboard's version never saw it. The order appeared to vanish.

In OOP, this problem literally cannot exist. If you have a list of orders and two functions both read from it, they both get the same list. There is no concept of reading the list consuming the data. The data just sits there until you explicitly change it.

The reactive fix is to make the stream hot — specifically to make it multicast, meaning it runs once and broadcasts to all listeners simultaneously, rather than spinning up a separate execution for each one. With that change, both the confirmation screen and the dashboard receive the same order event from the same single execution, and the bug disappears.

The real difference in how you fight bugs

In OOP, the state problems you fight are about mutation. Two pieces of code change the same variable at the same time and corrupt each other's work. You solve this with careful access control, with making data immutable, with being deliberate about who is allowed to change what.

In reactive programming, the state problems you fight are different in character. You worry about whether your stream is hot or cold. You worry about whether a listener that joined late missed data it needed. You worry about accidentally triggering duplicate side effects because two parts of your app are both listening to a cold stream that fires a network request for each of them.

Neither paradigm is free of complexity. The complexity just moves to a different place — and the bugs that come from not understanding where it moved tend to be subtle, timing-dependent, and genuinely confusing until the concept finally clicks.

N

About N Sharma

Lead Architect at StackAndSystem

N Sharma is a technologist with over 28 years of experience in software engineering, system architecture, and technology consulting. He holds a Bachelor’s degree in Engineering, a DBF, and an MBA. His work focuses on research-driven technology education—explaining software architecture, system design, and development practices through structured tutorials designed to help engineers build reliable, scalable systems.

Disclaimer

This article is for educational purposes only. Assistance from AI-powered generative tools was taken to format and improve language flow. While we strive for accuracy, this content may contain errors or omissions and should be independently verified.

SQL vs NoSQL: A Decision Framework for Real Systems (Trade-offs, Scale...