Spring
Non-blocking
Rest API
Send and Forget
Async Programming

Spring Non-blocking Rest Send and forget

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In Spring, "send and forget" means firing an HTTP request without waiting for or caring about the response. This is useful for non-critical operations like logging, analytics, notifications, or triggering background jobs. Spring provides several ways to implement this pattern: @Async with RestTemplate, WebClient (reactive), and CompletableFuture.

Why Send and Forget?

Typical use cases where you do not need the response:

  • Sending audit logs to a logging service
  • Triggering a webhook notification
  • Posting analytics events
  • Invalidating an external cache
  • Notifying a downstream service about a state change

The caller should not block or slow down waiting for these non-critical operations to complete.

Approach 1: @Async with RestTemplate

The simplest approach uses Spring's @Async annotation to run the REST call on a separate thread:

java
1@Configuration
2@EnableAsync
3public class AsyncConfig {
4    @Bean
5    public Executor taskExecutor() {
6        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
7        executor.setCorePoolSize(5);
8        executor.setMaxPoolSize(10);
9        executor.setQueueCapacity(100);
10        executor.setThreadNamePrefix("async-rest-");
11        executor.initialize();
12        return executor;
13    }
14}
java
1@Service
2public class NotificationService {
3
4    private final RestTemplate restTemplate;
5
6    public NotificationService(RestTemplate restTemplate) {
7        this.restTemplate = restTemplate;
8    }
9
10    @Async
11    public void sendNotification(String event, Map<String, Object> payload) {
12        try {
13            restTemplate.postForEntity(
14                "https://hooks.example.com/notify",
15                payload,
16                Void.class
17            );
18        } catch (Exception e) {
19            // Log but don't propagate — caller already moved on
20            log.warn("Notification failed for event {}: {}", event, e.getMessage());
21        }
22    }
23}

The caller invokes sendNotification() and continues immediately. The HTTP call runs asynchronously on the thread pool.

Approach 2: WebClient (Reactive, Non-blocking)

Spring WebFlux's WebClient is truly non-blocking — it does not tie up a thread while waiting for the response:

java
1@Service
2public class AnalyticsService {
3
4    private final WebClient webClient;
5
6    public AnalyticsService(WebClient.Builder builder) {
7        this.webClient = builder.baseUrl("https://analytics.example.com").build();
8    }
9
10    public void trackEvent(String eventName, Map<String, Object> data) {
11        webClient.post()
12            .uri("/events")
13            .bodyValue(Map.of("event", eventName, "data", data))
14            .retrieve()
15            .toBodilessEntity()
16            .subscribe(
17                response -> log.debug("Event tracked: {}", eventName),
18                error -> log.warn("Failed to track event: {}", error.getMessage())
19            );
20        // Returns immediately — subscribe() handles the response asynchronously
21    }
22}

The key is calling .subscribe() instead of .block(). The subscribe() callback handles the response (or error) when it eventually arrives, while the calling thread continues.

Approach 3: CompletableFuture

For more control over async execution:

java
1@Service
2public class WebhookService {
3
4    private final RestTemplate restTemplate;
5    private final Executor executor;
6
7    public WebhookService(RestTemplate restTemplate,
8                          @Qualifier("taskExecutor") Executor executor) {
9        this.restTemplate = restTemplate;
10        this.executor = executor;
11    }
12
13    public void fireWebhook(String url, Object payload) {
14        CompletableFuture.runAsync(() -> {
15            try {
16                restTemplate.postForEntity(url, payload, Void.class);
17            } catch (Exception e) {
18                log.warn("Webhook to {} failed: {}", url, e.getMessage());
19            }
20        }, executor);
21    }
22}

Approach 4: ApplicationEventPublisher

Decouple the send-and-forget from the caller entirely using Spring events:

java
1// Define the event
2public record NotificationEvent(String type, Map<String, Object> payload) {}
3
4// Publish the event (fire-and-forget from caller's perspective)
5@Service
6public class OrderService {
7    private final ApplicationEventPublisher publisher;
8
9    public OrderService(ApplicationEventPublisher publisher) {
10        this.publisher = publisher;
11    }
12
13    public void placeOrder(Order order) {
14        // Business logic...
15        publisher.publishEvent(new NotificationEvent("ORDER_PLACED", Map.of("orderId", order.getId())));
16    }
17}
18
19// Handle the event asynchronously
20@Component
21public class NotificationEventHandler {
22    @Async
23    @EventListener
24    public void handle(NotificationEvent event) {
25        // Make the REST call here
26        restTemplate.postForEntity(webhookUrl, event.payload(), Void.class);
27    }
28}

Comparison of Approaches

ApproachThread ModelRequires WebFluxError Handling
@Async + RestTemplateThread pool (blocking I/O)NoIn async method
WebClient + subscribe()Event loop (non-blocking)YesIn subscribe callback
CompletableFutureThread pool (blocking I/O)NoIn runAsync lambda
ApplicationEventPublisherDecoupled via eventsNoIn event listener

Common Pitfalls

  • Lost exceptions: Since the caller does not wait for the result, exceptions are silently lost unless you log them in the async method. Always add error handling inside the fire-and-forget code.
  • Thread pool exhaustion: @Async with RestTemplate uses a thread pool. If the downstream service is slow, all threads can be blocked waiting for responses. Configure maxPoolSize and queueCapacity appropriately, and set timeouts on RestTemplate.
  • Missing @EnableAsync: The @Async annotation does nothing without @EnableAsync on a configuration class. The method will run synchronously without it.
  • Self-invocation: Calling an @Async method from within the same class bypasses the proxy — the method runs synchronously. Inject the service into another bean or use ApplicationEventPublisher instead.
  • Transaction context: @Async methods run in a separate thread with no access to the caller's transaction. If the caller rolls back, the async method has already fired.

Summary

  • Use @Async + RestTemplate for simple fire-and-forget in servlet-based Spring apps
  • Use WebClient with .subscribe() for truly non-blocking send-and-forget in reactive apps
  • Always log errors in async methods — exceptions are otherwise silently lost
  • Configure thread pool limits and HTTP timeouts to prevent resource exhaustion
  • Use ApplicationEventPublisher to fully decouple the sender from the HTTP call

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.