Banner on asynchronous programming in Java for scalable apps

Asynchronous programming, or async programming, is the programming method used to allow a program to start an activity and continue with others while the former goes on, unlike synchronous or blocking operations. This concept is essential in understanding non-blocking I/O in Java, especially when building scalable, reactive services.

For teams looking to adopt or scale this approach efficiently, it’s often valuable to hire Java concurrency experts who can architect, optimize, and troubleshoot complex async workflows.

Why is Async Programming Used?

Comparison of synchronous and asynchronous Java programming patterns

In traditional (synchronous) programming:

  • Each task blocks the program until it completes.
  • If one task is slow (e.g., a network request), it delays all other tasks.

In asynchronous programming in Java:

  • Slow operations (like reading a file, calling an API, or querying a database) are non-blocking.
  • The program continues to run and is notified when the slow operation is complete.

This is a key aspect of Java asynchronous programming and is widely adopted in Java reactive programming models.

Real-World Analogy

Imagine you’re cooking dinner:

  • You put rice on the stove (it takes 15 minutes).
  • While it’s cooking, you chop vegetables, set the table, and boil water.
  • You’re not waiting idly; this mirrors asynchronous task execution.

This is asynchronous behavior; you’re handling multiple things without waiting for each one to finish before starting the next. It’s much like asynchronous task execution in real-world programming.

In Programming Terms

In many languages, async programming is supported using:

  • Callbacks (e.g., JavaScript)
  • Futures/Promises (e.g., Java, Python, JavaScript)
  • async/await keywords (e.g., Python, JavaScript, C#, Kotlin)
  • CompletableFuture in Java
 Java development services for async performance and concurrency

Asynchronous programming might sound complex at first, but let’s make it simple, fun, and tasty with pizza and juice! This blog is for Java concurrency for beginners, and we’ll walk through Java’s async tools step by step, using real-life metaphors.

The Situation

You’re hungry. You order a pizza.

  • Do you just stand at the door and wait?
  • Or do you go do other things while it’s being made?

This is the difference between blocking and asynchronous programming in Java.

Blocking Way (No Async)

/* Blocking way */
String pizza = orderPizza();  // You just wait
System.out.println("Pizza is here: " + pizza);Code language: JavaScript (javascript)

You stand at the door doing nothing until the pizza arrives.

Flow Diagram: Blocking Way

Diagram illustrating blocking task flow in synchronous Java execution

 Async Way with CompletableFuture

CompletableFuture<String> pizzaFuture = CompletableFuture.supplyAsync(() -> orderPizza());
System.out.println("While pizza is coming, I’m watching TV...");

String pizza = pizzaFuture.get();  // Wait only when needed
System.out.println("Pizza is here: " + pizza);Code language: JavaScript (javascript)

What’s happening?

  • You place the order (supplyAsync())
  • You go watch TV
  • You wait (get()) only when the pizza is supposed to arrive

Flow Diagram: Async Way with CompletableFuture

Diagram showing async flow using CompletableFuture in Java

ExecutorService = Your Delivery Team

The pizza is being delivered by your custom team (not the default one).

ExecutorService deliveryTeam = Executors.newFixedThreadPool(2);

CompletableFuture<String> pizzaFuture = CompletableFuture.supplyAsync(() -> orderPizza(), deliveryTeam);
System.out.println("Doing dishes while waiting for pizza...");

String pizza = pizzaFuture.get();
System.out.println("Pizza is here: " + pizza);

deliveryTeam.shutdown();
Code language: JavaScript (javascript)
Java TermPizza Example
CompletableFuturePizza receipt – it’s coming later
supplyAsync()You call and place an order
get()You open the door when the bell rings
ExecutorServiceYour delivery guys (not the pizza shop’s default team)

Pizza and Juice in Parallel (Hands-on Code!)

A small Java program where you order pizza and also make juice in parallel makes async clearer.

This is a practical Java multithreading tutorial showing how to:

1. Order pizza (takes 3 seconds)

2. Make juice (takes 2 seconds)

Do both in parallel using CompletableFuture, demonstrating java parallel stream vs async execution behavior.

import java.util.concurrent.*;
public class PizzaAndJuice {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        CompletableFuture<String> pizzaFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("Ordering pizza...");
            sleep(3000);
            return "Pizza is ready!";
        }, executor);

        CompletableFuture<String> juiceFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("Making juice...");
            sleep(2000);
            return "Juice is ready!";
        }, executor);

        System.out.println("I’m cleaning the table while waiting...");

        String pizza = pizzaFuture.get();
        String juice = juiceFuture.get();

        System.out.println(pizza);
        System.out.println(juice);
        System.out.println("Let's eat and drink!");

        executor.shutdown();
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}Code language: JavaScript (javascript)

