skip to content

Scheduling, Async & Caching

The three declarative features built on proxies: @Async for off-thread execution, @Scheduled for periodic work, and the cache abstraction. Interviewers like this area because all three share the same proxy caveats and the same production failure modes.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

What happens to an exception thrown inside a void @Async method, and how is that different from a normal (synchronous) method call?

level: juniorimportance: must knowfreq 70%

answer

  1. different thread = no caller stack to throw into
  2. void = swallowed from caller, logged by default
  3. SimpleAsyncUncaughtExceptionHandler logs ERROR
  4. caller @Transactional won't roll back
  5. return CompletableFuture to observe failure

basics

~20 s

In a synchronous call the exception bubbles up to the caller. In a void @Async method it runs on another thread, so the caller never sees it — Spring catches it and, by default, just logs it.

solid answer

~40 s

A void @Async method runs on a separate thread from an executor, so the caller has already returned before the method finishes — there is no call stack to propagate the exception back to. The caller's try/catch cannot see it. Spring's async support catches the uncaught exception and hands it to an AsyncUncaughtExceptionHandler; the default (SimpleAsyncUncaughtExceptionHandler) logs it at ERROR level. So the exception is not lost, but it is effectively 'swallowed' from the caller's point of view: no rollback signal reaches the caller, no return value carries the failure. If you need the caller to observe failures, return a Future/CompletableFuture instead of void, or register a custom handler to alert/record on error.

code

java · 25 lines
java
@Service
public class NotificationService {

    // Fire-and-forget: exception is caught by Spring, logged by
    // SimpleAsyncUncaughtExceptionHandler, and NOT seen by the caller.
    @Async
    public void sendWelcomeEmail(Long userId) {
        throw new IllegalStateException("SMTP down for user " + userId);
    }

    // Returns a future: the caller CAN observe the failure.
    @Async
    public CompletableFuture<String> render(Long userId) {
        throw new IllegalStateException("render failed for " + userId);
    }
}

// Caller
service.sendWelcomeEmail(1L);   // try/catch here would never fire

service.render(1L)
       .exceptionally(ex -> {   // this DOES fire
           log.error("render failed", ex);
           return "fallback";
       });

go deeper

for a junior

Must know that void @Async exceptions don't reach the caller and are logged by default.

for a middle

Should explain the missing call stack and that caller transactions are unaffected.

for a senior

Connects to choosing Future/CompletableFuture vs void based on whether failures must be observed.

for a principal

Frames it as an at-least-once/fire-and-forget design decision with explicit failure-capture strategy.

## The core problem `@Async` (enabled by `@EnableAsync`) makes Spring run the annotated method on a **different thread**, pulled from a `TaskExecutor`. The proxy Spring creates around your bean submits the method body to that executor and immediately returns to the caller. With a **void** return type, the caller gets `void` back the instant the task is *submitted* — long before the method body actually runs. There is therefore **no shared call stack** between the caller and the async method. In normal (synchronous) Java, an exception propagates *up the call stack* to the nearest matching `catch`. When the work runs on another thread, the caller's stack is already gone, so the exception has nowhere to propagate to. ```java try { service.sendEmailAsync(); // returns immediately } catch (Exception e) { // NEVER runs for a failure inside the async body } ``` ## What Spring actually does with it The exception is **not** silently dropped into the void. Spring's `AsyncExecutionAspectSupport` wraps execution of void `@Async` methods and, on an uncaught throwable, calls an **`AsyncUncaughtExceptionHandler`**. The default implementation is **`SimpleAsyncUncaughtExceptionHandler`**, which **logs the exception at ERROR level** with the method that threw it. So by default you *do* get a stack trace in the logs — but the caller is completely unaware, and nothing downstream (a transaction rollback in the caller, an HTTP error response, a retry) is triggered. Key consequence: **`@Transactional` on the caller does not roll back** because of an async failure — the async method runs in its own thread and (if annotated) its own transaction; the caller's transaction already committed/returned. ## Why 'swallowed' is the word people use - The caller's `try/catch` is useless. - The return value (`void`) carries no failure signal. - Business flow continues as if the call succeeded. Only the log line remains — and if logging is misconfigured or the default handler is replaced badly, even that can disappear. ## The fix / alternative If the caller (or any code) must **observe** the outcome, don't return void. Return `Future<T>` or, preferably, `CompletableFuture<T>`. Then the exception is captured *inside* the future and re-thrown when someone calls `future.get()` (wrapped in `ExecutionException`) or is delivered to `.exceptionally(...)`/`.handle(...)` callbacks. For fire-and-forget void methods, register a custom `AsyncUncaughtExceptionHandler` (via `AsyncConfigurer`) to do more than log — alert, increment a metric, persist a dead-letter record. ## Gotcha summary - Void async exceptions are logged, not propagated. - Caller try/catch and caller `@Transactional` are blind to them. - Use `CompletableFuture` when the result/failure matters. - Only *uncaught* exceptions reach the handler — a `try/catch` inside the method obviously stops it.

  • If the exception is 'swallowed', is it lost entirely?
    No. Spring routes uncaught exceptions from void @Async methods to an AsyncUncaughtExceptionHandler; the default SimpleAsyncUncaughtExceptionHandler logs them at ERROR level. It is invisible to the caller but visible in the logs (unless you replace the handler).
  • Will the caller's @Transactional roll back if the async method throws?
    No. The async method runs on its own thread with its own (optional) transaction. The caller's transaction is independent and unaffected by the async failure.

