Spring Boot Scheduling & Async: @Scheduled, @Async, TaskExecutor

Learn Spring Boot task scheduling with @Scheduled, asynchronous processing with @Async, and custom TaskExecutor configuration.

published: reading time: 21 min read author: GeekWorkBench
Quick Summary

Learn Spring Boot task scheduling with @Scheduled, asynchronous processing with @Async, and custom TaskExecutor configuration. The guide uses practical examples to explain introduction: task scheduling vs async processing, @scheduled: fixed rate, fixed delay, and cron and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

Spring Boot Scheduling & Async: @Scheduled, @Async, TaskExecutor

Building enterprise Java applications often means dealing with workloads that should not block the main request thread. Whether you need to run cleanup jobs on a fixed schedule, send confirmation emails after a user signs up, or aggregate reports in the background, Spring Boot provides first-class primitives for these scenarios. This post covers everything you need to know about @Scheduled, @Async, and the TaskExecutor abstraction that ties them together.

Introduction: Task Scheduling vs Async Processing

Introduction

Spring Boot scheduling and asynchronous execution let applications run recurring or slow work outside the request path. This guide explains @Scheduled, task executors, async method behavior, thread-pool sizing, error handling, and the coordination concerns that appear when jobs run across multiple application instances.

@Scheduled: Fixed Rate, Fixed Delay, and Cron

The @Scheduled annotation marks a method for execution at fixed intervals or cron expressions. By default, scheduling is disabled, so you must opt in with @EnableScheduling.

Enabling Scheduling

@SpringBootApplication
@EnableScheduling
public class SchedulingApplication {

    public static void main(String[] args) {
        SpringApplication.run(SchedulingApplication.class, args);
    }
}

Once enabled, any bean method annotated with @Scheduled will run according to its configured schedule.

Fixed Rate vs Fixed Delay

Spring Boot supports two fixed-time strategies.

Fixed rate executes the task at a steady interval, measured from the start of one execution to the start of the next:

@Scheduled(fixedRate = 5000)
public void pollEveryFiveSeconds() {
    // Runs every 5 seconds regardless of how long the previous run took
}

Fixed delay executes the task at a steady interval, measured from the end of one execution to the start of the next:

@Scheduled(fixedDelay = 5000)
public void cleanupAfterCompletion() {
    // Always waits 5 seconds after the previous run finishes
}

Fixed rate is useful when you want consistent throughput regardless of task duration. Fixed delay is safer when tasks must not overlap, such as when writing to a shared resource.

Both variants support an initialDelay parameter to defer the first execution:

@Scheduled(fixedDelay = 5000, initialDelay = 10000)
public void startAfterDelay() {
    // Waits 10 seconds before the first run, then every 5 seconds after
}

Cron Expressions

For calendar-based scheduling, use cron expressions:

@Scheduled(cron = "0 0 2 * * MON-FRI")
public void runWeekdayMorningJob() {
    // Runs at 2:00 AM every weekday
}

Spring supports cron expressions with six fields: second minute hour day month weekday. You can also use special characters like *, ?, -, and /.

The @Scheduled annotation also supports time zones:

@Scheduled(cron = "0 0 9 * * *", zone = "America/New_York")
public void runInEasternTime() {
    // Runs at 9 AM Eastern Time
}

Configuration Properties

Spring Boot exposes sensible defaults through application.properties:

spring.task.scheduling.thread-name-prefix=scheduled-
spring.task.scheduling.pool.size=1

The pool size controls how many scheduled tasks can run concurrently. The default of one thread means tasks run sequentially.

@Async: Background Execution with CompletableFuture

While @Scheduled handles time-based triggers, @Async handles event-based handoffs. When a method is marked @Async, Spring proxies the call and executes it on a separate thread, allowing the caller to continue without waiting.

Enabling Async Processing

@SpringBootApplication
@EnableAsync
public class AsyncApplication {

    public static void main(String[] args) {
        SpringApplication.run(AsyncApplication.class, args);
    }
}

