About this talk
This talk focuses on the new core resilience features in Spring Framework 7. The speaker discusses how elements previously part of external projects, like Spring Retry, have been integrated into the core programming model. The session highlights the significance of modern threading architectures, specifically the need for features that are friendly to virtual threads. The speaker introduces new annotations such as @Retryable and @ConcurrencyLimit, which allow developers to manage retries and limit concurrency in a programmatic way. Additionally, the talk covers the important design goals of enhancing the developer experience and improving interaction with reactive programming. Various configurations, including backoff strategies and delay settings for retries, are explained to provide a deeper understanding of how these features can be implemented effectively.
Full transcript
[music] >> All right. So, thanks for the kind words for the kind introduction. Um yeah, my pleasure to be back up here on the stage. This time with a dedicated session about what we tend to call the core resilience features. And I'll go into what that actually means and what's and why why's of of what we're doing here. In in in just a moment. the um some
of the motivation behind what we're uh just is very indicative of the Spring portfolio overall. If you if you look at the wider Spring portfolio, you can see uh some projects appearing as independent projects being merged into other parts of the projects. We sometimes rearrange things within the Spring portfolio. And this is actually very related here. They used There still is There used to be sort of
a Spring retry project. Uh quite a bit of which has been sort of re-revisited, redesigned, re-implemented as as core features of the core programming model. Uh but it's by no means the first time this is happening. Um but as you will have noticed, these things only happen as a new generation of the framework launches, right? So, that's why it happens now in Spring Framework 7 and in
yeah, in in former times. Even go far enough back in time and you'll you'll find out that even configuration classes at configuration classes used to be an extension called Spring Java config having been brought into the core Spring Framework in Spring Framework 3.0. So, um Now very core elements of the programming model used to be sort of extension projects external to the core framework. Uh but got
merged in over time. Yeah, with that said, that's exactly where we are here. We revisited a uh a an an important, interesting space for the programming model itself. Um This is a little bit in the light of modern-day threading architecture. So, I'll I'll go into this in a moment. Things are not quite uh the the conditions that we live within here are not quite the same from
from 10 years ago. Anyway, so as as usual, this is common annotations, part of the common annotation programming model. So, just like you'll find at transactional, at cacheable at async you'll now find features such as at retriable and at concurrency limit. They operate at the very same level. They're part of the core programming model, hence core resilience features. None of this is implemented actually in an annotation-centric
way because of course core retrying or even say core concurrency management is a very programmatic affair underneath the covers. And we we made all those levels public, right? So, you can programmatically interact with what we call a retry template and you can interact with what we call task executors for for similar purposes or even the very same purposes that you used the annotations for. It's your choice.
Annotations obviously complement existing annotated components very nicely, whereas if you have existing programmatic arrangements, by all means consider the more flexible and more to some degree more expressive programmatic variants. Now, before we before we primarily spend our time looking at source code variations um I'd like to discuss a couple of the design goals. The because they aren't necessarily obvious. A key design goal here actually is virtual
threads friendliness. Um so, why that and what does that even mean? Well, in in traditional architectures, say a traditional platform thread uh scenario, you have a thread pool, right? Um a thread pool with a limit. So, thread pools any kind of pooling arrangement is usually naturally limited, bound by an upper bound. You if if your thread pool does not allow for more than 10 threads in the
pool, your effective concurrency level using that thread pool is never going to be higher than 10, right? So, there's an implicit concurrency limit baked into pooling arrangements. The same is true for any pool that makes you block on waiting for like a connection to be obtained from it, like a JDBC connection pool. If it's configured to make clients block until a connection becomes available it's a natural
concurrency limit. If you have a JDBC connection pool of size 10 max you're never going to hit the database with more than 10 connections in parallel, a natural concurrency limit. The um other part of this, right? Is that the the introduction of reactive programming quite a while ago um gave us primitives like basically building blocks, elements, operators for expressing your your your concurrent needs. So, there's ways
of crafting a very fine-tuned reactive pipeline which which naturally even has retry features baked in and can also naturally control the level of of concurrency of that particular pipeline through how many things it spins off and joins again. So, we are trying to be virtual threads friendly a little bit from that perspective. Consider what you can do with reactive programming right now with a project reactor in
particular. All of all of the nuances and all of the operators that it provides. We're trying trying to port some of that experience, some of that flexibility and some of those building blocks um over to virtual thread-based architectures where can use similarly fine-grained, nuanced processing in a virtual thread-based blocking style, right? And of course virtual threads increase the concurrency exposure, right? There you can start a virtual
thread at any point. It's always going to well, start and edit cells a sort of queue, right? The There's no natural limit to the number of virtual threads. That's part of the game, part of the design there. You can literally have a million virtual They're all going to be attached to a very limited number of carrier threads underneath the cover. Um but the number of virtual threads
themselves is basically unbound, unlimited. And you don't usually pool them or at least not in the traditional sense. So, there's no pooling post limit for virtual threads. If you want to constrain your actual concurrency level, right? The the maximum processing hitting a particular service, a particular component through in in a virtual thread-based architectures, you need to do so through custom means like adding some custom semaphores, adding
our concurrency limiting features here. You need to do so explicitly, whereas in a traditional thread pool environment, it was basically implicitly coming from the thread pool setup. So, things that used to be implicitly applying now have to be explicitly expressed in a virtual thread-based Um there's a bit of alignment also in in the design goals here. Back off is something we already have in the core framework.
We use it for listener container management for basically connection recovery purposes. Uh also there's a a very close relationship with what retries have to do if they have to back off and wait a little till you attempt the next retry. Um so, we're trying to reuse that to the best possible degree in those core features rather than reinventing anything similar. Um and for an inspiration, project has
a very flexible retry back off spec arrangement that you can apply to reactive pipelines. This has been a source of inspiration for our retry features for uh what you can for how you can express yourself and for basically what you can fine-tune. Um we were trying to align with everything reactor can do there. At the same time, whenever we design something new for the core programming model,
we also need to know how far we intend to take this. What are we trying to cover and where do we draw the line? Drawing the lines is hard, right? In in particular if you're um talking about a programming model which theoretically could make many things first class in the core We chose to focus on basically the same level that reactive operators are at. Or you could
say Java util concurrent level. So, basically what the Java util concurrent primitives give you in the core JDK libraries, we operate at pretty much the same level, just in a more convenient fashion. we don't do we won't do more than this, uh but we're trying to significantly improve the uh developer experience, of course. We're trying to empower architectures on virtual threads, um but the Java util concurrency
primitives are uh concurrent primitives are actually very virtual threads um aligned already, so uh that that goes along with it. A an equally important thing, maybe even more important point. We're talking about the core programming model. This is the internals of a service that you're implementing. You're basically implementing repositories, services, delegates. You're implementing may- maybe specific controller endpoints. Um specific message gateways, right? We're talking about about
a pretty fine-grained component level that you are applying these features to. This is not meant to replace or even tackle um the same problems that say circuit breakers, rate limiters, uh cloud platform level uh features of that kind. Those remain separate. Those are applied to your deployment, to the cloud platform handling your deployments, to the platform auto scaling your deployment. There are measures at place at that
um and I would highly recommend to use those, to keep using those, What we provide here are additional measures within the system for specific components to be a little more fine-grained of which component needs what kind of uh measure applied. And um just like just like security, right? Or uh security measures that you apply to your system, to your implementation, it's a multi-level affair. Uh it's a
good idea to do that with security, right? Do it at the entry point, have some level of security uh management within the application as well when you're hitting uh critical services. The same is true here. Try to manage basically the boundary level, the the the the system level, platform level rate limiting, try try to define how much you want to be forwarded to your service there. Whereas
within the service, you might be delegating to other services. You might be using some internal uh components where you know that they um produce target messages, talk to some target database, and you want to constrain those, or you want to retry management for those. Then now we're talking, right? That's what the core resilience features are for. Right. Um so much for a bit of design discussion. Um
now let me show you a couple of examples. Those are pretty straightforward. Um anyone knowing Spring Retry, the uh former Spring Retry project, which has been around for many years, um should find themselves familiar here, uh but this is basically common ground in terms of usage. Um so you'll you'll find um say Resilience4j or other uh projects out there, other programming models out there, use use very
similar uh annotations in particular here. In our case, um we have a core retryable with uh uh many attributes that you can customize. We'll go into some of them. But also sensible defaults. So, if you just say at retryable, you'll get a an basically initial attempt plus a maximum of three retries. delays of seconds, uh yeah, you'll there there's sensible defaults behind those. the methods that you
choose to apply those to are of your choice, obviously, right? What's worth retrying in case of failure? And in case of which specific failures is it worth retrying? You need to say so, right? We can't infer that. So, um you're going to be as specific as you can. Don't try to micromanage necessarily every every definition here, but you can be as specific as you need to be.
For a start, how often do you want to retry uh for a particular target system before you give up? Depends on factors that you know best. Um and say for a messaging system, you might have more retry attempts maybe than say for a a database that is usually always available, where it's very unusual for it not to be reachable or for it to hit to hit the
timeout. So, you maybe for a messaging endpoint uh as as as has been suggested here in this little code sketch, uh a notification passed onto a uh JMS destination, may- maybe you'll you'll increase the maximum number of retries a little. If you want to be more specific uh about which exceptions are worth retrying, uh you you can be so, right? By by by default, we're basically retrying
any failure. If the method throws an exception, unless you say otherwise, we're always going to re- retry it if you annotate it with at retryable. If you only want to uh in this case handle message delivery exceptions, so a very specific kind of exception um that in this case would come out of the JM out of our messaging exception hierarchy uh say let's take a database case.
Um You're you're trying to store something through JPA or maybe through some custom JDBC usage. And uh and you're um uh you want to react to optimistic locking exceptions or generally locking failures. You you can say so, right? You can uh react to optimistic locking exceptions or to our hierarchy of locking failure exceptions covering all kinds of locking scenarios, and you're only going to retry those, but
not if uh anything out of the ordinary happens, right? So, your your choice. Um your your method that you're annotating, you know what you want to retry. Ob- obviously, there's more attributes waiting for you, um and they can be combined. So, if you want to retry message delivery exceptions, you only have includes, no excludes, so you can also exclude specific exceptions if you if that's easier for
you, right? Retry everything other than this. There's an exclude attribute for this. Uh a maximum number of retries. Uh in this case, a shortened delay. So, how long are you going to wait? How long are you going to back off before the next attempt, right? If an initial failure happens or also between retry attempts. That's the delay. Um there's a maximum why is there a maximum delay
as well down here? Because we have a multiplier factor in here. So, uh which means first 100 milliseconds, on the second back off, 200 milliseconds, next one uh well, do the math. Um 400, 800. Um it would add up, but um if you want to limit this, possibly also for customiz- delays where you where you always want to have a hard limit. So, whatever the multiplication leads
to, never be never wait longer than this between attempts, right? Uh then that's what makes delays for. Um we also have a jitter feature in here uh that's indicated here. Jittering is basically a little artificial deviation, right? Don't always make a a strict 100 milliseconds. Go maybe 10 milliseconds below or above it for every attempt. So, classic jittering feature as known from uh network uh retries is
also available here. Um it's literally it it uses the base calculation, the delay plus multipliers for every attempt. It uses that as the uh uh reference point, and then just jitters a little so that it's that that if you have multiple service instances running into the same retry for the same method hitting the same target service, that they they don't hit target service at the very same
time, so that there's a little deviation. So, that's jittering. Whether you need to be as fine-grained as this, it's your decision, right? You can keep it simple. Just go with the defaults. No jitter, fixed delay. Now, in your state, may not be so obvious, um since I was saying we're trying to provide building blocks that match what reactive programming gives you, what Reactor gives you. Um they
actually also interact directly. If you have a uh method with a reactive return type, just like our at transactional or our at cacheable, um they are actually specifically understood. just quickly, the if you if you do transaction management or cache management, um there's basically two modes of operation. We we immediately detect whether the method is imperative or whether it returns a reactive pipeline, and there's a different
mode of operation if it's if there's a reactive return type. For transactional, we uh we basically uh decorate the reactive pipeline with a transactional operator. similar for caching, we decorate the pipeline to perform a cache lookup first, and only only then call the actual reactive pipeline. Different mode of operation. Um the same is true here. If there's a reactive pipeline returned, the mono or flux in in
in in reactor terms? We actually decorate the reactive pipeline. We use the features provided by reactor or the retry back off spec feature in reactor and translate the at retryable attributes, the definition that you you attached as an annotation, we translate it on to on to the reactive pipeline. Including the features that we have in customizable form, which isn't actually showing here, but you can uh there's
string equivalents of those attributes that you can pass in uh placeholders that come in from some configuration source. So, um the annotation is actually a little more flexible in being able to pull uh say the maximum number of retries or the delay and that stuff from from from external configuration sources through the common spring placeholder model. Um and it's still being applied to the reactive pipeline that
this method returns. Um so, we won't go into this uh too deeply, but retryable is actually capable of handling reactive methods. Not least of it all, um this is only really a quick hint in an area that is still a little evolving. There are opportunities to monitor what's happening um or well, we have the facilities in there to basically listen to what's happening. Every time that an
at retryable method fails, it publishes a method retry event, um which is an extension of method failure event, which is something that you would uh well, it makes a general a general sense to have a uh a listener for it and just log it specifically because it's a little out of the ordinary for that to happen on methods that you declare so. And method retry event itself
exposes some of the state, so you can basically reason about um what failed if it's already the final step that failed uh or if it's an intermediate failure and just log it accordingly. It's primarily meant for logging. You're not supposed to influence the retry algorithm itself. It's just tracking it basically, listening to it. So, there's an eventing model behind this and we'll see there's also a programmatic
listener equivalent for the programmatic facilities in a bit. So, what is this at concurrency limit thing? The kind of second part of the current Um concurrency limiting is an old facility in Spring. Um very literally, I mean, this has been in there since like 2004. Um so, 22 years old in as a as an AOP interceptor and as a mechanism to constrain what we call the simple
async task executor. We basically took this feature or this this way of expressing a concurrency limit and made it a an annotation based feature. At concurrency limit is basically applying an AOP interceptor to your method, right? It decorates your method with a proxy plus a what we call a concurrency throttle interceptor and it's going to make sure that your method is not being hit by more than
10 threads in parallel with a concurrency limit of 10. some configuration options in you can basically usually uh by by default it's going to block if one more comes in and uh the concurrency limit is already exceeded uh would be exceeded then you're uh blocking the caller until uh someone some some other thread leaves the method, right? Uh you can also make it fail early if you
prefer, uh but the usual way is blocking. Why blocking? Well, that's exactly what you should be doing for a virtual thread that is currently not able to continue processing, right? You're supposed to block it. Again, virtual threads friendliness. In a virtual thread scenario, blocking is a cheap operation and part of the idiomatic way of implementing a virtual thread based processing sequence. So, it's it's a little bit
like you had a semaphore and a custom semaphore and you're basically um um tell me when I'm allowed to enter. The concurrency limit itself is uh obviously customizable. You can also put a placeholder in there. So, it's you can externalize the actual configuration setting. Concurrency limiting of one essentially makes it a sort of lock, right? It's almost like you had a a Java util concurrent lock around
it and uh just waiting for the lock to be released. Uh it's a little bit like you had a synchronized method basically um or a custom custom lock usage. Um so, that would be a special case, uh but yeah, it's definitely possible to do this, right? You can you can basically use concurrency limit like a declarative lock if you want, right? Uh at a pretty at at
the method at the level of method granularity. So, um yeah, the concurrency limiting is basically well, almost literally like using a Java util concurrent lock or or semaphore, right? They you you or like like a synchronized keyword. It's a JVM local um way of constraining how many threads are allowed to enter that method at the same time. In that sense, it has no technical relationship with any
kind of rate at the service entry point level or at the platform deployment level, right? Um it is sort of conceptually related in saying you maybe you may have rate limited your service, but if the uh the code path in your service, the code flow leads to this method being invoked, you have a hard limit applying here, right? The service itself may be hit by whatever number
of requests at the same time, this method you only want to hit to be hit by one thread at the same time 10 threads at the same time or if there's a placeholder by a configurable number of threads at the same time. It's as simple as this. It's also no more than this, right? Distributed lock management is an entirely different topic. Uh in the Spring portfolio, this
would be more like a Spring Cloud level feature and that's where you find such features. That's where you find uh circuit breakers and rate limiting at the platform level or at boundary cloud frameworks basically, right? Or in the in the Spring ecosystem, uh the facilities that Spring Cloud provides for such purposes. quick reminder, multi-level concurrency management, right? So, it's a good idea to constrain things at the
entry point, how many requests are allowed to hit the and if you have sensitive components inside to also express limits at a more fine-grained level like here. Right. Um one more thing. How can you actually activate all of this? In the typical Spring sense, at enable resilient methods like at enable transaction management. It's you can customize the proxy options and that stuff as attributes, but by default,
it's just this. Um it makes at retryable and at concurrency limit to actually automatically be wrapped with an interceptor as you're used to from at Now, let's move on to the programmatic action, guys. Um and without um going too deeply into every nuance of you'll find a similarity here in terms of capabilities, obviously, because at retryable underneath the covers either uses retry template or the reactor retry
back off spec. There's those two code paths, right? For non-reactive and reactive methods. Uh so, obviously, they would have similar or the same capabilities, but there's a little more flexibility if you use those programmatic elements So, for for a simple default usage, retry template is basically a callback based way of expressing a retry. Uh retry template is a very light instance. Feel free to reuse it if
you want to reuse its configuration. Feel free to just create a new one for every operation. That's fine. It's very lightweight. Retry template invoke takes a callback and applies the retry policy, in this case the default retry policy to the callback. So, it's going to invoke it once. If the initial attempt fails, it's going into the retry procedure and it's going to do as many retries as
allowed. It's going to do the back off as defined uh just like for the annotation based If you want to customize it feel free to pass in a retry policy. Um this is a builder. It's basically an interface, but it has a builder with it. um if you just want to customize the max retries do it do it here at this level. If you want to be
a little more fine-grained, um there's literally a builder where again, we constrain the retries to message delivery exceptions. There might be some custom excludes, maybe some sub exceptions of message delivery exception uh and everything else you've been seeing before is available here on retry policy. Retry template takes a retry policy. It's basically the executor for a retry policy if you if you want to see it like
this, and then you invoke it again. Invoke semantics will will get to that a little later. Uh basically, um exceptions originally exceptions get propagated back to the caller. We'll get um alt ultimate original exceptions, right? If they if we if we exhaust the retry policy, if we ultimately fail, we tried to retry, but no luck, right? Um and there's a timeout that you can configure also for
the overall uh entire retry procedure. So, if if you if you need that, but there might be an exception coming back to you to the caller eventually. And that exception in the invoke case is like with an annotated method, it's the original exception. So, if the JMS client threw a message delivery exception and yet another one and yet another one and yet another one, then we give
up. You're going to get the last message delivery exception propagated to the Oh, yeah. Um one more just to give you an illustration that all everything is Um the retry policy has basically the same settings as at retriable. in this case duration-based, so there's a little bit of a difference because it's programmatic. Uh but yeah, uh feel free to build it as as specific as you need
it. Pass it into the retry template. Use the template to invoke a some some target operation. So, yeah. Now we come back to the exception handling nuances. If you want to handle this ultimate exception coming out of the retry template, um well, a try-catch block, right? Around the retry template invocation gives you exactly that. You can do the same to an at retriable method. If you call
the ultimately, policy exhausted, you get the last exception back. And the caller can actually try to catch it and try to handle it. The invocation here is a basically without a return value, runnable-based. There's a variation with a supplier where it's uh a including a return value, right? So, you can then literally use it just like an Um might be a method with a with no might
be some target operation without a return value, or it might be one that you need to carry through. And if you need to handle the ultimate exception coming out of it, there's the outer try-catch block doing doing So, notice [sighs] the little difference This is the invoke method on retry template having invocation semantics basically like a method invocation in terms of exception propagation. There's an underlying say
more slightly more powerful method um called execute. Takes the callback. Um however, what comes out of it is a retry exception. So, if anything fails that is um coming out of it the retry procedure, you ultimately get a retry exception. What's the difference? Well, it's a checked exception, which is unusual uh in Spring APIs, but it's unusual to to get that because you're not supposed to propagate
it. You're supposed to reason about the state of the retry exception. In this case, you have full insight into uh the retry state, how many attempts we had, uh all the exceptions that occurred, um or you can see the last exception if you want it, right? You can basically do some processing. Maybe maybe you log right there if you if if if you have specific logging for
this specific operation. And ultimately, you rethrow the last exception, or you rethrow a you throw a custom exception with uh this as a cause, that's your call. Um it's basically a combination of a uh of all the exceptions that happened with a little bit of uh retry state management around it of being fully exposed to you as an outcome of uh the retry attempts. The uh alternative,
sort of a kind of a alternative, right? is to specify a retry listener. So, what's the idea here? In the traditional case, you basically let it all happen. And then you wait for the final state of affairs. You get the retry exception exposing all of the state of the end of the outcome of the entire retry procedure, and then you you act. The alternative is to keep
using the down here, the one that propagates the original the last original exception um if if if it happened that way. And specifying a retry listener on the retry template allowing you to get a callback, and this is just one of the retry listener methods, but it's maybe the most useful or the most common one you can get a callback for every retriable execution. So, every time
we have to uh we we we failed, we can basically get a callback and say like, "Okay, let me log this. Let me forward it to some monitoring." Um as it happened, right? We may back off a couple of seconds in the meantime according to your specification. You get the callback right when it happened. Um every for every everything that happens. And at the very end, the
original exception gets propagated to the caller um if we ultimately fail. That's it's a different way of um interacting with the retry state Now, um last but not least, and maybe um the least obvious part of all of this, concurrency limiting um is not only available for annotations. As as um mentioned before, this actually is an old sort of idea, an old feature that is exposed in
various places in uh Spring Framework delegate and Spring Framework execution facilities. In sort of recent terms, it is in particular available on sync task executor. So, what is this? Uh this is part of Spring's task executor abstraction. Uh this is the same kind of thing that lets you choose between a simple async task executor or a thread pool task executor. It's all all the Spring components being
able to uh spawn a uh some some async operations, they all take uh a task executor. It's like a Java util concurrent executor with a little bit of definition around it. That's really what it is. Think Java util concurrent executor. Now, the sync task executor as of 7.0, it that's actually new that it uh acquired that is a uh is a delegate >> [snorts] >> with concurrency
limiting features baked So, you can take a sync task executor, again just create it, it's a light instance, lightweight instance, specify concurrency limit, then call execute on around an operation of your choice, and it's going to constrain um the threads being allowed to enter that operation at the same time. Just like at concurrency limit does for method invocations. It's it's literally the equivalent feature for programmatic uh
Sort of the equivalent of at retry template, right? If you see at retry template as an equivalent of at retriable, then this is the equivalent of at concurrency limits. Why is this in sync task executor? Um this makes it configurable in interesting ways, not least of it all for testing purposes. Um you can also reuse the task executor definition. You can create the instance in line like
here, but you could also uh define it as a bean, make it injectable, maybe for a customizable concurrency limit, right? You pass around a custom task executor instance um that in one place in the configuration is configured with a specific concurrency limit. Everywhere else, you inject it with that auto wire, whatever. And then just keep delegating to it. Um so, the there's there's flexibility in um reusing
a preconfigured instance. That's why there is a delegate for it and not just a utility. Now we have a variation here uh on sync task executor with a return which for a task executor is a little unusual because sync executor abstraction it's actually primarily meant for async tasks, and an async task is not going to give you a return value uh immediately, right? Um only the sync
task executor can produce a return value right away. So, you got an execute method of that kind And making it fully capable to do whatever uh at concurrency limit for method invocation can do. And now this is actually an old feature even if it looks uh it it is of very similar style, right? It looks very similar. The simple async task executor also has a set concurrency
limit method, uh but a 22-year-old one. And this method here uh this executor here is actually going to create a new thread and pass basically this callback the runnable callable to to the asynchronous thread. This is also an implementation of async task executor, so it actually has methods like submit returning futures or submit completable returning a completable future. It has all the facilities of asynchronous task management
that you would expect um are also available on the thread pool executors, you have them here as well, right? But stylistically, it's very similar. This is spawning asynchronous threads, right? So, obviously, it's a bit differently purposed. Not least of it all, it's one of our primary hooks into virtual threads. So, setting the virtual thread flag to true here makes the simple async task executor produce virtual threads
for every for every callback, which also kind of provides a nice circle of at the problem space here, right? So, in case you want to spawn asynchronous tasks with virtual virtual threads rather than just being invoked within a virtual thread, you can do so here. You would usually configure the simple async task executor once as a as a bean. Inject it and just use it. Then it's
preconfigured to use virtual threads, preconfigured to use a concurrency limit. And in this context, you basically have a sort of alternative to a traditional thread pool, right? This is behaviorally, this is similar to a thread pool with a size of 10. Yeah, maximum concurrency of 10, but using virtual threads that all go to the JVM's common fork-join pool underneath than managing local platform threads. Right? So, that's
where basically the story connects back to traditional threading. Traditional thread management. All right. So, all of these facilities, all of these options are available in Spring Framework 7.0.6, the latest at this point of time. It is effectively superseding the Spring Retry project for portfolio purposes. So, within the Spring portfolio, say in Spring Integration, in Spring AMQP, in Spring Kafka, in various projects in the wider Spring ecosystem,
you'll find in the current generation, you'll find use of these facilities. They all reuse the core retry features coming in Spring Framework 7. Which actually was one of the design goals that I haven't mentioned before was provide sufficient capabilities for the purposes in the entire Spring portfolio. Right. And the virtual thread support, related story, but obviously already a little bit longer and still continuing. Check it out.
There's many things you can do. There's the virtual threads enabled true flag in in in in in boot. There's many facilities at core Spring Framework level already since Spring Framework 6.1 being at least virtual threads oriented or virtual threads friendly, right? You can use all those facilities, simple async task executor, even the the the retry management, even the concurrency limiting. You can use all of this with
regular platform threads. not going to scale as much, right? You're going to block a platform thread, right? You get operating system level block threads through it. So, some of those facilities, if they friendly, virtual threads oriented, it just means that what they do and how they are designed makes particular sense with virtual threads, where it's kind of natural and idiomatic to do what they do against the
virtual threading That is all I had for you so far. Bit of a pretty complete rundown of what we currently do here. If there are any questions, I'm happy to take them. I I'm also going to look at the the app, the Woova app, the questions afterwards, right? If in case you you have any that you are going to add there, I can I can also respond
Right. So, thanks for your attention. If there are any immediate questions, we have 2 minutes left. Sure. All right. Good question. So, the question was is for the virtual thread arrangement, what about context propagation? A common a very common problem. The best you can currently do is to uh take our context propagating task decorator, which is part of the task executor world, and basically apply it to
this and like literally the simple async task executor, set task decorator, specify this thing. And what it's going to do, it uses context propagation as known from Micrometer. It basically takes a snapshot of the calling thread, all the context that the calling thread, the the the one that actually spawns the new it takes a snapshot and passes it on to the new thread. So, there this new
thread is going to see the same um the same context, which is basically predefined thread locals um that the original thread saw. Makes a lot of sense for monitoring, makes a lot yeah, custom custom state as well. It's a necessary part of the picture. You can do this in a thread thread factory arrangement theoretically as well, but I recommend doing it through a task decorator. Any immediate
questions? 15 seconds left. >> [laughter] >> You'd have to be quick. Otherwise, well, sure. So, transactions in combination with So, the question was how does this interact, right? Is it going to create a new transaction? Theoretically, you can combine the interceptors in any way you want, but obviously there's a a regular a a regular way of doing this. Transactions are part of the target operation, right? So,
um in particular, if you think about say database interactions, they are often going to fail late when you try to commit. Basically, oops, locking failure, right? You're a deadlock loser. So, there there's many exceptions that are worth retrying, but they only happen as you try to commit the transaction. the target operation would usually usually be transactional. And if you retry, you basically retry the entire transaction. There's
a new transaction for every retry attempt. That's the standard mode of operation. And that's also what I would strongly to to hold on to. Right. Well, thanks for your attention so far. Any questions, please use the Woova app and enjoy the rest of the show. >> [applause]
More from this event
See all 38 talks →
Spring I/O 2026 Keynote
1:08:44
The Spring AI Ecosystem in 2026: From Foundations to Agents @ Spring I/O 2026
43:39
Breaching LLM-Powered Applications: Overcoming Security and Privacy Challenges by Brian Vermeer
48:40
New in Spring Security 7: MFA, OAuth2 and more by Daniel Garnier @ Spring I/O 2026
46:43