saying these in an interview costs you the question

  • Claiming the caller's try/catch will catch an exception from a void @Async method
  • Saying the exception is completely lost / never logged
  • Thinking the caller's transaction rolls back when the async method throws
  • Believing @Async re-throws synchronously to the caller

context

open as a page

What does @Async do in Spring, and what must you do to switch it on?

level: juniorimportance: must knowfreq 78%

basics

~10 s

@Async marks a method so Spring runs it on a separate thread instead of the caller's thread. It only works if you add @EnableAsync (or Boot's equivalent) to a configuration class first.

open as a page

What does @EnableCaching do, and how does @Cacheable turn a method into a cached method?

level: juniorimportance: must knowfreq 78%

basics

~20 s

@EnableCaching switches on Spring's caching support (an AOP proxy). @Cacheable on a method means: before running it, look in the named cache by key; if found, return the cached value and skip the method; if not, run it and store the result.

open as a page

What are the built-in CacheManager implementations Spring ships, and when would you use each?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Spring provides ConcurrentMapCacheManager (in-memory maps, good for tests), CaffeineCacheManager (Caffeine with eviction/TTL, common in prod), NoOpCacheManager (disables caching), CompositeCacheManager (combines several), and JCacheCacheManager (JSR-107 providers).

open as a page

What is Spring's TaskDecorator, and why is it needed when running work on a thread pool like ThreadPoolTaskExecutor?

level: juniorimportance: must knowfreq 55%

basics

~20 s

TaskDecorator is an interface with one method, decorate(Runnable), that wraps every task before a ThreadPoolTaskExecutor runs it. You use it to carry thread-local context (like logging MDC or the logged-in user) from the calling thread onto the pool thread.

open as a page

How do you schedule a recurring background task in Spring, and what two things are required to make it run?

level: juniorimportance: must knowfreq 75%

basics

~10 s

Add @EnableScheduling to a @Configuration class to turn scheduling on, then put @Scheduled (with fixedRate, fixedDelay, or cron) on a no-argument, void method of a Spring bean. Spring calls it automatically on a timer.

open as a page

How do you customize what happens to uncaught exceptions from void @Async methods? Walk through AsyncConfigurer and AsyncUncaughtExceptionHandler.

level: middleimportance: must knowfreq 65%

basics

~10 s

Implement AsyncConfigurer and override getAsyncUncaughtExceptionHandler() to return your own AsyncUncaughtExceptionHandler. Its handleUncaughtException(throwable, method, params) method runs whenever a void @Async method throws, so you can alert, record, or retry instead of just logging.

open as a page

Contrast @Cacheable, @CachePut, @CacheEvict, and @Caching. When would you use each?

level: middleimportance: must knowfreq 70%

basics

~20 s

@Cacheable reads through (skips the method on a hit). @CachePut always runs the method and stores the fresh result (for updates). @CacheEvict removes entries (for deletes/invalidation). @Caching groups several of these annotations on one method.

open as a page

Explain the Trigger SPI in Spring scheduling, including CronTrigger, PeriodicTrigger, and TriggerContext.

level: middleimportance: must knowfreq 55%

basics

~20 s

Trigger is an interface with nextExecution(TriggerContext) that returns the next run time. CronTrigger computes it from a cron expression; PeriodicTrigger from a fixed period. TriggerContext gives the last scheduled/actual execution and completion times so the next time can be derived.