Return Types

@Async methods can return three kinds of values, each with different semantics.

Void return type is the simplest case:

@Async
public void sendEmail(String recipient) {
    // Runs in background, caller gets control back immediately
}

Future<V> return type lets callers retrieve the result later:

@Async
public Future<String> fetchReport(String reportId) {
    String report = generateReport(reportId);
    return new AsyncResult<>(report);
}

CompletableFuture<V> return type is the modern, more flexible option:

@Async
public CompletableFuture<Report> generateReport(String reportId) {
    Report report = buildReport(reportId);
    return CompletableFuture.completedFuture(report);
}

CompletableFuture supports chaining, composition, and error handling in ways that plain Future does not.

Exception Handling in @Async

By default, exceptions from @Async methods go unhandled unless you configure a handler. Spring provides AsyncUncaughtExceptionHandler:

@Component
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        log.error("Async method {} failed with exception", method.getName(), ex);
    }
}

Wire it into your async configuration:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

TaskExecutor Configuration: ThreadPoolTaskExecutor and TaskScheduler

Under the hood, both @Scheduled and @Async rely on an Executor abstraction. Spring Boot auto-configures sensible defaults, but production systems almost always need custom tuning.

ThreadPoolTaskExecutor

ThreadPoolTaskExecutor is the standard Spring abstraction for a configurable thread pool:

@Configuration
public class TaskExecutorConfig {

    @Bean
    public TaskExecutor applicationTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(250);
        executor.setThreadNamePrefix("app-");
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }
}

Key configuration parameters:

  • corePoolSize: Minimum number of threads kept alive even when idle.
  • maxPoolSize: Maximum number of threads created under load.
  • queueCapacity: Number of tasks that can wait in the queue before the pool expands.
  • threadNamePrefix: Prefix for thread names, useful in log diagnostics.

Pool Sizing Formula

A reasonable starting point for CPU-bound work follows the formula:

threads = number of CPU cores + 1

For IO-bound work, where threads spend time waiting:

threads = number of CPU cores * target CPU utilization * (1 + wait time / service time)

Measure before tuning. Generic formulas are only rough guides.

TaskScheduler

TaskScheduler handles @Scheduled execution. You can expose a custom scheduler:

@Bean
public TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    scheduler.setPoolSize(5);
    scheduler.setThreadNamePrefix("scheduler-");
    scheduler.initialize();
    return scheduler;
}

Spring Boot auto-configures this bean when you set spring.task.scheduling properties.

When to Use @Scheduled vs @Async vs Manual Threading

Not every background workload fits the same abstraction. Here is a practical decision framework.

Scenario Recommendation Reason
Cleanup job every night at 2 AM @Scheduled with cron Time-based, must run regardless of user activity
Send welcome email after signup @Async void method Event-triggered, should not block the HTTP response
Generate a report on demand but do not wait @Async returning CompletableFuture Caller can proceed, result available later
Process items from a queue continuously @Scheduled with fixedRate or a @Async loop Continuous work with controlled concurrency
Run a long computation in parallel with others CompletableFuture.allOf() with dedicated Executor Fine-grained control over concurrency and composition
One-off background work with no timing need @Async Simplest handoff model

When NOT to Use @Async

  • In constructors: Spring proxies do not intercept self-invocation or object construction. Use @PostConstruct or a separate initializer method instead.
  • With @Transactional self-invocation: Calling an @Async method on this bypasses the proxy, so the method runs synchronously. Use a separate bean reference.
  • For fire-and-forget without monitoring: Background failures go silent without observability. Ensure metrics or logging are in place.

Async Flow Diagram

The following diagram shows how a typical @Async call flows through the system.

graph TD
    A[HTTP Request] --> B[Controller]
    B --> C["@Async Method Call"]
    C --> D["Spring AOP Proxy"]
    D --> E["TaskExecutor Thread Pool"]
    E --> F["Background Thread"]
    F --> G["Actual Method Execution"]
    G --> H["CompletableFuture or void"]
    B --> I[HTTP Response Returned Immediately]
    H --> J[Result available for caller]