Sample Output:

Ordering pizza. . .

Making juice . . .

I’m cleaning the table while waiting . . .

Juice is ready!

Pizza is ready!

Let’s eat and drink!

What does this show?

  • Pizza and juice are prepared at the same time, not one after the other.
  • You’re not wasting time waiting, you can clean the table meanwhile.
  • This is how asynchronous programming helps speed things up in real life and software!

Adding Error Handling & Timeout

Let’s now take this a step further by adding:

1. Error handling – What if pizza delivery fails?

2. Timeout – What if the pizza takes too long?

Error handling

CompletableFuture<String> pizzaFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println("Ordering pizza...");
    sleep(3000);
    if (Math.random() < 0.5) throw new RuntimeException("Pizza shop is closed!");
    return "Pizza is ready!";
}).exceptionally(ex -> {
    System.out.println("Error: " + ex.getMessage());
    return "No pizza today ";
});Code language: JavaScript (javascript)

Timeout Handling

try {
    String pizza = pizzaFuture.get(2, TimeUnit.SECONDS);
    System.out.println(pizza);
} catch (TimeoutException e) {
    System.out.println("Pizza took too long! Canceling order.");
    pizzaFuture.cancel(true);
} catch (Exception e) {
    System.out.println("Something went wrong: " + e.getMessage());
}Code language: JavaScript (javascript)

Sample Output 1 (Success within time):

Ordering pizza. . .

Pizza is ready!

Sample Output 2 (Shop closed):

Ordering pizza. . .

Error: Pizza shop is closed!

No pizza today 

Sample Output 3 (Timeout):

Ordering pizza. . .

Pizza took too long! Canceling order.

Spring Boot Integration

We call GET /order

It will:

1. Order pizza (async)

2. Make juice (async)

3. Wait for both to finish

Step-by-step: Async Pizza + Juice in Spring

1. pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>Code language: HTML, XML (xml)

2. AsyncPizzaApp.java

@SpringBootApplication
public class AsyncPizzaApp {
    public static void main(String[] args) {
        SpringApplication.run(AsyncPizzaApp.class, args);
    }
}Code language: PHP (php)

3. PizzaService.java

@Service
public class PizzaService {

    public CompletableFuture<String> orderPizza() {
        return CompletableFuture.supplyAsync(() -> {
            sleep(3000);
            return "Pizza is ready!";
        });
    }

    public CompletableFuture<String> makeJuice() {
        return CompletableFuture.supplyAsync(() -> {
            sleep(2000);
            return "Juice is ready!";
        });
    }

    private void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { }
    }
}Code language: PHP (php)

4. OrderController.java

@RestController
public class OrderController {

    private final PizzaService pizzaService;

    public OrderController(PizzaService pizzaService) {
        this.pizzaService = pizzaService;
    }

    @GetMapping("/order")
    public String orderFood() throws Exception {
        CompletableFuture<String> pizzaFuture = pizzaService.orderPizza();
        CompletableFuture<String> juiceFuture = pizzaService.makeJuice();

        CompletableFuture.allOf(pizzaFuture, juiceFuture).join();

        return pizzaFuture.get() + " & " + juiceFuture.get() + " — Enjoy your meal!";
    }
}Code language: PHP (php)

Run it!

http://localhost:8080/order 

Sample Output

Pizza is ready! & Juice is ready! Enjoy your meal!

Highlights:

ComponentPurpose
PizzaServiceContains async tasks (pizza, juice)
CompletableFutureRuns tasks in parallel
OrderControllerCalls both services, waits, and returns the result
CompletableFuture.allOf()Waits for both tasks to complete

Advanced: Timeout, Error Handling, Custom Thread Pool

  1. Timeout – Cancel pizza if it takes too long
  2. Exception handling – Handle pizza shop failures
  3. Custom thread pool – For better performance and control
