Caching, Async & Scheduling
Learn caching strategies using Redis, asynchronous execution with @Async, and cron scheduling using Spring's @Scheduled.
Downloaded from PrepIQ (https://prepiq.online)
Q1. How does caching work in Spring Boot with @EnableCaching and @Cacheable?
Spring's caching abstraction lets you cache the result of an expensive method call transparently, without changing the method's logic. Adding @EnableCaching to a configuration class activates Spring's caching infrastructure, and @Cacheable("cacheName") on a method tells Spring to check the cache first — if a value exists for the given arguments (the cache key), the method body is skipped entirely and the cached value is returned.
Under the hood, Spring wraps the bean in a proxy that intercepts calls to the annotated method, so caching only works on calls made through the Spring-managed bean (external calls), not on self-invocation within the same class, similar to the @Transactional proxy limitation.
@Service
public class ProductService {
@Cacheable("products")
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow(); // only runs on cache miss
}
}
Q2. What is the difference between @Cacheable, @CachePut, and @CacheEvict?
@Cacheable checks the cache before running the method and skips execution entirely on a cache hit. @CachePut always runs the method and then updates the cache with the returned value — used when you need the method to always execute (e.g. an update operation) while still keeping the cache in sync with the latest value.
@CacheEvict removes one or more entries from the cache, typically called on delete or update operations so stale data isn't served afterward; setting allEntries = true clears the entire cache region instead of a single key, useful when many entries could be invalidated by one change.
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product); // always executes, cache is refreshed
}
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
Q3. How do you integrate Redis as a distributed cache in Spring Boot?
Spring Boot's default cache manager (ConcurrentMapCacheManager) stores cached data in local JVM memory, which doesn't work across multiple instances of a horizontally scaled application — each instance would have its own inconsistent cache. Redis solves this by acting as a shared, external, in-memory cache that every instance reads from and writes to.
Adding spring-boot-starter-data-redis and setting spring.cache.type=redis is enough for Spring Boot to auto-configure a RedisCacheManager, so existing @Cacheable, @CachePut, and @CacheEvict annotations work unchanged while the underlying storage becomes centralized, persistent (optionally), and shared across all service instances.
spring:
cache:
type: redis
data:
redis:
host: localhost
port: 6379
Q4. How does @Async work in Spring Boot and what are its limitations?
@Async, enabled with @EnableAsync, runs an annotated method on a separate thread from a configured thread pool instead of the calling thread, letting the caller continue immediately without waiting for the method to finish — useful for fire-and-forget work like sending an email or logging an audit event.
Like @Transactional and @Cacheable, it works through a Spring proxy, so it has no effect on self-invoked calls within the same class. A void-returning async method silently swallows exceptions unless an AsyncUncaughtExceptionHandler is configured; a method that needs to report success/failure back to the caller should return CompletableFuture<T> instead of void.
@Async
public CompletableFuture<Void> sendWelcomeEmail(String email) {
emailClient.send(email, "Welcome!");
return CompletableFuture.completedFuture(null);
}
Q5. How do you configure a custom thread pool for @Async tasks?
Without a custom configuration, Spring's default async executor is SimpleAsyncTaskExecutor, which creates a brand-new thread for every task instead of reusing pooled threads — fine for very light usage but risky in production since it can exhaust system resources under load with no upper bound.
A custom ThreadPoolTaskExecutor bean lets you control core pool size, max pool size, queue capacity, and thread naming, giving predictable resource usage; multiple named executors can be defined and selected per method by passing the bean name to @Async("executorName") when different tasks have different concurrency needs.
@Bean(name = "emailExecutor")
public Executor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("EmailAsync-");
executor.initialize();
return executor;
}
@Async("emailExecutor")
public void sendEmail(String to) { ... }
Q6. How does @Scheduled work in Spring Boot, and what is the difference between fixedRate, fixedDelay, and cron?
@Scheduled, enabled with @EnableScheduling, runs a void, no-argument method automatically on a recurring basis without any external trigger like a cron daemon or a message. fixedRate starts a new execution at a fixed interval measured from the start of the previous execution, regardless of how long that execution took — so overlapping runs are possible if the task runs longer than the rate.
fixedDelay waits for the fixed interval after the previous execution completes before starting the next one, guaranteeing no overlap. cron uses a cron expression for precise, calendar-based scheduling (e.g. every weekday at 2 AM) rather than a simple recurring interval, offering the most flexibility for real-world scheduling needs.
@Scheduled(fixedRate = 60000) // every 60s from start of previous run
public void syncInventory() { ... }
@Scheduled(fixedDelay = 60000) // 60s after previous run finishes
public void cleanupTempFiles() { ... }
@Scheduled(cron = "0 0 2 * * MON-FRI") // 2 AM every weekday
public void generateDailyReport() { ... }
Q7. How do you prevent scheduled tasks from running on multiple instances (Distributed Locking)?
@Scheduled runs independently on every JVM instance of an application, so a horizontally scaled service with 3 replicas would run the same scheduled job 3 times simultaneously by default — often causing duplicate emails, duplicate report generation, or race conditions on shared resources.
ShedLock is the standard solution: it uses a shared external store (a database table, Redis, or ZooKeeper) as a distributed lock, and only the instance that successfully acquires the lock for that execution window runs the task, while the others skip it. It's added declaratively with @SchedulerLock alongside the existing @Scheduled annotation, requiring minimal code changes.
@Scheduled(cron = "0 0 * * * *")
@SchedulerLock(name = "generateReport", lockAtMostFor = "10m", lockAtLeastFor = "1m")
public void generateReport() { ... }
Q8. What is the difference between CompletableFuture and @Async, and how are they used together?
CompletableFuture is a general-purpose Java class (not Spring-specific) representing a value that will be available in the future, with rich composition methods like thenApply(), thenCombine(), and allOf() for chaining and combining async operations. @Async is Spring's mechanism for actually running a method on a separate thread pool in the first place.
They're commonly combined: an @Async-annotated method returns a CompletableFuture<T>, giving the caller a handle to track completion, retrieve the result with get() or join(), and compose it with other async calls — for example, fetching data from two independent services concurrently and combining the results once both complete.
@Async
public CompletableFuture<Price> getPrice(String productId) {
return CompletableFuture.completedFuture(pricingClient.getPrice(productId));
}
CompletableFuture<Price> priceFuture = pricingService.getPrice("p1");
CompletableFuture<Stock> stockFuture = inventoryService.getStock("p1");
CompletableFuture.allOf(priceFuture, stockFuture).join();
Q9. What are the common cache eviction policies (LRU, LFU, TTL) and how do you configure them in Spring Boot (Caffeine)?
An unbounded cache eventually exhausts memory, so eviction policies decide which entries to remove once a size or time limit is reached. LRU (Least Recently Used) evicts the entry that hasn't been accessed for the longest time; LFU (Least Frequently Used) evicts the entry accessed the fewest times; TTL (Time To Live) evicts entries a fixed duration after they were written or last accessed, regardless of access frequency.
Caffeine is the recommended high-performance local cache library for Spring Boot (replacing the older Guava cache), configured with maximumSize for an LRU-like bounded cache and expireAfterWrite/expireAfterAccess for TTL-based expiration, auto-detected by Spring Boot's cache abstraction when it's on the classpath.
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
Q10. How do you handle exceptions in @Async methods, and why doesn't try-catch in the caller work?
Because an @Async method runs on a completely different thread, an exception thrown inside it cannot propagate back up the original caller's call stack the way a normal synchronous exception would — by the time the exception occurs, the calling thread has often already moved on, so wrapping the call in try-catch in the caller does nothing.
For a CompletableFuture-returning async method, the exception is captured inside the future and surfaces when the caller calls .get() (wrapped in ExecutionException) or is handled with .exceptionally()/.handle(). For a void-returning async method, exceptions are otherwise silently logged and lost unless a custom AsyncUncaughtExceptionHandler is registered via AsyncConfigurer to catch and handle them centrally.
@Bean
public AsyncConfigurer asyncConfigurer() {
return new AsyncConfigurer() {
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> log.error("Async error in {}", method.getName(), ex);
}
};
}
Caching, Async & Scheduling
Learn caching strategies using Redis, asynchronous execution with @Async, and cron scheduling using Spring's @Scheduled.
How does caching work in Spring Boot with @EnableCaching and @Cacheable?
Spring's caching abstraction lets you cache the result of an expensive method call transparently, without chan...
What is the difference between @Cacheable, @CachePut, and @CacheEvict?
@Cacheable checks the cache before running the method and skips execution entirely on a cache hit. @CachePut a...
How do you integrate Redis as a distributed cache in Spring Boot?
Spring Boot's default cache manager (ConcurrentMapCacheManager) stores cached data in local JVM memory, which...
How does @Async work in Spring Boot and what are its limitations?
@Async, enabled with @EnableAsync, runs an annotated method on a separate thread from a configured thread pool...
How do you configure a custom thread pool for @Async tasks?
Without a custom configuration, Spring's default async executor is SimpleAsyncTaskExecutor, which creates a br...
How does @Scheduled work in Spring Boot, and what is the difference between fixedRate, fixedDelay, and cron?
@Scheduled, enabled with @EnableScheduling, runs a void, no-argument method automatically on a recurring basis...
How do you prevent scheduled tasks from running on multiple instances (Distributed Locking)?
@Scheduled runs independently on every JVM instance of an application, so a horizontally scaled service with 3...
What is the difference between CompletableFuture and @Async, and how are they used together?
CompletableFuture is a general-purpose Java class (not Spring-specific) representing a value that will be avai...
What are the common cache eviction policies (LRU, LFU, TTL) and how do you configure them in Spring Boot (Caffeine)?
An unbounded cache eventually exhausts memory, so eviction policies decide which entries to remove once a size...
How do you handle exceptions in @Async methods, and why doesn't try-catch in the caller work?
Because an @Async method runs on a completely different thread, an exception thrown inside it cannot propagate...