The HTTP response returns to the client while the actual work continues on a separate thread.

Implementation Snippets

@Scheduled with Cron

@Service
public class ReportScheduler {

    private final ReportService reportService;

    public ReportScheduler(ReportService reportService) {
        this.reportService = reportService;
    }

    @Scheduled(cron = "0 0 3 * * SAT")
    public void generateWeeklyReport() {
        log.info("Starting weekly report generation");
        reportService.buildAndStoreWeeklyReport();
        log.info("Weekly report generation complete");
    }
}

@Async with CompletableFuture

@Service
public class UserService {

    private final TaskExecutor taskExecutor;

    public UserService(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    @Async
    public CompletableFuture<UserProfile> enrichUserProfile(String userId) {
        UserProfile profile = fetchBasicProfile(userId);
        profile.setSocialData(fetchSocialData(userId));
        profile.setRecommendation(fetchRecommendations(userId));
        return CompletableFuture.completedFuture(profile);
    }

    private UserProfile fetchBasicProfile(String userId) { /* ... */ }
    private SocialData fetchSocialData(String userId) { /* ... */ }
    private Recommendation fetchRecommendations(String userId) { /* ... */ }
}

Custom ThreadPoolTaskExecutor Configuration

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "customTaskExecutor")
    public Executor customTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(32);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("custom-async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        return executor;
    }
}

Apply this executor to a specific method:

@Async("customTaskExecutor")
public void processLargeBatch(List<Item> items) {
    items.forEach(this::processItem);
}

Failure Scenarios

Understanding what can go wrong helps you build resilient systems.

Lost Updates with Fixed Rate

If a task takes longer than the fixed rate interval, the next execution starts immediately after the current one finishes. This can cause race conditions if the task writes to shared state:

@Scheduled(fixedRate = 1000)
public void incrementCounter() {
    int current = counter.get();      // Read
    counter.set(current + 1);         // Write
    // If two executions overlap, one increment is lost
}

Use fixedDelay if tasks must not overlap, or add distributed locking if they run on multiple instances.

Thread Pool Exhaustion

If all async threads are busy and the queue fills up, the RejectedExecutionHandler determines what happens. The default AbortPolicy throws a RejectedExecutionException. Using CallerRunsPolicy makes the calling thread execute the task, which can degrade response times but prevents silent failures.

@Async in Constructor

Spring’s AOP proxies are not active during object construction. If you call @Async methods in a constructor, the call bypasses the proxy and runs synchronously:

@Service
public class MyService {

    public MyService() {
        // This runs synchronously, not asynchronously!
        doAsyncWork();
    }

    @Async
    public void doAsyncWork() { }
}

Move async calls to @PostConstruct or an event listener.

Transaction Context with @Async

@Async methods run on different threads, which means transaction context is not propagated by default. If you need a transaction:

@Async
@Transactional
public void processWithTransaction(String id) {
    // Transaction context is NOT propagated automatically
}

Use TransactionTemplate or propagate the transaction manually if required.

Trade-off Table

Feature @Scheduled @Async Manual Threading
Trigger Time-based (fixed rate, fixed delay, cron) Event-based (method call) Fully custom
Return value None void, Future, CompletableFuture Fully custom
Concurrency control Pool size config Pool size config Fully custom
Transaction support No automatic propagation No automatic propagation Fully custom
Exception handling AsyncUncaughtExceptionHandler AsyncUncaughtExceptionHandler Manual try-catch
Use case fit Recurring jobs, batch processing Non-blocking handoffs, parallel work Complex coordination
Complexity Low Medium High
Thread management Automatic via TaskScheduler Automatic via TaskExecutor Manual

Observability Checklist