open as a page

How do you make SecurityContextHolder's authenticated user available inside @Async / thread-pool work, and why isn't it automatic?

level: middleimportance: must knowfreq 60%

basics

~20 s

SecurityContextHolder uses a thread-local (MODE_THREADLOCAL) that isn't shared with pool threads, so the async thread sees no user. Fix it with Spring Security's DelegatingSecurityContextAsyncTaskExecutor, or a TaskDecorator that copies the SecurityContext and clears it afterward.

open as a page

Explain the difference between @Scheduled fixedRate, fixedDelay, and initialDelay. When do fixedRate executions overlap or pile up?

level: middleimportance: must knowfreq 80%

basics

~20 s

fixedDelay measures the gap between the end of one run and the start of the next, so runs never overlap. fixedRate measures from the start of one run to the start of the next, regardless of how long the run takes. initialDelay delays only the very first execution.

open as a page

Compare exception handling for a void @Async method versus one returning Future or CompletableFuture. When would you choose each?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Void: the exception can't reach the caller, so Spring sends it to the AsyncUncaughtExceptionHandler (default: log). Future/CompletableFuture: the exception is stored in the future and re-thrown when you call get() (as ExecutionException) or handled via exceptionally/handle. Use void for fire-and-forget, futures when you need the result or failure.

open as a page

How does Spring decide which executor an @Async method uses, and how do you route a specific method to a specific pool?

level: seniorimportance: must knowfreq 58%

basics

~20 s

By default Spring looks for a single TaskExecutor bean, or one named 'taskExecutor', else falls back to SimpleAsyncTaskExecutor. To target a specific pool, put its bean name in the annotation: @Async("reportExecutor"). You can also set a global default via AsyncConfigurer.getAsyncExecutor().

open as a page

Explain the @Async self-invocation pitfall and the ways to work around it.

level: seniorimportance: must knowfreq 70%

basics

~20 s

@Async works through a proxy. If a bean calls its own @Async method directly (this.method()), the call skips the proxy and runs synchronously on the same thread. Fix it by calling through another bean, self-injecting the proxy, or using AspectJ mode.

open as a page

Explain key, keyGenerator, condition, and unless. How do they differ and when does each SpEL expression evaluate?

level: seniorimportance: must knowfreq 62%

basics

~20 s

