Why Streams?
Imperative, Object-Oriented programming is quite powerful, yet for some problems, streams are way more suitable. In this article, I enumerate some use cases where I found that RxJs streams lead to much better solutions than using only imperative approaches.
I’m coming from an Object-Oriented background, having experience in C++, C#, and JavaScript/TypeScript. I’m quite confident with the OO mindset and I can write pretty good imperative code to solve most of my problems. However, in the past years, I’ve found myself writing more and more stream-heavy code. And I think that I’m creating significantly better codes using OO architecture and streams combined.
In this article, I go through those use cases where I found that streams excel. I focus on applying streams in mostly OO codebases, NOT on ones that are already built on some Redux-like state management (e.g. NgRx).
First, I have to make a trivial notion. Trivial may it be, yet it took me a while to fully grasp it.
Streams are Not Object-Oriented
If you’re coming from an Object-Oriented background, chances are high that you need to embrace a different approach besides the OO principles. Because streams are NOT Object-Oriented.
OO approach uses the divide-and-conquer method. The whole problem is too big, so we separate it into smaller chunks again and again, and create smaller groups of responsibilities that we can manage — classes. Perhaps you’ve been doing this for years. You know design patterns, created nice architectures, you’ve mastered a few MV* frameworks, 3-layered architectures, and so on.
Switching Mindset
Designing proper streams goes somewhat against this mindset. We shouldn’t ask the typical questions like “who owns this data”, “which responsibility does this task belong to”, or “what data types (classes) are to be defined”. These won’t necessarily help us.
Instead, focus on the goal: what to solve with this stream? And then: what inputs (data sources, preferably input streams) are required to solve this task?
The rest will be arranged accordingly. A typical example goes along these lines: we have all the inputs as streams. Let’s combine them into a single one, then pipe it through a few map operations, perhaps some filters, and then subscribe to the result to make things happen when they ought to (= when your pipe fires). But this may feel somewhat strange at first.
For more complex pipes, there will be lots of intermediate data types. The hardcore OO mindset might start whispering to define types for everything. You shouldn’t.
For asynchronous tasks, the execution might feel too volatile and without clear ownership. Yes, because it’s a stream, rather than a well-known OO solution (e.g. an async function of a class).
Streams might combine data sources from various sources, which might feel like the betrayal of the Single Responsibility Principle. But maybe your OO muscles are being a bit too lively.
OO is Static, Streams Focus on the Dynamics
OO focuses mostly on the more static parts of the code: objects that exist more or less permanently (like services) or data being passed around. OO mindset is close to the DB, Class, and maybe the Deployment Diagrams in UML.

Streams are closer to the Sequence or Activity Diagrams of UML.
Streams excel where imperative code is weaker: where the focus is on the dynamic behavior of the code, not a single, linear execution.

These two approaches are not exclusive, they often complete one another. E.g. you can still arrange your streams into classes. I often do so. It provides a sound OO architecture for the application, which can be desired if you already have an OO framework (e.g. a 2/3-layered Node.js app or anything in Angular). On the other hand, one can also use pure functions.
Where to Use Streams?
Programming is hard. We have complex problems to solve fast and well. And quite often mess up, often by the bad code written by Past Me. Dang it.

So, at the very least, we shall pick up a broad variety of solutions to prepare ourselves against the wide variety of challenges. Let’s see why Streams are an important asset in our belt.
1. Streams Bring Complexity to the Surface
Streams are not easy. I think imperative code is much easier to understand. (This might be because of my education and background, but hey, the majority of coders probably see this similarly.)
But there is a fundamental problem with imperative code: It focuses on sequential execution.
The linear execution of an imperative code is easy to understand. However, if an imperative code — e.g. an object — gets triggered in various ways, and we have to deal with a changing internal state and a few constraints, it’s getting difficult to see the whole picture.
I mean, it’s pretty easy to cover like 95% of all possible state transitions. But what about that single edge case we missed? How can we make sure that all possible transitions are covered properly? Well, if you’re a seasoned engineer, you have a few tricks in your sleeves. You may use state machines. Design patterns from the gang of four, and also from your experience. Test, of course, in all levels and flavors. SOLID, DRY, YAGNI, and most importantly: the general wisdom accumulated through years of coding.
But still, how do you know you’ve covered all edge cases?