Monitoring scheduled and async tasks is critical for production reliability.

  • Log task start and completion: Add structured logging to track execution duration.
  • Track thread pool metrics: Monitor activeCount, queueSize, and poolSize via JMX or Micrometer.
  • Set timeouts: Use CompletableFuture.orTimeout() to prevent indefinite blocking.
  • Expose actuator endpoints: Enable /actuator/scheduledtasks to inspect registered schedules.
  • Alert on failures: Configure AsyncUncaughtExceptionHandler to emit metrics or alerts.
  • Correlate with traces: Add trace IDs to async threads using CompletableFuture.thenApply().

Security Notes

Running code in background threads introduces security considerations that are easy to overlook.

Security Context Propagation

Spring Security’s SecurityContext is stored in a ThreadLocal. When @Async dispatches to a different thread, the security context is not propagated by default. Any @Async method that relies on SecurityContextHolder.getContext().getAuthentication() will find it empty.

If you need the security context in async methods:

@Bean
public TaskDecorator securityContextTaskDecorator() {
    return Runnable::run;
}

Or use Spring Security’s DelegatingSecurityContextRunnable:

Executor executor = new TaskExecutorBuilder()
    .taskDecorator(new DelegatingSecurityContextTaskDecorator())
    .build();

@Scheduled with Security Context

The same issue applies to @Scheduled tasks. If a scheduled job needs the current user’s context, it must be passed explicitly as a parameter.

Principle of Least Privilege for Thread Pools

Run background tasks with the minimum required privileges. If a scheduled job only reads data, its service method should not require write permissions. Design tasks as discrete units that carry only the permissions they actually need.

Common Pitfalls / Anti-Patterns

Fixed Rate vs Fixed Delay Drift

fixedRate does not account for task duration. Under sustained load, executions can drift later and later (a “busy period”). If drift is unacceptable, use fixedDelay with a sufficient gap, or implement your own “next run at fixed calendar time” logic using TaskScheduler.

@Async Does Not Work with @Transactional Self-Invocation

Spring’s proxy-based AOP only intercepts calls that go through the proxy. Calling this.asyncMethod() from within the same class bypasses the proxy entirely:

@Service
public class OrderService {

    public void placeOrder(Order order) {
        // This calls the proxy, runs async
        ((OrderService) AopContext.currentProxy()).sendConfirmation(order);
    }

    @Async
    public void sendConfirmation(Order order) {
        // Runs async when called via proxy
    }
}

Avoid this by injecting the bean into itself or using AopContext.currentProxy().

Forgetting @EnableScheduling or @EnableAsync

A missing @EnableScheduling annotation silently causes scheduled tasks to never run. Similarly, @Async methods run synchronously without @EnableAsync. These are easy to overlook during refactoring.

Blocking in Async Methods

Async methods should not block on other async operations. Use CompletableFuture chaining instead of .get() or .join() within the async chain.

Production Failure Scenarios

Scheduled and async tasks fail in ways that are hard to reproduce locally but surface under load or specific timing conditions.

Task Overlap with Fixed Rate Under Sustained Load

fixedRate measures time from the start of one execution to the start of the next. If a task takes longer than the interval, the next execution starts immediately after the current one finishes with no gap. For tasks that write to shared state, this causes race conditions. A counter increments twice with only one increment recorded, or a scheduled job that sends an email sends it twice. Use fixedDelay when tasks must not overlap. If fixedRate is required, add application-level overlap detection or use distributed locking.

Async Thread Pool Exhaustion Silently Dropping Work

When all async threads are busy and the queue is full, the RejectedExecutionHandler takes over. The default AbortPolicy throws RejectedExecutionException. If this exception is caught and swallowed, the work is silently dropped. Background tasks that send notifications, update search indexes, or record analytics disappear without warning. Use CallerRunsPolicy during development to surface the problem immediately, and configure monitoring that alerts when thread pool queue depth exceeds normal levels.

Transaction Context Not Propagating to Async Threads

@Transactional does not propagate across thread boundaries. An @Async method called from inside a @Transactional method runs on a different thread without the transaction context. Any database operations in the async method run outside the transaction, which can cause inconsistent state if the outer transaction later rolls back. Use TransactionTemplate explicitly inside async methods, or restructure so database operations stay in the synchronous call chain.

