What is Asynchronous Programming?
When you execute your code, the OS creates and assigns a thread where the application runs. This is called the Main Thread or the UI Thread.
When an intensive task such as reading a file or a database call happens and the main thread waits for the operation to complete, it freezes the application making it unavailable for other requests.
Asynchronous programming is a paradigm that solves this issue by letting the main thread to run these blocking operations in the background without making the main thread wait for it.
The main thread is notified when the background operation is complete so that it can resume from where it has split.
It does it in 3 simple steps –
- The main thread starts an asynchronous task and gives the blocking operation to it.
- It doesn’t wait for the result and moves on with the other requests or code.
- When the asynchronous task is complete, the main thread is notified of the result and the main thread then resumes the execution.
Advantages of Asynchronous Programming
Non-blocking Operations
as explained above, the main thread doesn’t stay blocked waiting for an operation to complete. that means it can handle other HTTP requests while the blocking operation is happening in the background, separated from the main thread.
Better Resource Utilization
Because the execution is quickly moved to the background and are short spanned when compared to the main thread, it provides an opportunity for thread resuability. the need for large thread pool for handling load is reduced as threads are reused for other tasks while waiting for I/O.
Improved Scalability
Since the main thread is quickly relieved of its load because of the background mode of operation, it enables the application to handle high load of requests without being strained.
when you have the main thread free from blocking operations and all background threads are reusable, you have an application that can easily scale with the load.
Responsive APIs
as mentioned above, when API endpoints are written to serve in asynchronous fashion, it helps for responsive endpoints.
Avoiding Thread Contention
when multiple services or processes try to access a shared resource, it results in a situation called thread retention. in environments with limited resources, asynchronous processing will help keep utilization at minimum and helps keep application responsive, avoiding such situations.
But that doesn’t mean asynchronous programming is the way to go. It also has its own concerns:
Issues with using Asynchronous Programming
- When the application has a very low traffic or concurrent request handling requirements, the benefits of asynchronous programming doesn’t make much difference.
- Due to its concurrent nature of execution, exception handling becomes complex
- When we are dealing with applications requiring strict transaction management, async makes it hard to maintain.
- If the application requires a predictable execution flow or an expected response time, asynchronous programming doesn’t make any sense.
When to use Asynchronous Programming?
- Applications with high traffic (e.g., public APIs or e-commerce sites).
- Micro services architecture where services communicate asynchronously.
- Applications with mixed workloads (e.g., some requests involve long-running tasks, while others are lightweight).
- Systems that need to integrate with reactive frameworks or message brokers (e.g., Kafka, RabbitMQ).
When not to use Asynchronous Programming?
- Low traffic applications
- CRUD applications with strict transaction management requirements
- Simple applications where ease of troubleshooting and a predictable execution flow is desired over performance
Asynchronous Programming in Java – CompletableFuture Example
In Java, we can implement asynchronous programming in multiple ways, such as –
- Completable Futures
- Executor Services
- Reactive Programming (such as RxJava)
For example, we can build a simple REST API using Spring Boot and Java that serves requests asynchronously as below –
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name;
private String email;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
import org.springframework.data.jpa.repository.JpaRepository;
public interface StudentRepository extends JpaRepository<Student, Long> {}
import java.util.concurrent.CompletableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StudentService {
@Autowired private StudentRepository studentRepository;
public CompletableFuture<Student> saveStudentAsync(Student student) {
return CompletableFuture.supplyAsync(() -> {
studentRepository.save(student);
// save some files to storage
saveStudentDocuments(student);
});
}
private void saveStudentDocuments(Student student) {
// do some stuff that takes really loong time
Thread.sleep(10000);
}
}
Let’s say if the Student save operation takes some time due to saving documents to some storage, the web application doesn’t wait for the operation to complete and instead moves on to other requests to handle.
When the operation is complete, the thenApply()
method is called which returns the result as response.
import java.util.concurrent.CompletableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/students")
public class StudentController {
@Autowired private StudentService studentService;
@PostMapping
public CompletableFuture<ResponseEntity<String>> createStudent(
@RequestBody Student student) {
return studentService.saveStudentAsync(student).thenApply(savedStudent
-> ResponseEntity.ok("Student saved with ID: " + savedStudent.getId()));
}
}
Summary
Asynchronous programming prevents application freezes caused by long-running tasks by offloading them to background threads. This improves responsiveness, resource utilization, and scalability, especially in high-traffic applications.
However, it adds complexity to exception handling and isn’t suitable for all applications, particularly those with low traffic or strict transaction requirements.
Java offers several ways to implement asynchronous programming, including CompletableFuture and ExecutorServices, as illustrated by the Spring Boot example above.