The main problem I have with reactive programming in Java, at least with Project Reactor which is the only one I've used is the cryptic stack traces. You can regain some information by enabling runtime instrumentation to be used in development, but has too much of a performance hit to be used in production. It doesn't produce much better stack traces either. Trying to read a profiler flamegraph is nigh impossible for reactive code. For the debugger, you need to manually insert breakpoints in each step of the chain, because otherwise you'll just be looking around the reactive library's internal code.

The other problems I have is that as soon as you need to do anything blocking (e.g talking to SQLite or RocksDB over JNI), the whole thing falls apart in terms of threading. Doing a HTTP request somewhere in a reactive chain that does some blocking operations - well shit now you're blocking the reactor-http-netty-* threads, which are fixed so you're worse off than with thread-per-request at that point. Or even just using Caffeine cache, or any caching library really, but Caffeine is one of few that prevents async stampede - well shit now you're running on ForkJoin pool during cache misses, but "reactive" threads on cache hits. You can see how this quickly becomes a tangled mess of thread-hopping and avoiding blocking event loops, and it's extremely complicated to get this right with how subscribeOn/publishOn works and how the profiler can't tell you which threads runs what code because the stack traces are filled with garbage.

You also opt out of a lot of what Java offers in terms of synchronization. Using the synchronization keyword is a big no-no, the JVM can park the thread while waiting for the lock. Now that thread cannot schedule other tasks just because that one task ran into lock contention, and it's highly likely a fixed pool of threads. You're basically left with CAS, AtomicLong/AtomicBoolean/etc, and that's it. I've even seen BlockHound, project reactor's tool to help you find if you are blocking somewhere, trigger "Blocking call to LockSupport.park()!" during a ConcurrentHashMap lookup. Add to this that most caching libraries are implemented on top of ConcurrentHashMap. Getting async right in reactive code has a huge mental overhead and you need intricate knowledge of the libraries you depend on, the platform you run on and what it is that actually enables async in the kernel, and where it is unavailable. Good example of this; that UUID library you use uses a CSPRNG. Have you set the JVM flag to read it from the non-blocking /dev/urandom rather than the default /dev/random on Linux?

I think reactive programming in Java is decent as long as all you're really doing is accepting/performing HTTP requests and talking to a database that has a reactive client connector. In essence, anything that strictly deals with networking, since that's all that can really be made truly async currently, thanks to epoll/kqueue/iocp on linux/mac/windows respectively. Hopefully io_uring will broaden that to file IO as well eventually, but unfortunately that's linux-only for now.

Contrast this with Go's scheduler that just takes care of this for you - no need to think about it. Or Rust + Tokio, where it is explicit with tokio::spawn_blocking and offering async counterparts to std Mutex, RWLock, Channels, etc that doesn't block the current thread on lock contention.