DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Comparing the Efficiency of a Spring Boot Project to a Go Project
  • The Startup Time Trick Hiding Inside Your Docker Build
  • A Spring Boot App With Half the Startup Time
  • Optimizing Java Applications for Arm64 in the Cloud

Trending

  • Kill the Worker, Keep the Research: Build a Recoverable LangGraph Agent on Temporal
  • Building Enterprise File-Heavy AI Workflows: From Secure Uploads to Governed Document Intelligence
  • Understand the Sidecar Pattern by Deploying n8n to AWS Fargate
  • AI Agents for Data Warehousing
  1. DZone
  2. Coding
  3. Java
  4. Going All In On the JVM 25 Virtual Threads

Going All In On the JVM 25 Virtual Threads

Learn how JDK 25 virtual threads let Spring applications replace reactive APIs with blocking code for simpler development, debugging, and efficient scaling.

By 
Sven Loesekann user avatar
Sven Loesekann
·
Oct. 14, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
167 Views

Join the DZone community and get the full member experience.

Join For Free

The project AngularAndSpring has been refactored from Spring Reactor to Spring blocking APIs using JDK 25 and virtual threads.

The Old Architecture

The reason AngularAndSpring was built using Spring Reactor was primarily the efficient use of operating system threads and memory under high load. Spring Reactor had a limited thread pool (usually the number of available CPU cores) and used OS threads only when the CPU had work to do. It did not let the operating system threads sleep. This used CPU resources much better and required far fewer OS threads, each of which needs around a megabyte of memory.

The trade-off was a reactive programming style, which can become harder to read as the complexity of the implemented functionality grows, along with stack traces that are very difficult to read. These hard-to-read stack traces make finding and fixing errors much harder and significantly more time-consuming.

How Virtual Threads Change the Trade-Off

Before JDK 25, the choice was between efficient hardware use with Spring Reactor and easy-to-read code with easier debugging and readable stack traces.

Spring's support for Java virtual threads offers the use of very efficient virtual threads for request processing. With virtual threads, operating system threads are used only when processing is needed; virtual threads are unmounted from the OS thread, freeing the OS thread to run a different virtual thread; and virtual threads have minimal memory requirements. That means a limited number of operating threads is able to handle a very large number of virtual threads.

With JDK 25 and the virtual threads update, the choice has changed:

JDK 25 with virtual threads uses hardware resources efficiently, and you can keep the code easy to read and debug with readable stack traces.

There are limitations for virtual threads when native code is called, but the same limitations apply for Spring Reactor too. 

The main virtual threads blocker: The missing synchronized support was fixed in JDK 25.

That means the main advantages of Spring Reactor are now provided by the JVM with its virtual threads.

The New Architecture

The basic building blocks of the new architecture can be found in the backend pom.xml. It now uses these starters:

XML
 
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>


The virtual threads are switched on with this in the application.properties:                       

Properties files
 
spring.threads.virtual.enabled=true


The spring-boot-starter-webmvc is used to provide REST endpoints with blocking APIs. The spring-boot-starter-restclient is used to provide the new REST client for calling the blocking REST endpoints of other systems. The spring-boot-starter-data-mongodb is used to provide repositories and APIs for accessing MongoDB; all of these APIs are blocking.

The ‘spring.threads.virtual.enabled=true’ enables the Spring Boot support for virtual threads provided by the JVM. 

The REST Endpoint

The blocking REST endpoints now look like this in BitstampController:

Java
 
@RestController
@RequestMapping("/bitstamp")
public class BitstampController {
	private final BitstampService bitstampService;
	
	public BitstampController(BitstampService bitstampService) {
		this.bitstampService = bitstampService;
	}
	
	@GetMapping("/{currpair}/orderbook")
	public String getOrderbook(@PathVariable String currpair) {				
		return this.bitstampService.getOrderbook(currpair);
	}
    ...
}