Lost Security Context in Background Threads

Spring Security’s SecurityContext lives in a ThreadLocal. When @Async dispatches to a thread pool, the security context does not follow. Any code in the async method that calls SecurityContextHolder.getContext().getAuthentication() gets an empty context. Users appear unauthenticated inside background tasks. Configure DelegatingSecurityContextTaskDecorator on your TaskExecutor so the security context is copied to the async thread before execution.

Quick Recap Checklist

  • Add @EnableScheduling to enable @Scheduled methods.
  • Add @EnableAsync to enable @Async methods.
  • Use fixedRate when tasks should run at steady intervals regardless of duration.
  • Use fixedDelay when tasks must not overlap.
  • Prefer CompletableFuture over plain Future for async return types.
  • Configure ThreadPoolTaskExecutor pool size based on workload characteristics.
  • Handle exceptions in async methods with AsyncUncaughtExceptionHandler.
  • Security context does not propagate to async threads by default.
  • Do not call @Async methods via this — use the proxy.
  • Monitor thread pool metrics and set timeouts on long-running tasks.
  • Use @Transactional with caution in async contexts — transaction context does not propagate.

Interview Questions

1. What is the difference between @Scheduled fixedRate and fixedDelay?

fixedRate measures time from the start of one execution to the start of the next. fixedDelay measures time from the end of one execution to the start of the next. If a task takes longer than the interval, fixedRate will run the next iteration as soon as the current one finishes, while fixedDelay will always wait for the current iteration to complete first.

2. Why does @Async not work when calling a method from within the same class?

Spring uses proxy-based AOP for @Async. When you call this.asyncMethod() from within the same bean, you bypass the proxy and call the target method directly on the current thread. The solution is to inject the bean into itself or use AopContext.currentProxy() to invoke through the proxy.

3. How do you handle exceptions in @Async methods?

Implement the AsyncUncaughtExceptionHandler interface and register it by overriding getAsyncUncaughtExceptionHandler() in an AsyncConfigurer configuration class. This handler receives any exception thrown by an async method that is not caught within the method body itself.

4. What happens when the async thread pool is exhausted?

When the queue is full and all threads are busy, the RejectedExecutionHandler takes over. The default AbortPolicy throws a RejectedExecutionException. You can switch to CallerRunsPolicy to make the calling thread execute the task, or implement a custom policy for custom behavior such as logging and alerting.

5. Does Spring Security context propagate to @Async threads?

No, by default the SecurityContext stored in ThreadLocal does not propagate across thread boundaries. If you need the security context in async methods, configure a DelegatingSecurityContextTaskDecorator as the TaskDecorator for your async executor so that the security context is copied to the async thread.

6. What is the purpose of the initialDelay parameter in @Scheduled?

The initialDelay parameter defers the first execution of a scheduled task by the specified milliseconds. Both fixedRate and fixedDelay support this parameter. It is useful when you want to give the application time to fully start up before a scheduled task begins running, or to stagger the first run of multiple scheduled tasks.

7. How does @Transactional interact with @Async methods?

@Transactional does not automatically propagate across thread boundaries. When an @Async method is called, it runs on a different thread that does not share the caller's transaction context. Any database operations in the async method execute outside the transaction, which can cause inconsistent state if the outer transaction rolls back. Use TransactionTemplate explicitly inside async methods or restructure the code to keep database operations in the synchronous call chain.

8. What is the difference between Future and CompletableFuture as return types for @Async methods?

Future provides basic asynchronous result retrieval with get() and cancel() methods, but does not support chaining or composition. CompletableFuture is the modern alternative that supports chaining via thenApply(), thenCompose(), and error handling via exceptionally() or handle(). It also supports allOf() and anyOf() for parallel composition of multiple async operations.

9. What is the formula for sizing thread pools for CPU-bound vs IO-bound work?

