The Java Reactive Streams APIs have been part of the JDK since Java 9.

It is about time for the modern Java ecosystem to migrate away from the legacy APIs (org.reactivestreams:reactive-streams Maven coordinates) and adopt the interfaces in java.util.concurrent.Flow.

I have recently started migrating the Mutiny and Mutiny Zero libraries and thought these notes would be useful to others as well.

Migration of isomorphic APIs

The good news is that the legacy and the Flow APIs are isomorphic. For instance org.reactivestreams.Publisher<T> becomes java.util.concurrent.Flow.Publisher<T>.

One option is to perform string replacements to move from one API to the other, but an IDE like IntelliJ can help you with API migrations (see Refactor > Migrate Packages and Classes):

IntelliJ type migration map

Transition period

The bad news is that moving from one API to the other could be a breaking change for your own code bases.

If your code relies on a high-level implementation of Reactive Streams then the change will be mostly transparent at the source code level. For instance the Hibernate Reactive library uses Mutiny and none of the low-level Reactive Streams types such as Publisher, hence the migration of Mutiny to the JDK Flow APIs requires no change in Hibernate Reactive.

By contrast RESTEasy Reactive does support exposing endpoints using org.reactivestreams.Publisher<T> return types (and not just, say, Multi<T> from Mutiny), so the migration requires more work than just bumping a dependency version.

  1. In many cases one can simply perform an API migration.
  2. In some cases such as that of RESTEasy Reactive there needs to be a transition period where both the legacy and Flow types will be supported.
  3. In some other cases such as using a library with a dependency to the legacy APIs, type adaptation will need to be made.

Flow / Legacy type adapters

The reactive-streams-jvm project contains adapters to go back and forth from legacy types to Flow types.

You might as well use the adapters that I developed and maintain as part of Mutiny Zero.

Suppose that you have a library that has yet to migrate to Flow APIs. You can easily turn a Publisher<T> into a Flow.Publisher<T>:

Publisher<String> rsPublisher = connect("foo"); // ... where 'connect' returns a Publisher<String>

Flow.Publisher<String> flowPublisher = AdaptersToFlow.publisher(rsPublisher);

Type adapters exist for the 4 interfaces of Reactive Streams, and they have virtually no cost.

Passing the Reactive Streams TCK

While the Reactive Streams APIs are fairly simple, the evil is in the protocol and semantics. This is why publishers, processors and subscribers need to pass the Reactive Streams TCK.

There is fortunately a Flow variant of the TCK, so if you have implemented Reactive Streams the changes will be minimal as you transition to Flow.

First, the TCK dependency Maven coordinates will become org.reactivestreams:reactive-streams-tck-flow.

Next, you will need to move your test classes from org.reactivestreams.tck.PublisherVerification<T> as a base class to org.reactivestreams.tck.flow.FlowPublisherVerification<T>.

The rest of your TCK test code will be the same, except that some method names have Flow in them: createPublisher(long) becomes createFlowPublisher(long), etc. You can see that in one of the test cases from Mutiny Zero.


Migrating to the JDK Flow APIs is important for the modern Java ecosystem, especially as Reactive Streams APIs have been part of the JDK since Java 9.

The migration is fairly transparent for application developers as they are unlikely to be directly using the low-level Reactive Streams types. This is instead the duty of frameworks, libraries and drivers to do this transition and impose one less dependency in application stacks.

The migration in itself isn’t too hard to perform as types are isomorphic, but there is an inevitable transition period for stacks where multiple dependencies need to be aligned past Java 8 and on top of the JDK Flow APIs. Type adapters represent a virtually no-cost solution when alignment is not possible yet.

The most important part for Reactive Streams implementers remains its TCK as the guardian of interoperability between various libraries. As the TCK already ships with a Flow variant, migrating away from the legacy APIs won’t break the behavior and interoperability of Reactive Streams implementations.