These endpoints do not return the reactor wrapper types. It just returns the intended return type. The ‘BitstampService’ methods now also return the intended return types without any wrappers. These endpoints just use plain Java.

The Restclient

The new blocking RestClient is now the Spring RestClient and looks like this in ScheduledTask:

Java
 
@Component
public class ScheduledTask {
	private static final Logger LOG = LoggerFactory.getLogger(ScheduledTask.class);
	private static final String URLBS = "https://www.bitstamp.net/api";
	private static final String URLCB = "https://api.coinbase.com/v2";
	private static final String URLBF = "https://api.bitfinex.com";
	private final BitstampService bitstampService;
	private final BitfinexService bitfinexService;
	private final CoinbaseService coinbaseService;
	private final MyUserService myUserService;
	private final RestClient.Builder restClientBuilder;
	public ScheduledTask(BitstampService bitstampService, MyUserService myUserService,
			BitfinexService bitfinexService, CoinbaseService coinbaseService, RestClient.Builder restClientBuilder) {
		this.bitstampService = bitstampService;
		this.bitfinexService = bitfinexService;
		this.coinbaseService = coinbaseService;
		this.myUserService = myUserService;
		this.restClientBuilder = restClientBuilder;
	}
    ...
    	@Async("clientTaskExecutor")
	@Scheduled(fixedRate = 60000, initialDelay = 3000)
	@SchedulerLock(name = "BitstampQuoteBTC_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S")
	public void insertBitstampQuoteBTC() {
		String currPair = "btceur";
		insertBsQuote(currPair);
	}
	private ClientHttpRequestFactory customRequestFactory() {
		HttpClientSettings settings = HttpClientSettings.defaults()
				.withConnectTimeout(Duration.ofSeconds(4))
				.withReadTimeout(Duration.ofSeconds(5));
		return ClientHttpRequestFactoryBuilder.detect().build(settings);
	}
	private void insertBsQuote(String currPair) {
		LocalTime start = LocalTime.now();
		try {
			QuoteBs quote = this.restClientBuilder.requestFactory(this.customRequestFactory()).build().get()
					.uri(String.format("%s/v2/ticker/%s/", ScheduledTask.URLBS, currPair))
					.accept(MediaType.APPLICATION_JSON).retrieve().body(QuoteBs.class);
			quote.setPair(currPair);
			quote = this.limitPrecision(quote);
			this.bitstampService.insertQuote(quote);
			this.logDuration("Bitstamp", currPair, start);
		} catch (Exception e) {
			LOG.warn(String.format("Bitstamp data import failed for: %s", currPair), e);
		}
	}
    ...
}


The RestClient is created using the RestClientBuilder to allow connect and read timeouts to be configured. The timeouts are created in the customRequestFactory() method and set as a parameter of the RestClientBuilder.

The RestClient uses the .uri(...) method to set the URL to call. The .accept(...) method sets the MediaType in the HTTP header that is expected from the REST client. The .retrieve() method executes the REST call, and the .body(...) method specifies the object into which the JSON response should be mapped.

For debugging purposes, it can be useful to map the response body to a String first and analyze the response to ensure that it can be mapped correctly to the appropriate DTOs.

The MongoDB Client

For accessing the database, Spring Data MongoDB is used. The per-minute quotes are in the QuoteBs, QuoteBf, QuoteCb entities. The per-hour quotes are in the QuoteHourBs, QuoteHourBf, QuoteHourCb entities, and the per-day quotes are in the QuoteDayBs, QuoteDayBf, QuoteDayCb entities. 

The different entities enable Spring Data MongoDB support for each entity with its own repository. This is an example of QuoteBsMongoRepository:

Java
 
public interface QuoteBsMongoRepository extends MongoRepository<QuoteBs, ObjectId>,
		QuoteBsRepository {
    ...
}


The Interface ‘MongoRepository<QuoteBs, ObjectId>’ provides standard interfaces and enables the generated CRUD methods in QuoteBsRepository.