So the seemingly simple imperative code can easily become a spawning pool of ugly bugs.
When applied properly, streams can give stronger guarantees on the correctness of your applications. They come at a cost though, they make the real complexity of the problem visible. We’re gonna have to understand the problem as a whole to deal with streams.
When using streams, we have to lose the illusion of simplicity.
2. Dataflow: Create Once, Handle All Changes
Let’s say we have a frontend component that gathers data from like 3–4 sources, and then it displays some derived data. Any of these sources can change arbitrarily over time, and we gotta keep the UI updated.
The imperative code may recalculate all when anything changes. That’s pretty straightforward and easy to understand. But say, we gotta keep an interval running, cannot stop&restart it. Or the data is large and it changes quite often, so performance aspects are about to kick in. Now we cannot recalculate everything whenever something changes. Time to get clever and start to handle each change one by one, right? Wrong. Getting clever is usually a very wrong idea and often leads to cryptic code and/or a spawning pool of bugs.
This is the exact moment for streams to step in. If the 3–4 data sources are available as streams, we can construct a pipe from them that calculates the derived data.
Usually, creating this pipe is more difficult than writing imperative code. Especially if you are new at Stream Land. But once we understood the problem (and have a bit of experience with streams), it’s easy to create a performance-efficient, not-so-easy, but still-easier-to-reason-about code.
The stream pipe you construct will handle the very first calculation, and then all possible changes the same way. In my experience, this will leave less space for sneaky edge cases, the code will have fewer bugs, it will be shorter, and easier to maintain.
3. Cross-Platform, Standardized Toolbox
Rx is available in many programming languages. I focus mostly on RxJs, but the examples are valid in other languages as well.
When constructing pipes to create new streams, we have an impressive set of operators and other tools at our disposal. This toolbox can take the heavy load off our shoulders.
The operators and other tools in RxJs take quite a time to get familiar with. But they can improve programmers’ productivity drastically as they might help solve many of the more difficult problems.
In the world of programming, investing in one’s productivity is usually a very good idea. I highly recommend getting familiar with RxJs for all JS developers. And if you are a team leader, consider this from the viewpoint of your team’s productivity as well.
Not everything is worth the investment. But if you use Angular, you’d better get familiar with the details of Angular. The same goes for React or Vue, and all other frameworks. I think RxJs deserves a place among these tools that are worth getting familiar with.
Mastering RxJs takes time and a lot of practice. You’ll mess up big a few times. You’ll gotta pay the price. But in my experience, it’s worth it.
4. Testability
You do tests right? Right?!
There are quite good testing tools for RxJs, marble tests are really powerful. They are easy to understand, they can handle time-related stuff (except for Promises), and they help you identify the behavior of your streams surprisingly precisely!

5. Decoupling & Inversion of Control
It’s hard to define what good code means. But some aspects grasp a significant property of good code. Like with decoupling:
Good code is easy to delete.
There is more:
Deleting code is more fun than writing it.
Haven’t you felt it? When deleting a large chunk of obsolete code, when clearing a few annoying parts of some old code… it’s good. But the best is:
The best code is no code at all.

