I’ve always enjoyed studying reactive programming, starting with RxJS on the front-end, and I was blown away by the possibility of creating more dynamic and reactive applications. The idea of working with data streams and reacting to changes more efficiently really fascinated me. But when I discovered that reactive programming also played a major role in the back-end, especially with Java, my curiosity only grew. That’s when I realized how this approach solves the misconception many people have about Java being “heavy” and “slow,” particularly in scenarios with high request concurrency.
Many people have this perception about Java because of how it handles threads and task execution. In traditional applications, each HTTP request is tied to a separate thread. This thread remains occupied throughout the entire processing of the request, meaning that if it needs to wait for a database response or another external service, it becomes “blocked,” unable to handle other requests. The more concurrent requests the system handles (i.e., more requests arriving at the same time), the more threads are needed.
The first solution that might come to mind is: “Why not just increase the number of threads to solve the problem?” But the issue is that threads aren't “free.” They consume a lot of memory and CPU. Increasing the number of threads without careful planning can make the application “heavier,” consuming even more hardware resources. This not only slows the system down, worsening the user experience, but also raises operational costs.
Analogy
Imagine you're in a restaurant with several waiters serving tables. Each waiter can only serve one table at a time, and once an order is placed, the waiter goes to the kitchen and stands there, waiting for the dish to be ready. Meanwhile, other tables need service, but the waiter can’t help because they’re “blocked” waiting for the food. Now, if all the tables place orders at the same time, each waiter ends up stuck in the kitchen, unable to serve other customers. The restaurant would need to hire more waiters just to keep up with all the orders, which is inefficient.
This is the same issue with threads in Java: they get “blocked,” waiting, while other tasks are left in the queue. And the more clients (requests) come in, the more waiters (threads) are needed, making the system heavier and slower.
Now, imagine the restaurant adopts a more efficient approach: the waiters don’t need to stand idle in the kitchen waiting for the food. They take the order, pass it to the kitchen, and instead of waiting for the dish to be ready, they continue serving other tables. When the food is ready, the kitchen notifies the waiter, who then goes to serve the dish. This way, the waiter can serve multiple tables at the same time without being “blocked” by a single order. This is exactly what reactive programming and non-blocking I/O do.
In the reactive model, a thread (the waiter) can “listen” for when an operation (the dish) is ready and only act at that moment. Meanwhile, it can handle many other requests. This makes the system much more efficient, using fewer resources and handling many more clients (requests) simultaneously without needing to create a bunch of new threads (or hire more waiters). The result is a faster and more scalable system.
Example
To illustrate, I developed a non-blocking endpoint to fetch the attendance details of students in jiu-jitsu classes. I used Panache Reactive and a reactive driver for database communication, all running on the Quarkus framework with Mutiny.
package org.esdras.khan.entity;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.*;
import java.util.Set;
@Entity
@Table(name = "class_entity")
public class ClassEntity extends PanacheEntity {
@Column(name = "instructor", nullable = false)
public String instructor;
@Column(name = "class_name", nullable = false)
public String className;
@OneToMany(mappedBy = "jiuJitsuClass", fetch = FetchType.EAGER)
public Set<Classroom> classrooms;
public ClassEntity() {
}
public ClassEntity(String instructor, String className) {
this.instructor = instructor;
this.className = className;
}
}
package org.esdras.khan.entity;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.Set;
@Entity
@Table(name = "classroom_entity")
public class Classroom extends PanacheEntity {
@ManyToOne
@JoinColumn(name = "jiu_jitsu_class_id", nullable = false)
public ClassEntity jiuJitsuClass;
@Column(name = "start_time", nullable = false)
public LocalDateTime startTime;
@OneToMany(mappedBy = "classroom")
public Set<Presence> presences;
public Classroom() {
}
public Classroom(ClassEntity jiuJitsuClass, LocalDateTime startTime) {
this.jiuJitsuClass = jiuJitsuClass;
this.startTime = startTime;
}
}
package org.esdras.khan.entity;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.*;
import java.util.Set;
@Entity
@Table(name = "student_entity")
public class StudentEntity extends PanacheEntity {
@Column(name = "name", nullable = false)
public String name;
@ManyToMany
public Set<ClassEntity> classes;
@Enumerated(EnumType.STRING)
@Column(name = "belt_rank", nullable = false)
public BeltRank beltRank;
@Column(name = "degree", nullable = false)
public int degree;
public StudentEntity() {
}
public StudentEntity(String name, BeltRank beltRank, int degree) {
this.name = name;
this.beltRank = beltRank;
this.degree = degree;
}
}
public enum BeltRank {
WHITE, // Faixa Branca
BLUE, // Faixa Azul
PURPLE, // Faixa Roxa
BROWN, // Faixa Marrom
BLACK // Faixa Preta
}
package org.esdras.khan.entity;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.*;
@Entity
@Table(name = "presence")
public class Presence extends PanacheEntity {
@ManyToOne
@JoinColumn(name = "student_id", nullable = false) // Foreign key para student_entity
public StudentEntity student;
@ManyToOne
@JoinColumn(name = "classroom_id", nullable = false) // Foreign key para classroom_entity
public Classroom classroom;
@Column(name = "confirmed", nullable = false)
public Boolean confirmed;
public Presence() {
}
public Presence(StudentEntity student, Classroom classroom, Boolean confirmed) {
this.student = student;
this.classroom = classroom;
this.confirmed = confirmed;
}
}
-- Inserir uma classe com ID 1
INSERT INTO class_entity (id, instructor, class_name)
VALUES (1, 'Mestre Moises', 'Técnicas de Finalização');
-- Inserir alunos com IDs 1, 2 e 3
INSERT INTO student_entity (id, name, belt_rank, degree)
VALUES (1, 'Esdras Santos de Oliveira', 'WHITE', 1),
(2, 'Tiagão', 'WHITE', 1),
(3, 'Gilberto Gabriel', 'WHITE', 1);
-- Inserir 10 sessões de aula vinculadas à classe com ID 1
INSERT INTO classroom_entity (id, jiu_jitsu_class_id, start_time)
VALUES (1, 1, '2024-10-01 15:00:00'),
(2, 1, '2024-10-03 10:00:00'),
(3, 1, '2024-10-05 14:00:00'),
(4, 1, '2024-10-07 16:00:00'),
(5, 1, '2024-10-09 11:00:00'),
(6, 1, '2024-10-11 18:00:00'),
(7, 1, '2024-10-13 09:00:00'),
(8, 1, '2024-10-15 17:00:00'),
(9, 1, '2024-10-17 14:00:00'),
(10, 1, '2024-10-19 08:00:00');
-- Inserir presenças para as 10 aulas para os 3 alunos
INSERT INTO presence (id, student_id, classroom_id, confirmed)
VALUES
-- Presenças para a primeira sessão de aula
(1, 1, 1, TRUE), -- Esdras presente
(2, 2, 1, TRUE), -- Tiago presente
(3, 3, 1, FALSE), -- Gilberto ausente
-- Presenças para a segunda sessão de aula
(4, 1, 2, TRUE), -- Esdras presente
(5, 2, 2, FALSE), -- Tiago ausente
(6, 3, 2, TRUE), -- Gilberto presente
-- Presenças para a terceira sessão de aula
(7, 1, 3, FALSE), -- Esdras ausente
(8, 2, 3, TRUE), -- Tiago presente
(9, 3, 3, FALSE), -- Gilberto ausente
-- Presenças para a quarta sessão de aula
(10, 1, 4, TRUE), -- Esdras presente
(11, 2, 4, TRUE), -- Tiago presente
(12, 3, 4, FALSE), -- Gilberto ausente
-- Presenças para a quinta sessão de aula
(13, 1, 5, FALSE), -- Esdras ausente
(14, 2, 5, TRUE), -- Tiago presente
(15, 3, 5, TRUE), -- Gilberto presente
-- Presenças para a sexta sessão de aula
(16, 1, 6, TRUE), -- Esdras presente
(17, 2, 6, FALSE), -- Tiago ausente
(18, 3, 6, TRUE), -- Gilberto presente
-- Presenças para a sétima sessão de aula
(19, 1, 7, TRUE), -- Esdras presente
(20, 2, 7, TRUE), -- Tiago presente
(21, 3, 7, FALSE), -- Gilberto ausente
-- Presenças para a oitava sessão de aula
(22, 1, 8, FALSE), -- Esdras ausente
(23, 2, 8, TRUE), -- Tiago presente
(24, 3, 8, TRUE), -- Gilberto presente
-- Presenças para a nona sessão de aula
(25, 1, 9, TRUE), -- Esdras presente
(26, 2, 9, FALSE), -- Tiago ausente
(27, 3, 9, TRUE), -- Gilberto presente
-- Presenças para a décima sessão de aula
(28, 1, 10, TRUE), -- Esdras presente
(29, 2, 10, TRUE); -- Tiago presente
package org.esdras.khan;
import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.esdras.khan.entity.ClassEntity;
import org.esdras.khan.entity.Classroom;
import org.esdras.khan.entity.Presence;
import org.esdras.khan.entity.StudentEntity;
import org.esdras.khan.response.ClassDetailResponse;
import org.esdras.khan.response.ClassRoomDetailResponse;
import java.util.List;
import java.util.stream.Collectors;
@Path("/jiu-jitsu/nao-bloqueante")
@Produces(MediaType.APPLICATION_JSON)
@NonBlocking
public class NonBlockingJiuJitsuResource {
@GET
@Path("/{classId}")
public Uni<Response> getClassDetails(@PathParam("classId") Long classId) {
System.out.println("Thread (getClassDetails): " + Thread.currentThread().getName()); // Log da thread
return ClassEntity.<ClassEntity>findById(classId)
.onItem().transformToUni(classEntity -> {
System.out.println("Thread (findById): " + Thread.currentThread().getName()); // Log da thread
if (classEntity == null) {
return Uni.createFrom().item(Response.status(Response.Status.NOT_FOUND)
.entity("Aula não encontrada")
.build());
}
return Classroom.<Classroom>list("jiuJitsuClass.id", classId)
.onItem().transformToUni(classrooms -> {
System.out.println("Thread (list classrooms): " + Thread.currentThread().getName()); // Log da thread
List<Uni<ClassRoomDetailResponse>> classroomDetailsUnis = classrooms.stream()
.map(this::getClassroomDetails)
.collect(Collectors.toList());
return Uni.combine().all().unis(classroomDetailsUnis).usingConcurrencyOf(1).with(responses -> {
System.out.println("Thread (combine unis): " + Thread.currentThread().getName()); // Log da thread
List<ClassRoomDetailResponse> classroomDetails = responses.stream()
.map(response -> (ClassRoomDetailResponse) response)
.collect(Collectors.toList());
return Response.ok(new ClassDetailResponse(
classEntity.className,
classEntity.instructor,
classroomDetails)).build();
});
});
});
}
private Uni<ClassRoomDetailResponse> getClassroomDetails(Classroom classroom) {
System.out.println("Thread: " + Thread.currentThread().getName() + " - Buscando presenças para a sessão: " + classroom.startTime);
return Presence.<Presence>list("classroom.id", classroom.id)
.onItem().transform(presences -> {
List<StudentEntity> presentStudents = presences.stream()
.filter(presence -> Boolean.TRUE.equals(presence.confirmed))
.map(presence -> presence.student)
.toList();
return new ClassRoomDetailResponse(
classroom.startTime.toString(),
presences.size(),
presentStudents.stream().map(student -> student.name).collect(Collectors.toList()));
});
}
}
The getClassDetails method receives the class ID to fetch lessons and student attendance, combining all the information before returning the response.
By using Uni.combine().all().unis(), it allows the combination of results from multiple asynchronous queries. This happens non-blockingly: while the queries are being processed, the thread remains available for other tasks. Only when all operations are completed are the results combined, and the response generated. This ensures the system remains efficient and scalable, even with multiple requests happening simultaneously.
In the code, Uni is similar to a Promise in JavaScript, representing a value that will be provided in the future. When ClassEntity.findById(classId) is called, the database doesn't return the data immediately. Instead, it returns a Uni, which will be "resolved" once the data is ready. During this time, the code continues executing other tasks without being blocked. Once the result is available, the Uni triggers the next step of processing, as defined in .onItem().transformToUni(...).
Besides Uni, which handles a single value, there's also Multi, which is similar to Observable in other reactive libraries. Multi handles a collection of values emitted over time. In my next article on reactivity, I'll discuss Multi, along with Project Loom and RxJava. Topics like WebFlux will be addressed in future articles.
You can check out the project on GitHub through this link Github.
Top comments (0)