The DeferredResult Trick
class SomeController() { @PostMapping("/create-customer-portal-session") fun createCustomerPortalSession( @RequestBody someDto: SomeDto, ): DeferredResult<Success<SomeResult>> { val deferredSessionURL = someApplicationService.createCustomerPortalSession( someDto ) return deferredSessionURL } }
-
When we return
DeferredResult
, Spring understands that the result is not immediately available. It doesn't block the servlet thread but instead releases it back to the thread pool. -
Spring sets up an asynchronous context to handle the
DeferredResult
. It doesn't actively wait for the result but is prepared to process it when it becomes available. -
When
setResult()
is called on theDeferredResult
(within a coroutine), Spring is notified that the result is ready. -
The thread that calls
setResult()
notifies Spring's asynchronous request handling mechanism. -
Spring then uses one of its own servlet container threads to process the result and send it back to the client.
What Happens in Application Service?
We initiate DeferredResult(timeout in Long)
as a placeholder, we later setResult
in a coroutine scope asynchronously:
1class SomeApplicationService() { 2 fun createCustomerPortalSession( 3 someDto: SomeDto, 4 ): DeferredResult<Success<SomeResult>> { 5 val deferredSessionURL = DeferredResult<Success<SomeResult>>(10000L) 6 val scope = CoroutineScope(Dispatchers.IO) 7 scope.launch { 8 try { 9 val (customerId) = someDto 10 val params = SessionCreateParams.builder() 11 .setCustomer(customerId) 12 .setReturnUrl(managePlanURL) 13 .build() 14 val session = Session.create(params) 15 deferredSessionURL.setResult(Success(SomeResult(session.url))) 16 } catch (err: Exception) { 17 deferredSessionURL.setErrorResult(err) 18 } finally { 19 scope.cancel() 20 } 21 } 22 return deferredSessionURL 23 } 24}
Customer DSL to Simplify the Logic Via Trailing Closure
We further simplify the previous code block via the following util:
@Component class DeferUtil { fun <T> defer(block: suspend () -> T): DeferredResult<T> { val deferredResult = DeferredResult<T>(20000L) val scope = CoroutineScope(Dispatchers.IO) scope.launch { try { val result = block() deferredResult.setResult(result) } catch (err: Exception) { deferredResult.setErrorResult(err) } finally { scope.cancel() } } return deferredResult } }
Now our SomeApplicationService
becomes
class SomeApplicationService(private val deferUtil: DeferUtil) { fun createCustomerPortalSession(someDto: SomeDto): DeferredResult<Success<SomeResult>> { val deferred = deferUtil.defer { val (customerId) = someDto val params = SessionCreateParams.builder() .setCustomer(customerId) .setReturnUrl(managePlanURL) .build() val session = Session.create(params) Success(result=SomeResult(session.url)) } return deferred } }