The difference has to do with the Executor that is responsible for running the code. Each operator on CompletableFuture generally has 3 versions.
thenApply(fn)– runsfnon a thread defined by theCompleteableFutureon which it is called, so you generally cannot know where this will be executed. It might immediately execute if the result is already available.thenApplyAsync(fn)– runsfnon a environment-defined executor regardless of circumstances. ForCompletableFuturethis will generally beForkJoinPool.commonPool().thenApplyAsync(fn,exec)– runsfnonexec.
In the end the result is the same, but the scheduling behavior depends on the choice of method.