I went on a bit of a rant this morning after seeing Spring 5’s functional API for designing web application routes. Twitter is a terrible medium for properly distilling your thoughts, and I probably shouldn’t have been so reactive (pun intended) in my full-circle thread. Alas, this conversation needs a blog post, so here we are.

In spite of having talked with Stephane more than six months ago about this, I was caught off guard by Spring’s depature from ceremony and unintentionally conflated the conversation on async web applications with the functional programmatic approach to building them.

I consider myself a "friend of the family" to the Spring project and its community, which I have happily been a part of for a number of years. I want to be clear that any criticism I might have of Spring is absolutely in no way intended to demean any of the extremely talented developers working on the project, all for whom I have a tremendous amount of respect. Spring is the gold standard for code quality and collaboration, and has contributed tremendously to open source.

Let me start out by saying that in spite of my initial opinions on this not being the right space for Spring, with the many replies from multiple members of the Spring team and a little more time to think on it, I am thoroughly convinced this is a great move. That is to say, in the space of a few tweets, I went from: "this is different and doesn’t look right and things like Ratpack already do this and 😭 please don’t change, Spring, I love you the way you are!" to "🤔 ok, I can see what they’re doing here" to "👍 this is super fuckin cool". I’m sorry for any confusion and for the stream of consciousness — or, as Dean Iverson pointed out, I was "rubber duck debugging" these changes in a very public and not-so-great way on Twitter.

Spring 5 is super cool in a number of ways, not the least of which is the fact that they are decoupling from the Servlet API. This is a phenomenal decision, because it gives Spring applications the flexibility to be underpinned by a variety of transport libraries or frameworks or application containers.

For developers already familiar with Spring and who don’t want to change the way they are doing things, they won’t have to — the choice for running synchronous code in a thread-per-request manner on your favorite application container will still be there. But, for developers who need a little bit more out of their apps, and want to leverage asynchronous programming and non-blocking networking, they will be able to by making use of Spring’s new reactive programming capabilities.

In my tweet storm, I brazenly stated that Spring was going reactive for the "buzzwordiness" of it (but I put a "?" to give myself an out on that statement, so fuck you I’m doing that here). Buzzwordiness isn’t it at all. What going reactive means for Spring, as Rossen pointed out, is that Spring can target async servlets in Servlet API 3.1+, as well as use non-blocking and async APIs from libraries like Netty.

I happen to think the way Spring 5 is implementing their reactive model is super smart too. Rossen let me know that with their reactive model, they can target Tomcat, Jetty, and Netty (and probably any servlet API 3.1+ compliant container, and probably any non-blocking networking library). But this also means that they won’t be completely stuck with Netty either for their NIO layer. Given the layers of abstraction, literally any underlying engine can besit Spring 5 applications.

It is good to see the trend continuing of Java web frameworks decoupling themselves from the servlet API. Grails had previously decoupled themselves from the servlet API, and Netty is built completely free of servlets (and most other opinions for that matter). Since Ratpack builds on Netty, it too is servlet-free. Vert.x also falls into this category.

The functional programmatic interface for defining application structure will definitely be valuable for lightweight microservices that want to build on Spring, but I still think Spring’s major value is in its means for providing convention-over-configuration component bindings via things like classpath scanning. Defining your handlers this way also doesn’t lend itself very nicely to unit testing, but you can still test your handlers as integration tests. Given time, we will see how developers choose to adopt this interface.

I want to talk a little bit more about Spring 5’s reactive capabilities, and specifically how they differ from Ratpack’s. Many of the lessons that we have learned over five years of developing Ratpack as a powerhouse async web framework can be brought to Spring to help improve its overall position in the reactive Java web app space.

Spring uses Project Reactor for its reactive processing infrastructure, which gives the reactive constructs of Mono (single item stream) and Flux (multi item stream). These are similar in their usage to Ratpack’s Promise type, except that a Promise will only ever emit a single item (individual items from a multi item stream can be used with Ratpack’s Reactive Streams support. Also worth noting that Reactor’s types can be converted to Reactive Streams types, but that’s a bigger conversation for another day.)

