By Matthew Perry
In the modern world of software development, building responsive and scalable applications is essential. This is especially true for cloud-based deployments, where keeping costs down while maintaining performance is crucial. Reactive programming, a paradigm centered on efficiently processing asynchronous events and data streams, can be an excellent choice for achieving this. In this article, we will:
- Explore what reactive programming is and why you might use it.
- Take a brief look at Netty
- Compare traditional blocking I/O to reactive non-blocking I/O
- Recommend some reactive libraries and supporting frameworks to get you started.
What is Reactive Programming?
Reactive programming is a declarative programming paradigm primarily focused on processing asynchronous events and data streams. When used appropriately, it enables applications to be more resilient, concurrent, and responsive by managing data flows and reacting to changes in real time. This approach is especially beneficial for systems handling a large number of concurrent requests, such as web servers or real-time data processing applications.
Reactive programming’s declarative and functional approach abstracts away the “how” of asynchronous programming, allowing developers to focus on the desired outcome. This stands in contrast to the more imperative programming style familiar to many Java developers, where one explicitly defines step-by-step instructions for executing a task. For many Java developers, Reactive’s declarative approach presents a learning curve. However, once overcome, it enables them to write more concise, readable code, ultimately leading to more robust and maintainable software.
Why Utilize Reactive Programming?
With reactive programming gaining traction, it is crucial to stay ahead of the curve and understand why it may or may not suit your needs.
- Ecosystem and Community Support: Reactive programming is by no means a new concept, and has a wide range of support throughout the Java ecosystem with various libraries and resources to accelerate development efforts.
- Scalability and Performance: Allows for efficiently handling large numbers of concurrent events to ensure a responsive and performant system under heavy load.
- Real-time Data Processing: Provides ways to handle the continuous ingestion and processing of large streams of data making it ideal for real-time data processing applications.
- Cost Optimization: The efficient use of resources can significantly reduce cloud costs for options such as serverless deployments where you are paying only for the resources consumed.
- Responsive User Interfaces: Enhances user experience through asynchronous handling of user input, network requests/responses, and database updates.
- Future Proofing: Embracing reactive programming within your application early can save costly refactors in the future assuming there is an anticipated need for high concurrency.
- Learning Curve: Reactive programming is a paradigm shift away from what the typical Java developer may be comfortable with, and will require some ramp-up time for anyone new to it. Any organization that anticipates a need for it will find it invaluable to have developers that are well versed in its application.
Introducing Netty
One framework that capitalizes on the reactive paradigm is the Netty Client/Server framework (https://netty.io), which efficiently handles asynchronous I/O operations by leveraging reactive principles. Netty enjoys widespread adoption across the industry, and depending on your use case, you may interact directly with it or through other Java frameworks that rely on it as their underlying server model, particularly when working with reactive applications. Next, we will delve into the traditional model for blocking I/O and its drawbacks, before introducing the reactive non-blocking I/O model, which lies at the core of the Netty framework.
Traditional Blocking I/O — The Thread Per Request Model
With traditional blocking HTTP, an incoming request is assigned a dedicated thread until the request is completed. This is referred to as the Thread Per Request Model. This model offers simplicity and performs well within its limits. However, its downside lies in its limited ability to handle concurrent requests, as it depends on the number of threads available in its pool. Furthermore, if any external blocking calls are required during request processing (such as a database query), the corresponding thread must wait until a response is received, preventing it from serving new incoming requests.
While this approach may suffice for applications with low load and fast request processing times, it may encounter resource inefficiencies and scalability issues in high-load scenarios or when dealing with slower request processing times, such as external databases or service calls. In such cases, the application’s capacity to process concurrent requests is directly tied to the number of threads in its pool.
Of course, scaling the application itself, either vertically or horizontally, could address this issue but may incur significant costs. A more efficient solution would be to adopt a better threading model.
Reactive Non-Blocking I/O — The Event Loop Model
Netty achieves reactive, non-blocking I/O through its Event Loop Model. Incoming events, such as HTTP requests, are queued for processing. The event loop, operating on a single thread, periodically checks the queue, handling events sequentially. If a blocking operation occurs, Netty registers a callback to resume processing the event after the operation completes, freeing the event loop to handle other events. For any CPU-intensive tasks or blocking I/O, the event loop should delegate execution to a separate worker thread, while non-blocking I/O should be executed on the event loop thread itself. Generally, Netty employs 1 to 2 event loops per CPU core to leverage the asynchronous nature of multi-core systems.
In summary, Netty and its Event Loop Model facilitate concurrent request processing with minimal thread proliferation, optimizing resource utilization and scalability for high-performance web applications.
Considerations Before Adopting the Reactive Approach
- Use Non-Blocking I/O Libraries: To get the most benefit from the event loop model discussed above, it is crucial that the majority of I/O used by your application is non-blocking. For instance, if you are currently using JDBC for database interactions, you can switch to R2DBC. If there are no non-blocking alternatives for a specific library, many reactive frameworks provide options for handing off the execution of these calls to a separate thread pool, which is crucial to avoid blocking the event loop. However, be warned that this should only be done when absolutely necessary. Using a separate thread pool for the majority of your I/O will essentially revert your application to the thread-per-request model, and you will likely see no benefit from going reactive. In fact, you may experience worse performance due to the general overhead of using a reactive framework.
- Virtual Threads as an Alternative: Virtual Threads were added in JDK 21 and are designed to handle blocking I/O in a non-blocking manner. This means you can write your code in a typical imperative style and continue to use blocking I/O libraries like JDBC. Although Virtual Threads are fairly new, many frameworks, such as Micronaut and Spring Boot, already support them. If you require a fairly simple application, for instance, an API that takes a request, reads/writes to a database, and sends a response, then Virtual Threads might be a great option for you. You may find Virtual Threads have a smaller learning curve and could save you from having to refactor your application to use non-blocking reactive libraries like R2DBC. Note that Virtual Threads and reactive programming are both tools that can be used to solve similar problems, and in the future, they will likely each find their respective niches. You should weigh your options carefully when choosing which will best suit your needs.
Reactive Libraries Utilizing Netty
To harness Netty’s reactive non-blocking I/O for responsive, resilient, and scalable software, numerous libraries are available. One of the most popular and widely supported reactive libraries for Java is Project Reactor. Project Reactor seamlessly integrates with Netty and offers a reactive programming model based on the Reactive Streams specification, facilitating the development of reactive servers and clients with Netty as the transport layer. Other notable reactive libraries supporting Netty include Vert.x, RxNetty, and Akka Streams.
Java Frameworks Supporting Reactive Programming and Netty
Several Java frameworks can efficiently develop reactive applications running on Netty. These frameworks typically support various reactive libraries, allowing you to choose the one that best suits your use case. Below are some of the most widely used frameworks for your reference.
- Micronaut — Micronaut Documentation
- Spring Boot via Spring Webflux — Webflux Documentation
- Quarkus via Mutiny — Mutiny Documentation
Conclusion
Reactive programming offers a powerful solution to the challenges of modern application development, enabling developers to create highly responsive and scalable systems. By abstracting away the complexities of asynchronous programming and providing a clear focus on desired outcomes through its declarative programming style, the reactive programming paradigm opens up a plethora of new possibilities. With frameworks like Netty and libraries such as Project Reactor, developers have powerful tools at their disposal to harness the full potential of reactive programming and deliver high-performance web applications that meet the demands of software development in the modern world.
Further Reading:
https://www.reactivemanifesto.org/
https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html
Matthew J. Perry is a Senior Software Engineer at Object Computing, specializing in building and deploying high-performance cloud native applications for our clients. With over 8 years of experience, Matthew has a proven track record in leveraging Java and cloud-native frameworks such as Micronaut and Spring Boot to deliver scalable, robust solutions for our clients. He excels in driving complex projects from concept to completion, optimizing application performance, and implementing best practices in software development.