Comparing 3 Spring Boot Apps: Classic Servlet, Reactive, RSocket

Let’s test 3 different spring boot application types
- Classic Servlet App (Servlet)
- Reactive
- RSocket
Motivation
HTTP is a widely used transport for APIs, and it has pros and cons. It’s old and has big performance issues, but it is still widely used because of its ease of use. Let's highlight and compare it with more efficient solutions
Assumptions
- Prepare basic application without using best practices
- tests could not be the same on 100%
- JVM parameters could be optimized for each application type, but it’s not the goal
Requirements
- Accept message. Receive list/stream of messages and save it into DB. The controller should return an empty response
- List messages by author. The controller should return all messages by author.
- All tests should use the same data feed but could use different approaches to send/receive data
- All tests should measure RPS and
message format
{
"id": UUID,
"text": String,
"author": String,
"createdAt": ZonedDateTime
}
Test Scenario
Create Messages
- Iterate over different Concurrent Threads Count (4, 8, 16, 64) to Create Messages and Message Size (1, 10, 100, 1000) in Single Request
- Run Test
- Clean DB
- Sleep N Seconds
- Repeat
Get Messages
- Iterate with different Concurrent Threads Count (4, 8, 16, 64) to Get Messages and Message Size (1, 10, 100, 1000) that will be generated
- Generate Test Messages
- Run Test
- Clean Data
- Sleep N Seconds
- Repeat
Results
Execution Env
java -version
>openjdk version "21.0.5" 2024-10-15 OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu124.04) OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu124.04, mixed mode, sharing)
cat /proc/cpuinfo model name :
>Intel(R) Core(TM) i9-14900HX
cat /proc/meminfo MemTotal:
>32540280 kB
Servlet

Flux

RSocket

Test Summary
Send Messages (RPS)
threads\type | servlet | reactive | rsocket |
---|---|---|---|
4 | 3691.5 | 18668.4 | 20862.3 |
8 | 5088.7 | 15821.7 | 20362.9 |
16 | 4497.7 | 13346.9 | 21043.3 |
64 | 4501 | 12443.3 | 20881.3 |
Get Messages (RPS)
threads\type | servlet | reactive | rsocket |
---|---|---|---|
4 | 1786 | 7270 | 20862.3 |
8 | 3600 | 5367 | 20362.9 |
16 | 3502 | 2914 | 21043.3 |
64 | 3535 | 2064 | 20881.3 |
Where are the numbers hidden?
Let’s begin with the Servlet implementation.
Servlet
Servlet is a legacy interface that is not good for asynchronous calls. With the latest API it provided an opportunity for async dispatch, but even with it had bad performance. The main reason is that it is a classic way to process HTTP request/response.
When we need to send a List of messages, we need to:
- Collect Messages on the Sender side
- Serialize this Collection to bytes (json)
- Send it to Receiver (Server)
- On the Server side even with a good web server that works on NIO we need to accept whole HTTP requests with all Messages
- Deserialize Received bytes into the list of objects
- Iterate over received messages
- Insert Message into DB
- Respond to Sender with HTTP Code 200
This flow has several weaknesses
- Collecting Batch Size on the Sender side require memory to store this collections of messages and also required big buffers for serialization that could lead to problems like GC pauses and OOM.
- On the server side we need to collect the whole HTTP request (All messages) and only after that we could do something with it. If the Sender have a slow connection or big messages it will affect server performance and block whole processes. In this case we also need memory for incoming payload and for deserialized Messages that could lead to problems like GC pause and OOM.
So this is why Servlet has bad numbers.
Let’s review why Reactive have better number even if use same HTTP and serialization.
Reactive
Spring Boot Reactive provides HTTP Server but unlike Servlet it’s working on Netty that gives us more flexible and async interface that we will use.
The main difference is that we will use application/ x-ndjson which allows us to send/receive messages in a more convenient way for faster processing without blocking and awaiting all HTTP request/response
application/json payload
[
{..},
{..}
]
application/x-ndjson payload
{}\n
{}\n


Why does it matter?
When we need to send List of messages:
- We don’t need to collect all Messages on the Sender side. We could emit them as we are ready to produce them
- Serialize each event to bytes (json)
- Send it to Receiver (Server)
- On the Server side with a good web server that working on NIO we do need to await whole HTTP request with all Messages. We will accept as the Sender will be ready to produce and send them
- Deserialize Received bytes into Message
- Iterate over received Stream (Flux)
- Insert Message into DB
- Respond to Sender with HTTP Code 200 after Stream (Flux) is completed
class Controller {
public Mono<Void> addMessages(@RequestBody Flux<Message> messages) {
return messages.flatMap(message -> r2dbc.insert(Message.class).using(message))
.then();
}
}
This solution has a better performance because of using non-blocking way to process HTTP requests but it also has several weaknesses
- The first bottleneck is DB which could affect our performance on high load.
- If the stream fails on several items, the whole HTTP request will fail and we need to handle it properly. Reactive Application on Spring provides reactive architecture on the server side but it doesn’t give us all reactive functionality between client and server like backpressure
Let’s review RSocket implementation
RSocket
Spring RSocket server doesn’t work on HTTP, RSocket is reactive binary protocol that builds over TCP
When we need to send List of messages:
- We don’t need to collect all Messages on Sender side. We could emit them as we are ready to produce them
- Serialize each event to bytes (json)
- Send it to Receiver (Server)
- On the Server side with a good web server that working on NIO we do need to await the whole request with all Messages. We will accept as the Sender will be ready to produce and send them
- Deserialize Received bytes into Message
- Iterate over received Stream (Flux)
- Insert Message into DB
- Respond to Sender after Stream (Flux) is completed
Looks the same as Reactive right but this is not all. RSocket also provides backpressure and rich functionality of reactive paradigm where consumer could ask publisher to stop producing more messages if it overloaded or give more if it have enough capacity this is why RSocket provide better linear scalability and performance
Summary
Servlet
Classic HTTP/Servlet applications have bad performance because HTTP is blocking and it uses blocking operations on the server side to work with DB
Mitigating Blocking operations
To mitigate the issues associated with blocking operations we could use non-blocking or asynchronous alternatives:
- Non-blocking APIs: Utilizing non-blocking APIs provided by the operating system or third-party libraries, which allows for efficient, scalable handling of I/O operations, including network communication.
- Reactive: Using reactive frameworks that make it easier to handle streams of data and events asynchronously, thus avoiding the need for blocking operations.
Reactive/Flux
Reactive/Flux applications have better performance because it use a non-blocking API approach on the backend side and streaming support between client/server that helps to process more requests but they also used HTTP and have the same blocking issues
RSocket
The best performance have RSocket because it fully utilize reactive approach with backpressure (Non-Blocking API, Reactive)
Links
- Code: https://github.com/alimovalisher/reactive-tests
- Spring Boot Reactive: https://spring.io/reactive
- Flux / projectreactor: https://projectreactor.io
- RSocket: https://rsocket.io