Asynchronous Primitives

ServiceTalk provides different java interfaces to represent asynchronous operations with different result cardinalities, namely:

These interfaces are also referred to as asynchronous primitives or asynchronous sources in this document.

Interoperability

ServiceTalk follows the ReactiveStreams Specification to define its own asynchronous primitives. Since there are more than one standards, namely: ReactiveStreams Specification and JDK Flow, ServiceTalk decouples itself from a specific standard by defining its own primitives instead of directly using a specific standard. In order to make sure we are following the specifications correctly, we verify our sources using the ReactiveStreams TCK. For interoperability we provide ReactiveStreams adapters and may provide similar Flow adapters in the future.

Lazy execution

All asynchronous primitives provided by ServiceTalk are "lazy"/"cold" such that the work they represent does not start until someone is "listening" (a.k.a subscribed) for the results. This is different from "eager"/"hot" CompletableFuture usages where the work being done to complete the CompletableFuture has already started regardless of anyone "listening" for the results.

The lazy/cold approach has the following benefits:

  • Avoid internal queuing: For streaming operations, starting an operation without an associated listener, requires queuing till a listener is available. Lazy execution avoids this queuing.

  • Implicit work association: Since the asynchronous source is expected to start work only when a listener is available, there is an implicit association between the source and the work it represents. This subtle change is powerful as it means that work can be re-done without invoking the method that created the source. Thus enabling us to generically implement retries on the sources as opposed to the methods that create the sources.

Specifications

As defined above, ServiceTalk defines its own interfaces (specifications consistent with the ReactiveStreams Specification) for the different asynchronous primitives. These interfaces define the minimum API required to represent that asynchronous primitive and their names are suffixed by the term Source. Primary motivation for defining these specifications is the ability to inter-operate between different standards.

Specification interfaces are mentioned here for completeness, typical users are not expected to use them. Instead they are expected to use the richer asynchronous primitives with operators that hides the complexity of flow control and cancellation while enabling easier expression of application logic.

Publisher source

A PublisherSource is an asynchronous primitive that mimics ReactiveStreams interfaces and is designed to be used when the source may produce zero or potentially infinite number of results.

Single source

A SingleSource is an asynchronous primitive that is designed to be used when the source will produce exactly one result or terminate with an error.

Completable source

A CompletableSource is an asynchronous primitive that is designed to be used when the source will complete or terminate with an error.

Asynchronous control flow

An important part of writing an application is to have the ability to express control flow like retries, error handling, combining multiple operations sequentially or in parallel. Asynchronous programming is especially challenging when expressing complex control flow and without higher level abstractions can quickly lead to nested, complex callback logic colloquially known as the Callback hell. Error propagation, cancellation and backpressure is extremely complex to wire through this control flow and leads to subtle issues in applications.

Although specification interfaces express the asynchronous primitives completely, they do not address the common concern of expressing asynchronous control flow effectively, making them less useful for direct consumption by most applications. Neither ReactiveStreams, nor JDK Flow provides higher level abstractions that address this problem. This creates an opportunities for libraries like ServiceTalk to provide these higher level abstraction. The industry has a rich history of using function composition based control flow from Common Lisp and Erlang to more targeted approach in the reactive domain such as ReactiveX, Project reactor, and Akka streams. Function composition is an approach to define common control-flow primitives as functions which are used together with the asynchronous primitives. These functions are commonly referred to as operators.

Operators

ServiceTalk limits specification interfaces to define the contract for all asynchronous primitives and adds operators as part of the asynchronous primitives with operators. Although ServiceTalk developers referenced the eco-system (ReactiveX Operators, Akka streams operators, Microprofile operators, JDK Streams and Project reactor) for existing conventions, there is currently no de-facto standard governing operator names. More details on ServiceTalk operators can be found in our Javadocs.

Asynchronous primitives with operators

These primitives are an extension of specification interfaces and they add operators to the corresponding specification interface. ServiceTalk always provides these rich sources from its APIs making it easier for users to use those APIs.

Publisher

A Publisher extends Publisher source and adds commonly used operators.

Single

A Single extends Single source and adds commonly used operators.

Completable

A Completable extends Completable source and adds commonly used operators.