FeatureTool Used
Async tasksCompletableFuture
Parallel executionCustom ExecutorService
TimeoutcompleteOnTimeout()
Error handlingexceptionally()

1. AsyncConfig.java

@Configuration
public class AsyncConfig {
    @Bean
    public ExecutorService customExecutor() {
        return Executors.newFixedThreadPool(5);
    }
}Code language: CSS (css)

2. PizzaService.java

@Service
public class PizzaService {

    private final ExecutorService executor;

    public PizzaService(ExecutorService executor) {
        this.executor = executor;
    }

    public CompletableFuture<String> orderPizza() {
        return CompletableFuture.supplyAsync(() -> {
            sleep(3000);
            if (Math.random() < 0.5) throw new RuntimeException("Pizza oven broke!");
            return "Pizza is ready!";
        }, executor).completeOnTimeout("Pizza took too long, canceled! ", 2, TimeUnit.SECONDS)
          .exceptionally(ex -> "Pizza failed: " + ex.getMessage());
    }

    public CompletableFuture<String> makeJuice() {
        return CompletableFuture.supplyAsync(() -> {
            sleep(2000);
            return "Juice is ready!";
        }, executor);
    }

    private void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { }
    }
}
Code language: PHP (php)

3. FoodResponse.java

public class FoodResponse {
    private String pizza;
    private String juice;
    private String message;

    // Constructors, Getters, Setters
}Code language: PHP (php)

4. OrderController.java

@RestController
public class OrderController {

    private final PizzaService pizzaService;

    public OrderController(PizzaService pizzaService) {
        this.pizzaService = pizzaService;
    }

    @GetMapping("/order")
    public FoodResponse orderFood() throws Exception {
        CompletableFuture<String> pizzaFuture = pizzaService.orderPizza();
        CompletableFuture<String> juiceFuture = pizzaService.makeJuice();

        CompletableFuture.allOf(pizzaFuture, juiceFuture).join();

        return new FoodResponse(pizzaFuture.get(), juiceFuture.get(), "Enjoy your meal!");
    }
}
Code language: PHP (php)

5. Application.properties

spring.jackson.serialization.indent-output=trueCode language: JavaScript (javascript)
Sample Output
{
  "pizza": "Pizza is ready!",
  "juice": "Juice is ready!",
  "message": "Enjoy your meal!"
}Code language: JSON / JSON with Comments (json)

Or:

{
  "pizza": "Pizza took too long, canceled! ",
  "juice": "Juice is ready!",
  "message": "Enjoy your meal!"
}Code language: JSON / JSON with Comments (json)

Is This Truly Non-blocking?

 Is the example above (with CompletableFuture) a pattern for a non-blocking API in Spring MVC?

The answer is:

1) Yes, but only partially.

2)  It’s not truly non-blocking from end-to-end in traditional Spring MVC.

Great for speeding up I/O-heavy work like file ops, DB, HTTP calls, or even implementing DevOps for scalable Java applications that require non-blocking backend services.

Let’s break this down using Java concurrency principles:

We used:

  • CompletableFuture.supplyAsync() to run tasks in parallel.
  • Returned a normal JSON response from the controller (FoodResponse).

Benefits:

  • Internal processing is concurrent/asynchronous.
  • Fast, simple, and it works in traditional Spring MVC.
  • Good for speeding up I/O-heavy work like file ops, DB, HTTP calls, etc.

Limitation:

The Servlet thread is still blocked while waiting for pizzaFuture.get() and juiceFuture.get().

So it’s asynchronous internally, but blocking externally.

TRUE Non-blocking APIs in Spring (End-to-End)

To make it fully non-blocking, including the HTTP layer, you need:

1. WebFlux (Reactive Spring)

  • Based on Reactor and Netty, not Tomcat.
  • Uses Mono and Flux instead of CompletableFuture.
  • Truly non-blocking from request to response.
Example:
@GetMapping("/order")
public Mono<FoodResponse> orderFood() {
    Mono<String> pizza = orderPizzaAsync();
    Mono<String> juice = makeJuiceAsync();

    return Mono.zip(pizza, juice)
               .map(tuple -> new FoodResponse(tuple.getT1(), tuple.getT2(), "Enjoy!"));
}
Code language: JavaScript (javascript)

