by unscaled | 2025-10-28 05:13:38+00:00
I think there is an issue where reactive frameworks are massively overused in languages that have (or had) weak concurrency patterns. This was true for a while in JavaScript (before async/await became universally supported), but it's especially endemic in the Java world, especially the corners of it which refused to use Kotlin.

So yes, in this particular case, most of the usages of RxJava, Reactive Streams and particularly Spring Reactor is just there because programmers wanted pretty simple composition of asynchronous operations (particularly API calls) and all the other alternatives were worse: threads were too expensive (as Brian Goetz mentions), but Java did have asynchronous I/O since Java 1.4 and proactor-style scheduling (CompletableFuture) since Java 8. You could use CompletableFutures to do everything with asynchronous I/O, but this was just too messy. So a lot of programmers started using Reactive framework, since this was the most ergonomic solution they had (unless they wanted to introduce Kotlin).

That's why you'd mostly see Mono<T> types if you look at a typical reactive Spring WebFlux project. These Mono/Single monads are essentially CompletableFutures with better ergonomics. I don't mean to say that hot and cold multi-value observables were used at all, but in many cases the operator chaining was pretty simple: gathering multiple inputs, mapping, reducing, potentially splitting. Most of this logic is easier to do with normal control flow when you've got proper coroutines.

But that's not all what reactive frameworks can give you. The cases when I'd choose a reactive solution over plain coroutines are few and pretty niche: to be honest, I've only reached to a reactive 3 or 4 times in my career. But they do exist:

1. Some reactive operators are trivially mapped to loops or classic collection operators (map/reduce/filter/flatMap/groupBy/distinct...). But everything that's time-bound, is more complicated to implement in a simple loop, and the result is far less readable. Think about sample or debounce for instance.

2. Complex operation chains do exist and implementing them as a reactive pattern makes the logic far easier to test and reason about. I've had a case where I need to implement a multi-step resource fetching logic, where an index is fetched first, and then resources are fetched based on the index and periodically refreshed with some added jitter to avoid a thundering herd effect, as well as retries with exponential backoff and predictable update interval ranges which is NOT affected by the retries (in other words: no, you can't just put a delay in a loop). My first implementation tried to model that with pure coroutines and it was a disaster. My second implementation was RxJava, which was quite decent, and then Kotlin Flow came out, which was a breeze.

3. I'm not sure if we should call this "Reactive" (since it's not the classic observable), but hot and cached single values (like StateFlow in Kotlin) are extremely useful for many UI paradigms. I found myself reaching to StateFlow extensively when I was doing Android programming (which I didn't do a lot of!).

In short, I strongly disagree with Brian Goetz that Functional Reactive Programming is transitional. I think he's seeing this issue from a very Java-centric perspective, where probably over 90% of the usage we've seen for it was transitional, but that's not all that FRP was all about. FRP will probably lose its status a serious contender for a general tool for expressing asynchronous I/O logic, and that's fine. It was never designed to be that. But let's keep in mind that there are other languages than Java in the world. Most programming languages support some concept of coroutines that are not bound to OS kernel threads nowadays, and FRP is still very much alive.