For CPU-bound work: threads = number of CPU cores + 1. For IO-bound work where threads spend time waiting: threads = number of CPU cores * target CPU utilization * (1 + wait time / service time). These are starting points only — actual sizing requires measurement under production load. Generic formulas provide rough guidance; tuning should be data-driven.

10. Why should you avoid calling @Async methods from constructors?

Spring's AOP proxies are not active during object construction. When you call an @Async method from within a constructor, the call bypasses the proxy and runs synchronously on the constructing thread. Additionally, the object is not fully initialized when the async method executes. Move async invocations to @PostConstruct or an application event listener to ensure the proxy is properly engaged.

11. What is CallerRunsPolicy and when would you use it?

CallerRunsPolicy is a RejectedExecutionHandler that makes the calling thread execute the task when the pool and queue are saturated. It prevents silent task dropping but can degrade response times since the caller blocks. It is useful during development to surface backpressure immediately, and can be used in production with proper monitoring when graceful degradation is acceptable.

12. How do you configure different thread pools for different @Async methods?

Define multiple TaskExecutor beans with specific names, then reference them explicitly on each @Async method using the bean name: @Async("emailTaskExecutor") or @Async("reportTaskExecutor"). This allows you to isolate workloads, apply different pool sizes, and prevent one noisy task from affecting others.

13. What happens if you forget @EnableScheduling or @EnableAsync?

If @EnableScheduling is missing, scheduled tasks are silently ignored — the methods run once at startup and never again. If @EnableAsync is missing, @Async methods execute synchronously on the calling thread as if the annotation were not there. Both are easy to overlook during refactoring, so they should be covered by integration tests.

14. How does fixedRate behave under sustained load with long-running tasks?

With fixedRate, subsequent executions are scheduled from the start time of the previous iteration. If a task takes longer than the interval, the next iteration begins immediately after the current one finishes with no gap. This can cause task overlap and race conditions on shared state. Under sustained heavy load, execution times can drift later and later. Use fixedDelay when tasks must not overlap, or add application-level overlap detection.

15. How do you correlate async operations across distributed traces?

Use CompletableFuture.thenApply() to propagate trace IDs to async threads. Alternatively, configure a TaskDecorator on your TaskExecutor that copies the current trace context (from Tracer.currentSpan() in Spring Cloud Sleuth or Baggage in Micrometer) into the new thread's context before execution. Without this, async operations appear as separate unconnected traces.

Further Reading

Topic-Specific Deep Dives

Custom TaskExecutor Beans: When you expose a TaskExecutor bean, Spring Boot uses it as the default for both @Async and @EnableAsync. You can also name specific executors (@Async(“customTaskExecutor”)) to route different workloads to different thread pools.

Scheduling Across Time Zones: The zone attribute on @Scheduled accepts any IANA time zone identifier. This is critical for global applications where business hours differ by region.

Health Indicators: Spring Boot Actuator auto-configures a TaskExecutorHealthIndicator that reports thread pool state. Combine this with liveness and readiness probes in Kubernetes for proper shutdown handling.

Conclusion

Spring Boot’s scheduling and async processing capabilities provide the building blocks for handling background work in enterprise applications, but they come with subtle pitfalls that can cause production incidents. The core principle to internalize is that Spring’s annotation-based async and scheduling use proxy-based AOP, which means self-invocation bypasses the proxy entirely and operations run synchronously without warning.

The most impactful habits: always use fixedDelay when tasks must not overlap, configure DelegatingSecurityContextTaskDecorator for async methods that need security context, size thread pools based on actual workload characteristics rather than guesswork, and expose the Actuator scheduled tasks endpoint to get visibility into what is running and when. Exception handling deserves attention too — background failures without a custom AsyncUncaughtExceptionHandler are invisible until they surface as data inconsistencies or missed SLAs.

When done well, background processing improves application responsiveness and throughput without complicating the request-response cycle. When done poorly, async failures become silent data corruption that is difficult to reproduce and diagnose. Invest in observability for background tasks from the start.

Category

Related Posts

Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

#spring-boot #spring-boot-roadmap #learning-path

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path