Still, here we are, writing code on most of our workdays. (And sometimes during weekends. Thank you for your patience, My Love!)
Good code is decoupled. It’s easy to modify or delete each part more or less independently of the others. And streams can improve decoupling.
It’s similar to Inversion of Control (IoC). Originally, IoC is used regarding Dependency Injection (DI). Streams are something similar: they are “Inversion of Control” on the interaction of classes. What does it mean? Let’s assume we have “Class A” managing some data, and “Class B” is interested in the changes of that data.
- The old way: Class A expects a callback or a given interface to notify it. Class B will provide the callback or implement the given interface.
- The more decoupled way (a.k.a. Observer pattern): Class A provides an Observable. Class B will subscribe — or will be subscribed by an external party. Class A and Class B have no direct touchpoint.
See how the latter one is more decoupled? And a bonus: it can handle more subscriptions seamlessly.
6. Performance
Another experience of mine is that stream-oriented solutions are usually more performant than a basic imperative approach. For this reason, Angular’s OnPush change detection strategy builds heavily on streams and the async pipe.
The fine-tuning of stream pipes is relatively easy by nature. Of course, you gotta understand how subscribing works and learn distinctUntilChanged, share, shareReplay operators to make streams shine. But still, these are powerful and — once you’ve come to understand them — pretty straightforward tools.
I have a good debugging challenge regarding this topic, feel free to test your knowledge.
7. Fewer Compromises
When dealing with complex timing-related issues, I often found myself making an unsatisfying compromise. Something along with these lines: “Meh, let’s make it a 100ms interval, it will be close enough.” And it was usually more than enough, but still… it left a bad taste in my mouth.
When using streams, I hardly find myself facing such compromises. Maybe because when using streams, I’m forced to face the whole complexity of the problem. And besides understanding it, I also have the proper tools to create the solution easily.
When working with streams, I can work more uncompromisingly at significantly smaller costs in time and effort.
8. Start Using Them Iteratively
Want a bite of the streams already? Good news: you can introduce streams in an OO-heavy codebase, too. You don’t have to go all-out stream-oriented or functional to reap the benefits of streams.
Currently, I work on an OO-heavy codebase, where we’re introducing streams step-by-step when needed. New code is usually much more stream-oriented than the old ones. Still, we’re planning to keep the good old OO architecture. It serves us well and solves many of our problems.
So, while keeping the OO architecture, we add more reactive solutions here and there.
Of course, there’s a concern regarding the consistency of the codebase. You shouldn’t board every hype train you encounter. But streams and the toolbox of RxJs are worth considering adopting. I think that the benefits heavily outweigh the costs, especially if you can reach a breaking point where the benefits of streams start to add up and things get more synergic.
When NOT to Use Streams?
Streams are just one tool — an important one, though — in the belt of a skilled programmer. You should use it when it makes your life easier.

Let’s see 3 use-cases when you shouldn’t rush into using streams.
Very Simple Problems
Y’ain’t gonna need streams for these. They would just make very simple problems more difficult.
Strictly Linear Execution
E.g. algorithms; or most of the basic 3-layered and/or MVC architectures.
Typically, a call for a 3-layered web app goes like this:
- Controller: the client requests stuff
- Controller → Service: get the stuff
- Service → Repository1: get some stuff from the DB
- Service → Repository2: get other stuff from the DB
- Service: calculate and return stuff to the Controller
- Controller: return stuff to the client
It’s pretty straightforward, isn’t it? Not much room for streams here. ASP.NET MVC, Nest.js, or even a proper Laravel app can do this.
The tide starts to turn if you have a considerable in-memory state (e.g. a game logic) with which the clients interact. That’s a good candidate for streams.
I Wanna Use Streams
It’s commendable, nevertheless, desires should not direct architectural decisions. First, find a problem that is a good candidate for streams.
Wrap up
I highlighted the following areas when using streams may boost your performance and raise the quality of the code you create.
- Streams Bring Complexity to the Surface
- Dataflow: Create Once, Handle All Changes
- Cross-Platform, Standardized Toolbox
- Testability
- Decoupling & Inversion of Control
- Performance
- Fewer Compromises
- Start Using Them Iteratively
I’ll write more about streams in the following weeks. Subscribe if you’re interested.
Acknowledgments
You can learn pretty fast when having experts helping you. I’ve learned much about NgRx & some advanced stream stuff from Viktor and Péter at Betsson. Thanks, guys!
More content at plainenglish.io. Sign up for our free weekly newsletter here.