key sets the cache key via SpEL (e.g. "#id"). keyGenerator names a bean that builds the key instead — you use one or the other, not both. condition (evaluated before the method) decides whether caching applies at all. unless (evaluated after, can see #result) vetoes storing the result.

open as a page

How would you implement a DB-driven schedule where an admin can change a job's interval without restarting the app?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Register the task with ScheduledTaskRegistrar.addTriggerTask, passing a custom Trigger whose nextExecution reads the current interval from the database each time it's called. Since Spring calls nextExecution after every run, changing the DB value changes the next scheduled time.

open as a page

What is the default thread model for @Scheduled, why is it a common production pitfall, and how do you size a ThreadPoolTaskScheduler correctly?

level: seniorimportance: must knowfreq 70%

basics

~20 s

By default Spring uses a single-threaded scheduler, so all @Scheduled methods share one thread. A slow or blocked task delays every other scheduled task. Fix it by defining a ThreadPoolTaskScheduler bean with a pool size big enough for your concurrent jobs.

open as a page

How do you make cache reads resilient to backing-store failures using CacheErrorHandler, and what are the risks?

level: principalimportance: must knowfreq 45%

basics

~20 s

Implement CacheErrorHandler and override handleCacheGetError to log and swallow the exception so a failed cache read (e.g. Redis down) becomes a miss and the method runs. The default SimpleCacheErrorHandler rethrows. Be careful swallowing put/evict errors, which can cause stale data.

open as a page

What is SchedulingConfigurer and when would you use it instead of the @Scheduled annotation?

level: juniorimportance: should knowfreq 45%

basics

~20 s

SchedulingConfigurer is an interface you implement on a @Configuration class to register scheduled tasks in code via a ScheduledTaskRegistrar, instead of the static @Scheduled annotation. Use it when the schedule must be computed at runtime.

open as a page

What is SimpleAsyncUncaughtExceptionHandler and what exactly does it do out of the box?

level: middleimportance: should knowfreq 45%

basics

~10 s

It's Spring's default AsyncUncaughtExceptionHandler for void @Async methods. When such a method throws an uncaught exception, this handler just logs it at ERROR level (with the failing method) and does nothing else.

open as a page

How do you configure the thread pool behind @Async, and what's the classic ThreadPoolTaskExecutor sizing gotcha?

level: middleimportance: should knowfreq 55%

basics

~10 s

Define a ThreadPoolTaskExecutor bean and tune corePoolSize, maxPoolSize, and queueCapacity. The gotcha: with an unbounded queue the pool never grows past corePoolSize, so maxPoolSize is effectively ignored until the queue is bounded.

open as a page

What return types can an @Async method have, and how do void vs CompletableFuture differ for the caller?

level: middleimportance: should knowfreq 60%

basics

~20 s

@Async methods can return void, Future, CompletableFuture (or the old ListenableFuture). With void it's fire-and-forget — the caller gets nothing back. With CompletableFuture the caller gets a handle to fetch the result or chain more work later.

open as a page

How do you configure CaffeineCacheManager, and how does JCacheCacheManager differ as a provider bridge?

level: middleimportance: should knowfreq 40%

basics

~20 s

Configure CaffeineCacheManager by giving it a Caffeine builder or a spec string (e.g. maximumSize, expireAfterWrite). JCacheCacheManager instead wraps a JSR-107 javax.cache.CacheManager from a provider like Ehcache 3, so the provider's own config file drives behaviour.

open as a page

Walk through implementing an MDC-propagating TaskDecorator correctly. What are the common bugs?

level: middleimportance: should knowfreq 45%

basics

~20 s

In decorate(), call MDC.getCopyOfContextMap() on the caller thread to snapshot the log context. In the returned Runnable, MDC.setContextMap(copy), run the task, and in a finally MDC.clear() (or restore the previous map). Common bugs: capturing on the pool thread, and skipping cleanup.

open as a page

Explain Spring's cron support: the field layout, the zone attribute, and how CronExpression is used. How does Spring's cron differ from classic Unix cron?

level: middleimportance: should knowfreq 60%

basics

~20 s

@Scheduled(cron = "...") uses a six-field expression: second, minute, hour, day-of-month, month, day-of-week. Unlike Unix cron it has a leading seconds field. Add zone = "Europe/Paris" to pin the time zone, and use the special value "-" to disable the task.

open as a page

What does the sync attribute on @Cacheable do, and what problem does it solve?

level: seniorimportance: should knowfreq 45%

basics

~20 s

@Cacheable(sync = true) makes concurrent misses for the same key wait so the method runs only once; the others get the value it produces. It prevents the 'thundering herd' where many threads all recompute the same missing entry at once.

open as a page

What is the CacheResolver SPI, and how does CachingConfigurer let you customize resolution globally?

level: seniorimportance: should knowfreq 35%

basics

~20 s

CacheResolver decides at runtime which Cache instances an operation uses, given the invocation context (target, method, args). The default SimpleCacheResolver just delegates to a CacheManager. Implement CachingConfigurer to supply a custom cacheResolver (or cacheManager, keyGenerator, errorHandler) globally.

open as a page

What problem does CompositeCacheManager solve, and how does fallbackToNoOpCache interact with unknown cache names?

level: seniorimportance: should knowfreq 30%

basics

~20 s

CompositeCacheManager delegates to an ordered list of CacheManagers and returns the first that owns a requested cache name, letting you mix providers. Enabling setFallbackToNoOpCache appends a NoOpCacheManager so unknown names resolve to a no-op cache instead of throwing.

open as a page

How do you cancel or reschedule a running scheduled task at runtime using ScheduledFuture and TaskScheduler?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Schedule the task via a TaskScheduler, which returns a ScheduledFuture. Keep that reference; call future.cancel(mayInterruptIfRunning) to stop future executions. To reschedule, cancel the old future and call scheduler.schedule(...) again to get a new one.

open as a page

How does Micrometer's context-propagation library (ContextSnapshot / ThreadLocalAccessor) unify context propagation, and how does Spring integrate it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Micrometer's context-propagation library defines ThreadLocalAccessor SPI implementations for each thread-local (MDC, tracing, security). ContextSnapshot.captureAll() grabs all registered ones at once; wrapping a Runnable restores and then clears them on the pool thread. Spring 6.1's ContextPropagatingTaskDecorator plugs this into any executor.

open as a page

showing 1–30 of 36