How to Implement Reliable Webhooks in Spring Boot 3 with WebClient

This article explains how to build outbound Webhooks in Spring Boot 3 using the non‑blocking WebClient, demonstrates event‑driven publishing, shows code for custom events, retry mechanisms, and provides practical examples for integrating microservices with reliable HTTP callbacks.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Implement Reliable Webhooks in Spring Boot 3 with WebClient

Introduction

Sending data from one system to another is a common requirement, especially when using microservices or integrating with external services. One way to achieve this is by triggering outbound Webhooks, which are simple HTTP requests sent when specific events occur inside an application.

Webhook Basics

A Webhook is essentially an outbound HTTP POST request that carries event data, usually in JSON format, to a predefined URL. No extra libraries are required; the request is constructed and sent directly.

{
  "event": "order_created",
  "data": {
    "id": 102,
    "createdAt": "2025-06-08 18:20:46"
  }
}

2. Practical Cases

2.1 How to Send HTTP Requests

In older Spring projects RestTemplate was used for synchronous HTTP calls, but the modern recommendation is to use WebClient from spring-boot-starter-webflux, which offers a non‑blocking API suitable for both traditional MVC and reactive applications.

The RestClient offers a more modern API for synchronous HTTP access. For asynchronous and streaming scenarios, consider the reactive WebClient.

Define a shared WebClient bean to avoid repetitive configuration:

@Configuration
public class WebhookClientConfig {
  @Bean
  public WebClient webClient(WebClient.Builder builder) {
    return builder
        .baseUrl("http://www.pack.com")
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .build();
  }
}

2.2 Simple Example

Use the injected WebClient to send a simple Webhook request:

@Component
public class WebhookSender {
  private final WebClient webClient;

  public WebhookSender(WebClient webClient) {
    this.webClient = webClient;
  }

  public void sendRecordCreatedEvent(Long recordId) {
    Map<String, Object> payload = Map.of(
        "event", "order_created",
        "data", Map.of("id", recordId, "timestamp", Instant.now().toString())
    );
    webClient.post()
        .uri("/webhook-endpoint")
        .bodyValue(payload)
        .retrieve()
        .toBodilessEntity()
        .subscribe(); // triggers the call
  }
}

The subscribe() call makes the request execute; no need to understand Mono or Flux for fire‑and‑forget scenarios.

2.3 Event‑Driven Webhook Logic

Separate the trigger from the sending logic by using Spring’s event mechanism.

Custom Event

public class OrderCompletedEvent {
  private final String orderNo;
  public OrderCompletedEvent(String orderNo) { this.orderNo = orderNo; }
  public String orderNo() { return orderNo; }
}

Publishing the Event

@Service
public class OrderService {
  private final ApplicationEventPublisher publisher;

  public OrderService(ApplicationEventPublisher publisher) {
    this.publisher = publisher;
  }

  public void completeOrder(Order data) {
    String orderNo = createOrder(data);
    publisher.publishEvent(new OrderCompletedEvent(orderNo));
  }

  @Transactional
  public String createOrder(Order data) {
    // ... persist order ...
    return data.getOrderNo();
  }
}

Listening and Sending Webhook

@Component
public class WebhookNotifier {
  private final WebClient webClient;
  private final OrderRepository orderRepository;

  public WebhookNotifier(WebClient webClient, OrderRepository orderRepository) {
    this.webClient = webClient;
    this.orderRepository = orderRepository;
  }

  @EventListener(OrderCompletedEvent.class)
  @Async
  public void onPurchaseCompleted(OrderCompletedEvent event) {
    String orderNo = event.orderNo();
    Order order = orderRepository.findByOrderNo(orderNo).orElse(null);
    if (order == null) return;
    Map<String, Object> payload = Map.of(
        "event", "order_completed",
        "data", Map.of(
            "id", order.getId(),
            "price", order.getAmount(),
            "timestamp", order.getCreatedAt().toString()
        )
    );
    webClient.post()
        .uri("http://www.pack.com/webhooks/order")
        .bodyValue(payload)
        .retrieve()
        .toBodilessEntity()
        .subscribe();
  }
}

2.4 Reliability Enhancements

Network failures can cause HTTP requests to fail. Use reactive operators such as retryWhen to automatically retry a limited number of times.

webClient.post()
    .uri("http://www.pack.com/webhooks/order")
    .bodyValue(payload)
    .retrieve()
    .toBodilessEntity()
    .retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(4)))
    .subscribe();

This simple retry mechanism attempts the request up to three times with a fixed delay. Further enhancements could include logging failures or queuing them for later processing.

By keeping Webhook logic isolated from business code and handling errors gracefully, the system remains stable and easier to maintain.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backendjavaspring-bootwebclientwebhookEvent-Driven
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.