In Ratpack, a Promise is not simply an abstraction over an asynchronous event, as you may find in other libraries, languages, and frameworks. Instead, all processing in Ratpack is done within the confines of an Execution. An execution can be thought of as something akin to a continuation, which allows for processing of asynchronous events in a serial manner. (It does this without the need for bytecode manipulation or other specialized modifications to the JVM runtime. Note that the JVM inherently does not have support for continuations or coroutines.) A Promise in Ratpack denotes a segment in the execution, which is analogous to a frame in a continuation. Execution segments are queued and invoked in a serial manner, therein giving your asynchronous application a guarantee as to the order with which your asynchronous calls will be made and to where and when the resulting data will be available.

The "execution model" in Ratpack is unique in that it provides determinism to a processing flow that would be otherwise non-deterministic.

Ratpack’s Execution Model in Practice
class DataCompositionHandler implements Handler {
  private final ObjectMapper mapper = new ObjectMapper()
  private final String serviceOneEndpoint = "https://service1.internal/api/v1"
  private final String serviceTwoEndpoint = "https://service2.internal/api/v1"
  private final String serviceThreeEndpoint = "https://service3.internal/api/v1"

  void handle(Context ctx) {
    HttpClient httpClient = ctx.get(HttpClient)

    def stateHolder = [:]

    httpClient.get("${serviceOneEndpoint}/products".toURI()).then { response -> (1)
      def products = toList(response.body.text)
      stateHolder.products = products.collectEntries { [it.id, it] }
    }
    httpClient.get("${serviceTwoEndpoint}/reviews".toURI()).then { reviews -> (2)
      def reviews = toList(response.body.text)
      reviews.each { review -> stateHolder.products[review.product].review = review }
    }
    httpClient.get("${serviceThreeEndpoint}/prices".toURI()).then { prices -> (3)
      def prices = toList(response.body.text)
      prices.each { price -> stateHolder.products[price.product].price = price }
    }

    Operation.noop().then { (4)
      ctx.render(json(stateHolder.products))
    }
  }

  private List<Map> toList(String body) {
    mapper.readValue(body, List)
  }
}
  1. Anything that works with data as a Promise, as the HttpClient does, informs the Execution that it is an asynchronous event. Here we are guaranteed that the call to the serviceOne endpoint and its corresponding processing block will happen first.

  2. Next, we are guaranteed that the call to the serviceTwo endpoint and its corresponding processing will happen second.

  3. Then we are guaranteed that the call to the serviceThree endpoint will happen third.

  4. And finally, we use the special Operation promise type (which is a shortcut for Promise<Void>) to ensure that the call to render the products happens after all the processing blocks have completed.

This is a valuable premise for async Java web apps, because the processing flow is handled in a familiar, synchronous manner that can be easily reasoned about and debugged; the possibility for surprises from async events that are triggered out of sequence is therein reduced dramatically. Furthermore, as you can see, it also buys you guarantees in concurrency. Since we are given a deterministic processing order, we are also guaranteed the state of the data as it moves through the respective processing blocks.

Anyone who has written an asynchronous web application will immediately see the value in Ratpack’s execution model, as do the thousands of developers who write code with Ratpack every day, and never have to worry about non-determinism.

Part of our coming work in Ratpack 2 will (probably) involve extracting and generifying the execution model so that it is usable outside of Ratpack applications. When we get to that point, I’m sure that we can work to get it integrated with Spring and Reactor, which will make those projects that much better!

In closing, I’d like to thank all the members of the Spring Framework team who reached out on Twitter to set the record straight. I do have strong opinions on this stuff, but they are loosely held; I definitely would’ve come to this conclusion on my own, but I do appreciate the discourse (and patience)!