Java
 
public interface QuoteBsRepository extends QuoteRepository<QuoteBs>, QuoteMongoRepository<QuoteBs> {
    ...
    QuoteBs insert(QuoteBs quote);
    
    List<QuoteBs> findByPairAndCreatedAtAfterOrderByCreatedAtAsc(String pair, Date date, Limit limit);

	@Query("{ 'createdAt': { '$gt': ?0, '$lt': ?1 } }")
	List<QuoteBs> findByCreatedAtGreaterThanAndCreatedAtLessThan(Date from, Date to);
}


This interface uses the by-method-name-generated CRUD methods that Spring Data MongoDB provides and uses the ‘@Query’ annotation for a finder that can not be generated only by a method name.

The entity QuoteBs looks like this:

Java
 
@Document
public class QuoteBs implements Quote,QuotePairBs {
	
	@Id
	private ObjectId _id;
	@NotBlank
	@Indexed
	@JsonProperty
	private String pair;
	@NotNull
	@Indexed(name = "QuoteBs-createdAt")
	@JsonProperty
	private Date createdAt = new Date();
	private BigDecimal high;
	private BigDecimal last;
	private Date timestamp;
	private BigDecimal bid;
	private BigDecimal vwap;
	private BigDecimal volume;
	private BigDecimal low;
	private BigDecimal ask;
	private BigDecimal open;


The ‘@Document’ annotation marks the class as a MongoDB document. The ‘QuotePairBs’ interface is used to be able to access the common properties of minute, hour, and day entities of the same source. The ‘@Indexed’ annotation makes sure an index is created for this property in MongoDB for faster access and ordering. 

The precision of the Java BigDecimal type is higher than the precision of the MongoDB number type. Because of that, this method in ScheduledTask is used:

Java
 
private <T> T limitPrecision(T quote) {
        var fields = quote.getClass().getDeclaredFields();
        for(Field field : fields) {
            if(field.getType().isAssignableFrom(BigDecimal.class)) {
                field.setAccessible(true);
                try {
                    var value = (BigDecimal) field.get(quote);
                    value = value.round(new MathContext(30, RoundingMode.HALF_UP));
                    field.set(quote, value);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return quote;
    }


The precision is limited to 30 decimals for all properties with the type BigDecimal. This is enough for all use cases in this application.

Thread Pools

If the hour and day quotes are recalculated from a large number of minute quotes, this can take several minutes. To avoid competing with the thread pool handling the user requests, a separate thread pool is created for this task in SchedulingConfig:

Java
 
@Bean(name = "clientTaskExecutor")
public Executor threadPoolTaskExecutor() {
	ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
	executor.setVirtualThreads(true);
	executor.setThreadNamePrefix("clientTask-");
	return executor;
}


This creates a separate TaskExecutor that can be used in the ‘@Async(“clientTaskExecutor”)’ annotations. That makes sure that no user requests wait in the queue for a free thread while long-running threads are at work.

Conclusion

With virtual threads in JVM 25 and Spring’s support for them, many reasons for using reactive programming in Spring have disappeared. The synchronized issue with virtual threads has been fixed in JDK 25, and the remaining causes of thread pinning are not relevant anymore for most use cases.

The JVM now provides scalability and efficiency of reactive frameworks out of the box, while the blocking programming model is easier to implement and debug. Spring Data MongoDB support makes database access much easier with generated CRUD methods and the ‘@Query(…)’ support. It is time for projects still using reactive programming to move to an easier, faster programming model for developers.

Java Development Kit Java virtual machine Spring Boot

Opinions expressed by DZone contributors are their own.

Related

  • Comparing the Efficiency of a Spring Boot Project to a Go Project
  • The Startup Time Trick Hiding Inside Your Docker Build
  • A Spring Boot App With Half the Startup Time
  • Optimizing Java Applications for Arm64 in the Cloud

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook