JCON EUROPE

Virtual Threads, Structured Concurrency & Scoped Values: Putting it All Together | Balkrishna Rawool

47:13 · 20 Apr 2026 – 23 Apr 2026 · YouTube

About this talk

This talk discusses the key features of Project Loom, focusing on virtual threads, structured concurrency, and scoped values. The speaker explains that Project Loom aims to provide high throughput and lightweight concurrency in Java applications. They delve into virtual threads, which are lightweight and require less memory, enabling developers to create thousands or millions without the overhead of thread pooling. The speaker then covers structured concurrency, demonstrating how it allows for more readable concurrent applications by managing task hierarchies effectively. Lastly, the talk explores scoped values as an alternative to thread locals, highlighting their usability in maintaining immutability and inheritance in concurrent tasks. Real-world examples, particularly in a banking context, elucidate the practical application of these concepts in a Spring Boot web application.

Full transcript

[music] >> Okay. Uh, good afternoon. Thank you all for being here. we're going to discuss virtual threads, structured concurrency, and scoped values. Uh, these are the three main features of Project Loom. We will see what these features are and how we can combine them together in uh, a web application. And we'll see also how their APIs nicely fit together. Okay. Now, before uh, I start, I usually

do a quick check in the audience. Uh, this time I'm going to check which JDK versions are you using? So, I'm going to jump all the way to JDK 21. So, please raise your hand if you are using 21 or higher. Again, it can be in your personal project. Okay, that's like maybe 80% of the room. That's really good. Thank you. Um, now, all the people who

raised their hands, they have the possibility to use virtual threads without using the preview flag. So, now raise your hand if you are using virtual threads. Again, even in your personal project, that's also fine. Okay, that's Oh, that's less than 10%. That's actually quite low. It can be higher. Um, but thank you. Thank you for participating in my quick survey. Uh, so let's move on. Uh, and

a bit of uh, housekeeping. So, uh, this is the QR code uh, to the link where you can ask questions. Uh, all throughout this talk you can ask questions and uh, at the end we will look at them. I hope we still have time uh, for those questions. So, I'll keep this slide here for a few seconds more and then you can take a quick uh, what

do you say? Check on the on the link. Uh, you can also find this in in the schedule. Okay. So, let's move on. Uh, let's start with the Project Loom. So, Project loom is actually uh one of the quite important projects at OpenJDK. And the goal of uh project loom is to provide high throughput lightweight concurrency, one which is also easy to use. And it does so

by providing these three features: virtual threads, structured concurrency, and scope values. Now, what we're going to do in this talk is we're going to go through these features one by one. We'll look at what they are, how they work, and we will try to use them in our web application, and we'll see at the end when we have all these features used that how they uh work

together to enable us to provide uh readable concurrent applications. Okay, so let's get started, but first a very quick intro about me. My name is Balakrishna Rahul, and I work for ING Bank. Now, uh start with the first part of this talk. It's called uh virtual So, what we have here is an instance of JVM, which is running on some operating system. Now, the JVM has a

bunch of virtual threads, some platform threads, and the OS has some OS threads or kernel threads. Now, let's start with platform threads first. So, platform threads are actually a thin wrapper over operating system threads. Now, what does that mean? When JVM wants to create a new platform thread, it requests OS to create a new OS thread. And only when that thread can be created, it creates this

platform thread instance. And because of that, these platform threads, they map to the OS threads with this one-to-one mapping. Now, actually creating a platform thread is a resource-intensive task. You don't want to do that too often. So, when you are done with the platform thread, you don't throw it away, you put it in a pool, so that next time when you need it, you can get it

back from that pool. And therefore, uh platform threads require thread pools. Now, virtual threads on the other hand are lightweight user threads. Lightweight meaning they require significantly less memory. And because of that, you can create them in abundance. You can create thousands or even millions of virtual threads and that makes them highly scalable. There is actually one more thing that makes them highly scalable. We'll see that

on the next slide. Um but another thing about virtual threads is that they don't need pooling. Because creating a virtual thread is a cheap operation. When you're done with a virtual thread, you just throw it away. Means you can let it garbage collect and then the next time when you need it, you create another virtual thread. In fact, pooling virtual threads is an anti-pattern. Uh you don't

want to do that. Now, what we see here in this picture is that some of the virtual threads are connected to platform threads. Now, why is that? It is because a virtual thread on its own cannot execute anything. It requires platform thread to execute something. But then what about these other virtual threads which we don't have any platform thread connected to them? Well, for that we have

virtual thread scheduler. This is a newly introduced scheduler introduced along with virtual threads and what it does is it mounts a virtual thread onto a platform thread when a virtual thread requires platform thread and it unmounts it when it doesn't require platform thread. Now, what would be the scenario where virtual thread would not require platform thread? It is when it is waiting for something. Let's say a

virtual thread is waiting for a network resource to be available or database return something. Now, in all these cases, the virtual thread does not require any processing and therefore it gets unmounted from the platform Now, in a typical IO bound application, threads spend a lot of time waiting. And all of that waiting can be avoided if we use virtual threads. Meaning only your virtual threads would be

waiting, but not the underlying platform threads. in this entire endeavor of mounting and mounting, there are some platform threads which are carrying these virtual Uh and we also call them carrier these virtual threads like because uh um they can be mounted and unmounted from platform threads, and then the platform uh are always busy doing some of the other thing, processing some of the other virtual thread. And

therefore, your CPU is highly efficiently utilized, and that is what makes virtual threads Okay, now that is a lot of theory. Now, let's see some virtual threads in action. So, what we're going to do is we're going to create a web application and we will add some virtual threads to it. So, what we will do is we'll create a Spring Boot application, more specifically Spring Boot 4.0.

Um and we will use JDK 26. Now, the domain of the application is going to be banking because it's also close to me. Uh and I'm really good with names, so I'm going to call my bank a bank. Uh okay, now let's head over to IntelliJ and then see uh how does it work. So, what I have here is Spring Boot Loom, which is a Maven project.

And it has two Maven modules, a bank and services. Now, both of these are also Spring Boot Uh A bank is going to have all the logic that we're going to create and it is going to use services from the services module. And we'll explore them as we go on. Now, for the first demo, I'm going to create something which is not related to banking. Uh it's

going to be a controller called hello controller. Uh this will be a uh uh rest controller and it's going to have a method it's going to return string called hello and just going to say hello. Uh and this is available to us as a get endpoint So, now let me start this application uh send a request to it. So, everything works fine. We should get hello back.

Uh, should be localhost. Uh, let me check what is the port uh, this application is running on. So, I can look in the application properties. I see the port 8082. And then I'm going to say hello. So, let's see if we get a Yeah, we get hello back. So, everything is working fine. We send a request to our application. It creates a new thread which uh, processes

our request and sends hello back. Now, let's check what thread it is creating. All right, for that I'm going to add something here. I'm going to say thread.current thread. Um, and then send the same request again and see what happens then. What do we get back? We get back hello from a thread. So, this is actually a platform thread. Right? So, with our request, the server is

either creating a new platform thread or using it from thread pool and then executing our request there. But, can we change from platform thread to virtual threads? Well, with Spring Boot it it is actually quite easy. We can simply add a new property here, spring.threads. dot virtual.enabled. Set it to true. Uh, and then it starts using virtual threads. So, it actually uses virtual thread uh, executor. Uh,

so when we send a new request, uh, we create a new virtual thread and then that processes our request. So, this Tomcat handler zero is actually a Okay. So, with this simple flag, we have actually transformed the application from using platform threads to virtual threads and that gives us quite a lot of power. What what we can do with it can actually use structured concurrency. So, that

is the second part of our talk. We'll discuss what is structured concurrency and then we'll try to use that in our application. Okay. Starting with uh, first thing, virtual threads enable structured concurrency. How? So, normally with platform threads, you have the limitation that you cannot create too many of them, right? So, what you do is you create few platform threads and you use them very efficiently. In

fact, you view a platform thread as an abstraction of a process and then you submit multiple tasks to it. Right? But, that limitation is not there with virtual threads. You can create them in large numbers. So many so that you can actually create a new virtual thread for every task that you do. And then you can uh structure these virtual threads, structure these tasks in such a

way that reflects our business logic in the most logical way. Right? And that is what structured concurrency is and that is how virtual In fact, the term structured concurrency is inspired from the term structured programming. The way structured programming enables us to write readable programs, structured concurrency enables us to write readable concurrent programs. Uh let's look at some examples. So, what we have here is a main

task which is creating three subtasks. And when all these tasks are finished, we are continuing with our main task. Another example. Again, here we are creating three subtasks, but we are not waiting for all of them to finish. As soon as first task finishes, we continue A bit more complicated requirements. So, in this example, we have three subtasks, but when tasks two and three are finished, we

create fourth subtask. And when one and four are finished, we continue with our main task. Now, these are different concurrency requirements, examples. We can implement all of these using structured concurrency More specifically, we're going to use structured task scope to uh implement one of these examples. So, later on uh in the talk, we'll uh have some requirements, concurrency requirements, and we'll implement them using structured task scope.

Okay, a bit more theory about structured concurrency. So, the principle of structured concurrency is that when flow of execution splits into multiple concurrent flows, they rejoin in the same code block. For example, if you if you we were implement this example where we have three sub tasks being forked and then again all the three sub tasks are joined, that forking and join happens in And that gives

uh some benefits. First one is that uh error handling with short-circuiting. What does it mean? So here we have to wait for all the three sub tasks to finish before we continue with our main What if one of them fails? If one fails, there is no point in continuing the other two. So those are immediately canceled and that is what short-circuiting is. Cancellation propagation. So here uh

if let's say our main task is canceled, now that cancellation propagates to the child tasks and those are canceled as well. And that is what cancellation propagation is. Clarity. So if we implement this example using structured concurrency, we will see that the resulting code reflects this structure. And therefore it becomes easy to read and easy to understand. Brings in a lot of clarity in our uh implementation.

Finally, observability. So what we see here is that we have a main task that's creating three sub tasks. Now those sub tasks can have their own sub tasks and they can have their own sub task. So we can have a big hierarchy of tasks. And because we use a virtual thread for every task, we will have a big hierarchy of virtual threads. And let's say at any

point in time uh we have an issue and we want to get a thread dump of that application, we can get that thread dump in a format that preserves this hierarchy. So we can actually see which virtual thread is creating which other virtual threads. And if there is a problem, we can pinpoint the problem and then fix it. So that is an observability benefit that we get

with structured concurrency. Okay, so now let's explore the structured concurrency API. Uh for that we're going to add a new feature to our application and it's called loan application. So what happens here uh a customer applies for a loan. Uh and the first thing that we do is we get the customer details. And then using the customer details, we get their accounts, their loans, and their credit

scores. Now, to get the credit scores, we have two services, but only one of them is sufficient. So, as long as we get one credit score successfully, that is uh enough for us. And once we have all of this information, uh we have the customer details, accounts, loans, credit scores, everything, we send it to offer calculation service, which gives us an offer. We're going to send that

offer back to the customer. Okay. Now, these requirements, if we uh uh visualize them using task structure, they would look something like this. So, we first get the customer details. That's our first task. Then we create three subtasks to get accounts, loans, and credit scores. And once we have all of them, then we continue and calculate the offer for the customer. Now, here, getting the credit score

is not a single task, but there are two score one and score two. And we don't want to get results from both of them. Only one successful result is enough for us. Okay. Now, let's go ahead and implement this feature using structured concurrency API in our application. Oh, by the way, I didn't ask uh is the font big enough? At the back? Okay, I get a yes.

I had already increased it, so it should be already good. Okay. Uh let me create a new controller uh loan controller. Uh this is again going to be a rest controller, and it will have a method public uh which offers uh an offer. Uh the method is called apply for loan, and it takes apply for loan, and it takes uh loan called request. And for now, let's

say it does nothing. And this is available to us as a post uh called uh loan application and the request is uh request body obviously. Okay, now let's define these DTOs so that we can compile our controller. Uh for that I'm going to add a new interface called model and I'm going to put all my DTOs there in the form of records and I have it in

a live template so I don't spend time writing all this code. So here as you can see we have loan application request, we have offer and all other DTOs and I'm going to use these DTOs all throughout different layers of my application for simplicity. Also, there is an exception called A bank exception. It's a generic exception which handles all different kinds of errors uh in my application.

Okay, so uh let's move on. The first details. That's the first task, right? So let's implement that by using customer service. So I'm going to create new service called customer service and there is quite some boilerplate so I also have a uh live template for this. Uh so as we can see there is a logger here and there is a rest client. Now this rest client uh

is injected in the constructor. Where does it come from? Well, this bean comes from uh A bank application. Um and in the application we have defined this as a rest client which connects to our services app. Okay, now that reminds me that we have to look at the services app. Let's go and quickly take a look there. So this is a services app. What does it have?

It has a customer controller and it exposes many endpoints. So these are used to get the customer details, to get credit scores, to get accounts, and loans, and offer. So basically all the things that we need in our A bank application, they are provided here. Uh so, let me start this application as And now, let's go back to my because that is what we want to implement

now. So, here in the customer service, I have this method get customer, which gets customer details by using this rest client. So, let's implement that. Uh rest client dot get and [snorts] we're going to get the details from customer {slash} ID and it's going to be customer ID and this will retrieve uh customer. And let's put this in a customer variable and then we can basically return

that back. So, when we call get customer, we get the customer details from the services app. Okay, now let's put this as a dependency in loan controller. Uh so, we're going to add uh customer service, all customer and let me inject this uh as a constructor dependency uh and then use it here. So, I'm going to do customer service dot using customer ID and this is going

to give me Okay, now there is something interesting happening here. So, this call get customer, this actually a blocking call. It is going over the network to the services app and fetching customer details from there. Normally, when you have a blocking call, what you do is you put some ceremony around it. There is future or completable future and then you have a callback, which then gets the

response and then processes that response. Now, that is needed because you don't want your platform thread to be blocked, right? And you don't want your CPU to be wasted on a blocked platform thread. But with virtual threads, it is okay to block it because when a virtual thread is blocked uh it doesn't let the No, it gets unmounted and therefore the platform thread is able to have

another virtual thread mounted and then continues processing. So, the platform thread is never blocked. And therefore, CPU is not wasted. And therefore, we don't need that ceremony when you are using virtual threads. So, when you're using virtual threads, you can just make blocking call as if they are non-blocking. Right? And that gives lot of clarity or uh and increases the readability of our code. Okay, so that's

how we get the current customer. And then the next thing is to get accounts, loans, and credit score. Let's ignore credit score for now. Let's implement uh services to get account details and loan details. For that, I'm going to create uh a couple more services here. Uh first one is called account service. And again, I have a live template. It looks very similar to the customer service

that we just uh created. There is a logger, is a rest client, and a method that gets the accounts for the customer using the rest client. Uh and then I'm going to uh implement loan service. Another live template, and it's also very similar to the other services that we just implemented. Uh we have a logger here, rest client. And a method that gets loans details by using

the rest Okay, now let's inject these dependencies in uh loan controller. So, Uh and then we have Uh let's inject them in the constructor. Okay, and then use them here. So, the first thing we'll do is call account service or get account info by using the current customer. And then we'll use loan service to get the loans detail loans details for the current customer. So, now we

are making these two calls, but we want then these two calls to be done concurrently. Right? And for that, Uh more specifically, we will use Now, there is a method here that's called open, and that gives us a new instance of scope. Uh, also, uh, structured task scope is auto-closable. Uh, so we use it with try-with-resources. That way, it takes care of calling the close method, which

frees up resources. So, once we have scope, we want to, uh, make these two calls concurrently. Uh, so what we're going to do is we are going to fork them. So, when we call scope.fork, it actually forks a new subtask. Uh, we we have two subtasks here, so we will fork these two subtasks. Uh, and we'll call them task one and but task one and task two.

Okay, and then the next thing to do is called scope.join. What does join do? It actually ensures that all the subtasks are finished. Uh, again, there is exception handling, uh, here wrap it in an ABank exception. Uh, and then, once join is done, we have ensured that all the subtasks are finished. Now, let's get the results. So, we can do task one.get, and that gives us our

uh, accounts. And then we'll do task two.get, which will give us our loans. Okay, so this way, we use structured concurrency to make these two calls concurrent. Actually, let's take a quick quick look at what we have done here. Uh, so, uh, when we use structured task scope, uh, there actually, uh, it is a four-step process. So, step one is to call open, which gives us a

new scope. Step two is to call fork to fork subtasks, and we can call it as many times as we want to, uh, fork our Step three is to use join, which ensures that all the subtasks are finished. And step four is to get the results from these individual uh, uh subtasks. Okay. Now that this is implemented, let's go ahead and implement get credit score by using

another service. And that will be called service service. Okay. And again, I have a live template because uh it's very similar to other services. Uh there is a logger, rest client, and we have a method that uses the rest client to get the credit score. Uh but in this case, I actually want two from two different services. So, I'm going to use this method twice here. So,

credit score one yeah, for this customer, and then credit score And these two calls, I want to make them concurrently. So, again, let's use structured concurrency. And this time, we know the drill. We start with try with resources. We get a new scope from structured structured task scope dot open. we fork these subtasks. So, I want to do scope dot fork twice. Okay. And then, we do

scope dot join. So, that ensures that all the tasks are finished, and there is exception handling, which we're going to throw a bank exception with. Okay. Now, so we did exactly same as what we did in loan controller. So, it's going to work the same way. Uh that means when we call join, it is going to wait for both the subtasks to finish. But that is not

what we want. We want result from one of them. So, as soon as one task finishes, we want to get that result, and then ignore the other task or cancel the other task. So, how do we do that? Well, by configuring a joiner here. So, we pass uh to the open method we can pass we can pass multiple we can pass joiner and there are many joiners

available. The one that we are going to use is any successful or throw. What this joiner does is it configures the behavior of this join method. And in this case, because it is any successful or throw, it's going to ensure that only one task is finished. So, the moment one task finishes, it actually cancels the execution of the scope and it also returns the result of that

one task because there's only one task finished, right? So, we can actually get this uh as an as a return and we can put it in a score. We can also define the type of this response here. So, this will be credit And we can return that score uh over here. Okay, so we use again structured uh to then concurrently execute these two calls and we also

saw how to configure the joining behavior by passing a joiner. Now, let's use this credit score service in our loan controller. Uh this will be credit score service. And again, inject the dependency. And so, here we're going to be a third Task three. And then we are going to fork credit score service.getcreditscore Okay, and then we can get the result of task three over here, which is

going to be credit score. Okay, now that we have everything, we can continue with calculating the offer. And for that, I'm going to add another method in loan service. Uh it's called calculate offer. And again, it uses the uh REST client to just call the services um endpoint, get the offer, and return. So, let's use that over here. Uh so, it's going to be loan service.calculateoffer and

then we pass everything here, including the customer, accounts, loans, Uh, and then we need amount, which is coming from the request. Purpose also coming from the request. So, what we have now is an offer, return that offer from here. Okay, so we have now implemented our method, the loan application endpoint. Let's now restart our A bank application, and then test to see if it works the way

we want. Uh, so I have actually the request already here. Uh, so what is this doing? It's actually going to the loan application, sending all the details to get the offer back. Let's see how it works. When we send the request, let's go and check what is happening in the A bank application. So, we see that the request is received, and it's trying to get the customer

Once that is there, it sends request for the loans details, credit scores, accounts details. Uh, credit score two is finished. So, one is finished, and therefore that is enough. Credit score one uh, request is cancelled. And then we have loans details received, accounts details received, everything is received. So, we make the call to the calculate offer, and once we have the offer, we should see that offer

in our console, like this. Okay, so it actually works the way we want. So, let's take a quick look uh maybe let's take a look at the logs a bit. Because what we see here is that we have all these subtasks getting executed in different virtual threads. That is what we see here. And that is what the structured task scope API does. So, um what we did

here is that we looked at how structured task scope can be used, and we understood that it's a Uh, secondly, what uh we can we can also visualize the different the benefits that we get with structured concurrency. Uh, as we mentioned earlier, that it brings clarity. Now, in this implementation, we this actually resembles quite a lot to the structure that we had in mind. Right? So, it

becomes the code becomes I have actually same implementation, but using completable future in a different branch, and I will show it here. So, here it uses completable future. It has exact same behavior. The only difference is it uses completable future, but the thing with completable future is that the moment you use it, it sort of takes over your This appears in almost all the method signatures whenever

you want to deal with it. Secondly, it uses callbacks. So, the code is not as readable as using structured concurrency. Also, debugging is a bit more difficult. So, this shows the difference between completable future implementation and structured concurrency implementation, and also shows that it brings in a clarity. Now, let's see if we can see the difference see the benefits of uh of uh uh cancellation propagation Yeah,

let's for that I'm going to uh what I'm going to do is I'm going to uh create a failure scenario. So, in the customer controller, I am going to say that our accounts uh endpoint is failing. So, this is not going to return something, but it is going to throw new uh runtime exception. Error. And let me restart my services Um and then we will going to

send the same request again. Uh so, this time we should see that our uh service first of all should fail. Uh but let's see what happens with the other task. So, actually when uh account service fails, it should immediately cancel uh loan details and credit score. Uh let's see if that is what happens. So, there is a failure, which is good. Uh so, we have now uh

an account center which is failing, and we have we we can see here that it actually had started calling these subtasks, uh calling these services, but the moment one fails, it actually stops everything, and that is because it immediately cancels the loan details and credit score service. So, we see that uh cancellation, and then that cancellation also propagates to the uh And then these two subtasks are

also canceled. So, the benefits that we discussed earlier with structured concurrency, we can actually see them. And because of concurrency, there are many things that can go wrong, and I will share the link to the source code, so you can try different error scenarios as well, and see how it behaves. For now, I'm just going to quickly uh restore this. So, I need my services app to

work fine for the rest of the demo. Okay, so that's about uh structured concurrency, and then the third feature of Project Loom, and that is scoped Scoped values is actually an uh better alternative to thread locals. So, let's first quickly discuss uh what is thread local. When you have a variable defined as thread local, what it does is it actually creates a new copy for every thread.

So, every thread gets its own copy of that thread local object. Um and that is quite useful in some scenarios. For example, for for storing request or session specific data. So, let's say your server is operating in uh thread per request model. So, what you do is you create a new thread for every request that you receive. So, then you can actually use thread local to store

request or session Secondly, for resource management, uh for example, for non-thread-safe objects. So, if you have a class which is not thread safe, you can wrap it in a thread local, so every thread gets its gets its own copy, and therefore the problems of the class being non-thread safe are not seen. Also, for caching. So, you can use thread local for implementing a very specific kind of

caching. In this case, the caching would be for a particular And um by using thread local, it makes it easy to implement that caching. So, you can use that cache from anywhere in the thread, but also it is easy to invalidate that cache. Observability, specifically for implementing correlation ID. So, let's say you are operating in microservices architecture, then you have your service and then multiple It is

calling multiple services. If you want to trace a particular call, a particular chain of services being called, you use correlation ID. So, you can store that correlation ID in a thread local, and then you can pass to downstream services. Okay. Now, virtual threads support thread locals. What does it mean? So, the way thread locals work with platform threads, they work in the same way with virtual threads

as well. But, there are some problems with thread Uh firstly, it it has unconstrained mutability. So, mutability in general is a problem. Uh with thread locals, there is no constraint over when you can update the value in the thread local, and that can get you into trouble. Unbounded lifetime. So, a thread local has a method called remove, and you're supposed to call it when you are done

using that thread local object. But, developers often forget to call that, and what happen What happens is that then the thread local object stays with the thread for longer than it is supposed to. Sometimes you get a thread from a thread pool, and it has a thread local object which you don't expect. It can also take your application into an inconsistent state. Uh expensive inheritance. So, thread

locals are not inherited by default. What What does it mean? So, if you have a thread which has a thread local, and you create child threads, that thread local is not accessible from these child threads. But, if you use inheritable thread that thread local object gets copied to the child threads as well, and then it is accessible. Which is generally not a problem, but if you use

virtual threads, you generally create more threads. So, let's say you create 1,000 virtual Do you want your thread local object to be copied 1,000 times? No, because then it would take quite a lot of memory. So, that is an expensive inheritance that we have with thread Now, if you use scoped values, all these problems are solved. Because scoped values are immutable. So, the unconstrained mutability problem is

not there. Also, all allowed threads use the same copy, and therefore the expensive inheritance problem is not there. Also, they are inherited through structured task scope to the child virtual threads. We will see this in action as we use the structured scope scope value API, and then we'll see how the inheritance works. And they're bounded by scope. So, here we have an example where we create a

scope value called value, and we set it to some value. So, basically we set some value in the scope value. And that some value is available to us only within those curly brackets. So, only within that scope. It is bounded by Okay, now let's explore the scope values API. For that, I will again head over to IntelliJ, um and go to loan controller. But before we use

the scope value API, I want to do a small refactor. So, I actually want to uh extract this part uh into a method. I will call it get customer info. Uh I'm going to define a customer info record. Uh and what this um record does is it has accounts, loans, and credit score. And I want to return that from this method, get customer info. Uh basically what

we have done is we have extracted all the calls to the structure task scope into its own And I will take this part out and put it Uh so in order to return customer info from this method, I'll just create a new And that will have accounts, loans, and Okay, so now that I have customer info, uh let's put it in here, customer info. And then I

can use that to then get loans, and credit score data. Okay, let me indent this so it still is readable. Um okay, so I have not changed the logic uh of the implementation, but I just moved one part into its own method. So now if I restart my application and test it, it should work the same way as it did before. Uh so let me send the

same request again and see if it still working the same Uh let's look at the logs. I'm getting the customer details, gets loans, accounts, credit score, and everything. We calculate the offer, and we have the offer here. Okay, so it is working fine, there is no problem. we can actually also see the the request going through all these calls because it was a single request. What if

we had multiple concurrent requests? So three requests. So request one, request two, request three. Can we go and look at the logs and see what is happening in each request? It is not possible right now because all these logs are uh mixed together and it's very very difficult to identify one request from the other. Now, how can we solve that problem? Uh we can solve that by

introducing request ID. So let's implement some logic so we get a unique request ID for each request and then we can see it in these logs. Uh it's easy to implement that by using UUID, so I'm going to do that. And then I can pass this request ID all throughout my implementation. uh you know, log log it. So, I want to log it here and here. Uh

request ID, and this would be fine. So, I have a unique request ID every time a request comes in and I pass it through my application and then I can log it. And this would work okay. But the problem is I have now added a new parameter to my method signature. And this will be there in a lot of methods, if not all the methods uh that

I have in my application. What if I want to add more things? They will just get added to this method signature, so it will make my code unreadable, will introduce a lot of noise. Now, what is happening is in our case, we are operating in thread-per-request model. So, we create a new thread every time a request comes in. What if we s- put this request ID in

a thread local? Right? Then we can get it in the thread from that thread local. And that would work. And then we we don't even need to pass them through these method uh signatures. But why use thread locals when you can use scoped values? So, let's use scoped value uh and then put that request ID For that, I'm going to add a new class called request context.

And this request context will hold our scoped value. So, there will be public static final scoped value of type UUID called uh request ID. And I can already initialize it here. {dot} new instance. Okay. And then I can set this uh request ID in the loan controller. The moment I have my request ID, I can use scoped value {dot} where, and then I'll say, oops, uh request

context {dot} request ID and then set this request ID there and then make a call to this lambda so my scope value is available to me within that lambda and I can return the offer here. So what is happening here is we set the request ID in the scope value and then that is available to us in this in this call. So then I don't need to

pass to this method here. I don't need it here and how do we get it? We get the request ID from the request context request ID dot get. So this is how the scope value API looks like and so this way you basically set your value in a scope value and then here you can use it all throughout in that lambda. Okay, this is fine and this

would work but I want to take this one step further. What I want to do is I want to change this to private member and add couple of methods. So request again I will share the link to the source code so you can take a look at how these are implemented but what this does it it limits the use of scope value API to this class and

it exposes a domain friendly API outside. So in the loan controller I can simply do and set my request ID there and wherever I need it I can simply do request context.get request ID. And now I want to do that at multiple places so let's do that at all those places and then see how it works. So the next thing is account service. I need it here

and I want to log it here Request ID then in the loan service I need it at two places so here and And then I want to go to credit score log it also Okay, all the changes are done. Uh we have our scoped value. We are using it in all the places. So, let's restart our application and send multiple requests. Uh three requests and then see

how it works. So, request one, request two, Let's go to the logs. And now we see these IDs floating around. So, this is uh first request with this ID and then we can actually trace which logs are being made, which calls are being made uh with that request. Then this another request, we can look at all these logs uh all the calls that are being made there.

And the third one, we have actually offers from all three of them. So, we should be able to see them also here in the console. So, this is second, this is third, and this is first. So, this way we can uh use scope values to then implement such a logic and then uh in our case we used it to differentiate between different requests that are being made.

Now, let's take a quick look at the loan controller to see what is happening. So, we did set the scoped value here, which is the parent virtual thread. And we are using it uh in the child virtual threads here in these tasks. And it is possible because we are using structured task scope. So, the structured concurrency API enables the inheritance of scoped values. Okay, so that was

the last demo that I had to show you. Now, if you're interested in the source code, it is present here. And if you have any questions, feedback, comments, you can reach me out over And I guess there's a possibility to rate these talks in the schedule app. You can also rate my talk there. And while you start rating five star, let me quickly summarize what we uh

So, we looked at three different features. We looked at virtual threads, values. Virtual threads enable us to provide high scalability. Structured concurrency allows us to have clarity in our concurrent code. And scoped values, we saw that through structured concurrency API, they allow us to inherit these scoped values. So, these three APIs, they work really nicely well together to allow us to write readable And I hope this

was useful. Thank you. >> [applause] >> We have 1 minute for questions, so let me quickly see if there are any questions. Yes, there are. So, how you see the virtual threads in profiler tools, for example, J VisualVM? Okay, this is a very specific question about the tool VisualVM. Uh I think you can see it, but I don't I I I will not be able to give

you a specific answer now, but we can take a look at together. Maybe you can find me Uh is there a reason not to use virtual threads in a Spring Boot application? A reason not to use? Well, so virtual threads are really good when you have IO-bound applications. But if your application is compute-heavy, where there's a lot of CPU involved, you won't get the benefit of virtual

threads as much. So, in those scenarios, you well, then not use virtual threads, stick to platform threads. But typically, the applications that we write uh I work in a bank, we use a lot of microservices, a lot of calls being made, and we are waiting. In those cases, uh virtual threads are a good choice. How does join know the type to return when using joiner any of

throw uh when each subtask returns different stuff? Well, uh the joiner captures the the type, and uh in the join method, it is available. So, you can look at the implementation of the joiner and the structured task scope to figure out how that is happening, but it's the the magic of generics. Um how to set multiple scope values at the same time? Well, you can do that.

So, basically, you use scope values and then you have a lambda. Within that lambda, then you can have another scope value, or you can also set in the same thread multiple scope values. And then whenever you call call, then within that you have it available. in fact, you can have multiple scope values and you can even set the same scope value in a nested fashion to different

values, and it will be available within that scope. So, yeah, just explore the scope values API, try, play around, and then you will see how that works. Uh, in real systems, if correctness requires serialization when concurrent requests touch the same business entity, is in the commit boundary still the bottleneck? Not sure if I understand the question, and we are also running out of time. And my wife

is calling me, so I think I have to get out from here. So, thank you all.

From event

JCON EUROPE

20 Apr 2026 – 23 Apr 2026

All event videos
Back to Watch