2. Callable or DeferredResult in Spring MVC  (semi-non-blocking)

Spring MVC also supports asynchronous request processing using:

Callable<T> (simple async task)

DeferredResult<T> (more flexible)

Example with Callable:
@GetMapping("/order")
public Callable<FoodResponse> orderAsync() {
    return () -> {
        String pizza = pizzaService.orderPizza().get();
        String juice = pizzaService.makeJuice().get();
        return new FoodResponse(pizza, juice, "Enjoy!");
    };
}Code language: JavaScript (javascript)

This tells the Servlet container (e.g., Tomcat):

  • “Release the thread for now.”
  • “Resume response when the result is ready.”

A clean balance between async vs sync in Java inside web contexts.

PatternInternally AsyncFully Non-blocking at HTTP LevelFramework
CompletableFuture + get()YesNoSpring MVC
Callable / DeferredResultYesYesSpring MVC
Mono / FluxYesYesSpring WebFlux
Summary :
  • If you’re using Spring MVC and want non-blocking APIs:
  • Use Callable or DeferredResult
  • Use WebAsyncTask for timeouts
  • Move to Spring WebFlux only if you need a full reactive stack

To go truly non-blocking, use WebFlux or Spring MVC’s Callable. This is essential if you want to monitor asynchronous Java tasks and performance metrics via OpenTelemetry or distributed tracing systems.

Final Thoughts

  • CompletableFuture makes async programming intuitive.
  • Use ExecutorService for performance control.
  • completeOnTimeout() and exceptionally() = essential for real-world resilience.
  • Spring MVC supports semi-async with Callable; full async needs WebFlux.

Now you can enjoy pizza and parallel programming without blocking!

Absolutely! Let’s replace the playful example with a professional, real-world mini project that demonstrates the practical use of CompletableFuture and asynchronous programming in a Spring Boot application.

Mini Project: Asynchronous Document Processing Service

Real-World Scenario:

Your company provides an API for uploading documents (PDF, Word, etc.) that performs:

  1.  Metadata extraction (e.g., title, author, page count)
  2.  Virus scanning for uploaded files
  3.  Returns the combined result in JSON when both tasks are done

These are independent operations and can be processed in parallel.

Technologies Used

  • Java 11
  • Spring Boot 3.x
  • CompletableFuture with custom ExecutorService
  • RESTful API with Spring Web

Project Structure

src
└── main
    └── java
        └── com.mobisoftinfotech.documentprocessor
            ├── DocumentProcessorApplication.java
            ├── config
            │   └── AsyncConfig.java
            ├── controller
            │   └── DocumentController.java
            ├── dto
            │   └── DocumentResponse.java
            └── service
                └── DocumentService.java
Code language: CSS (css)

Step-by-Step Code

1. Pom.xml (dependencies)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>Code language: HTML, XML (xml)

2. DocumentProcessorApplication.java

package com.example.documentprocessor;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DocumentProcessorApplication {
    public static void main(String[] args) {
        SpringApplication.run(DocumentProcessorApplication.class, args);
    }
}Code language: JavaScript (javascript)

3. Config/AsyncConfig.java

package com.example.documentprocessor.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Configuration
public class AsyncConfig {

    @Bean
    public ExecutorService executorService() {
        return Executors.newFixedThreadPool(5); // For parallel tasks
    }
}Code language: JavaScript (javascript)

4. Dto/DocumentResponse.java

package com.example.documentprocessor.dto;

public class DocumentResponse {
    private String metadata;
    private String virusScanResult;
    private String message;

    public DocumentResponse() {}

    public DocumentResponse(String metadata, String virusScanResult, String message) {
        this.metadata = metadata;
        this.virusScanResult = virusScanResult;
        this.message = message;
    }

    public String getMetadata() { return metadata; }
    public void setMetadata(String metadata) { this.metadata = metadata; }

    public String getVirusScanResult() { return virusScanResult; }
    public void setVirusScanResult(String virusScanResult) { this.virusScanResult = virusScanResult; }

    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
}Code language: JavaScript (javascript)

5. Service/DocumentService.java

package com.example.documentprocessor.service;

import org.springframework.stereotype.Service;

import java.util.concurrent.*;

@Service
public class DocumentService {

    private final ExecutorService executor;

    public DocumentService(ExecutorService executor) {
        this.executor = executor;
    }

    public CompletableFuture<String> extractMetadata(String fileName) {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println("Extracting metadata for " + fileName);
            sleep(3000);
            return "Metadata: Title=Report, Pages=12";
        }, executor).completeOnTimeout("Metadata extraction timeout!", 2, TimeUnit.SECONDS)
          .exceptionally(ex -> "Metadata failed: " + ex.getMessage());
    }

    public CompletableFuture<String> scanForVirus(String fileName) {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println("Scanning for virus: " + fileName);
            sleep(2000);
            if (Math.random() < 0.3) {
                throw new RuntimeException("Virus scan engine error");
            }
            return "Scan Result: No virus found";
        }, executor).completeOnTimeout("Scan timeout!", 3, TimeUnit.SECONDS)
          .exceptionally(ex -> "Scan failed: " + ex.getMessage());
    }



    private void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}Code language: JavaScript (javascript)

6. Controller/DocumentController.java

package com.example.documentprocessor.controller;

import com.example.documentprocessor.dto.DocumentResponse;
import com.example.documentprocessor.service.DocumentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.CompletableFuture;

@RestController
public class DocumentController {

    private final DocumentService documentService;

    public DocumentController(DocumentService documentService) {
        this.documentService = documentService;
    }

    @GetMapping("/process")
    public DocumentResponse processDocument(@RequestParam(defaultValue = "report.pdf") String file) throws Exception {
        CompletableFuture<String> metadataFuture = documentService.extractMetadata(file);
        CompletableFuture<String> scanFuture = documentService.scanForVirus(file);

        CompletableFuture.allOf(metadataFuture, scanFuture).join();

        String metadata = metadataFuture.get();
        String scanResult = scanFuture.get();

        return new DocumentResponse(metadata, scanResult, "Document processed.");
    }
}
Code language: JavaScript (javascript)

7. Application.properties

spring.jackson.serialization.indent-output=trueCode language: JavaScript (javascript)

Run & Test

Start the application and test:

URL: curl “http://localhost:8080/process?file=report.pdf” 

Example Output (success):
{
  "metadata": "Metadata: Title=Report, Pages=12",
  "virusScanResult": "Scan Result: No virus found",
  "message": "Document processed."
}Code language: JSON / JSON with Comments (json)
Example Output (with error):
{
  "metadata": "Metadata: Title=Report, Pages=12",
  "virusScanResult": "Scan failed: Virus scan engine error",
  "message": "Document processed."
}Code language: JSON / JSON with Comments (json)
Step-by-step Java async code for scalable backend execution

Real-World Relevance

This project simulates:

  • File processing pipelines
  • Non-blocking concurrent backend operations
  • Timeout/error recovery in long-running operations (e.g., cloud scans, OCR, antivirus)

These are independent operations and can be processed in parallel, even more so when you secure async API calls in Java to maintain end-to-end data integrity.

Conclusion

Asynchronous programming in Java may seem complex at first, but it becomes a powerful ally once you grasp the core concepts. From basic CompletableFuture usage to integrating async patterns into real-world Spring Boot applications, you now have a strong foundation to build non-blocking I/O in Java, efficient, and scalable services.

By mastering async constructs like CompletableFuture, handling exceptions, managing timeouts, and applying these patterns to production-like scenarios, you’re not just writing faster code; you’re writing smarter code. Concepts such as Java thread management and practical examples like a java executorservice example are critical for building reliable systems.  You can even explore advanced patterns like Java async search integration to enhance your application’s search capabilities.

To explore the full working code,  Mini project setup, check out the complete source on GitHub

Keep experimenting, profiling, and applying these concepts in your projects. That’s how theory turns into mastery with practical Java performance tuning along the way.

Scalable Java solutions for modern asynchronous application needs

Author's Bio

Blog author Rushikesh Keskar on Java async programming
Rushikesh Keskar

Rushikesh Keskar is a Senior Software Engineer at Mobisoft Infotech Pvt Ltd with more than 10 years of experience in backend development. Skilled in Java, Spring Boot, REST APIs, PostgreSQL, and third-party service integrations, he specializes in building scalable and efficient enterprise solutions. His expertise includes REST API development, database and code optimization, and resolving critical production issues.