In today's digital world, real-time communication and immediate responsiveness have become crucial for modern applications. Webhooks, a powerful mechanism for enabling instant event notifications, have emerged as a key player in achieving this real-time interaction. In this blog post, we'll learn about webhooks, exploring their significance, use cases, and the fundamental differences with pull-based API approaches. Additionally, we'll provide a step-by-step instructions on building a webhook provider using Spring Boot. Let's embark on a journey to empower your applications with seamless, event-driven communication.
Understanding Webhooks:
What are Webhooks?
Webhook is a method for one system to inform another system about events in real-time by sending HTTP requests. Rather than relying on continuous polling, webhooks enable immediate communication when specific events occur by pushing the events.
Why Webhooks?
Real-time Updates: Webhooks provide instant notifications, eliminating the need for periodic polling and ensuring real-time updates.
Efficiency: They reduce network and server load by transmitting data only when events happen, rather than at fixed intervals.
Automation: Webhooks automate the process of notifying external systems about changes, streamlining workflows and reducing manual intervention.
Event-Driven Architecture: Systems become event-driven, responding to events as they occur, leading to more responsive and agile applications.
Use Cases:
Payment Notifications:
A payment processing system can use webhooks to notify the merchant system of successful payments or chargebacks.
Order Fulfillment:
E-commerce platforms can use webhooks to inform shipping systems about new orders, updates, or cancellations.
Integration with Third-Party Services:
Integrate with external services like messaging platforms or CRMs to receive updates on user activity or interactions.
Continuous Integration/Deployment:
Trigger builds or deployments when code repositories are updated, facilitating automated workflows.
Monitoring and Alerts:
Receive immediate alerts and notifications when predefined events or errors occur in a system.
The below sequence diagram explain the Webhook communication. In the Webhook model, the Event Source (Webhook Provider) sends HTTP POST requests directly to the Event Handler (Subscriber System) when an event occurs. This eliminates the need for the Subscriber System to continuously poll the Event Source.
In the Pull model, the Consumer actively pulls data from the Event Source at regular intervals. This can lead to latency, increased network traffic, and inefficiencies compared to the push-based Webhook model.
Drawback with Pull-Based API Approach:
Latency:
In pull-based approaches, there is inherent latency as the subscriber needs to wait until the next polling cycle to receive updates.
Increased Load:
Continuous polling puts unnecessary load on both the client and server, leading to increased network traffic and server processing.
Inefficiency:
Pulling data at fixed intervals may result in redundant requests when there are no updates, leading to inefficiencies.
Limited Real-time Responsiveness:
Pull-based systems lack the real-time responsiveness provided by webhooks, especially in scenarios where immediate action is required.
Now, it's time to make our hands dirty by building an application to make Webhooks in action. We are using Spring Boot for this.
Step 1: Create a new Spring Boot Project
Use Spring Initializr (https://start.spring.io/) or your favorite IDE to create a new Spring Boot project. Include the following dependencies:
Spring Web
Spring Data JPA
H2 Database (for simplicity)
Lombok
Step 2: Define Entities
Create entities, Subscriber and Event, representing subscribers and events.
package com.dgraphtech.entities;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String eventType;
private String eventData;
}
package com.dgraphtech.entities;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Subscriber {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String callbackUrl;
}
Step 3: Create Repositories
Create repositories for Subscriber and Event to interact with the database.
package com.dgraphtech.repositories;
import com.dgraphtech.entities.Subscriber;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SubscriberRepository extends JpaRepository<Subscriber, Long> {
Optional<Subscriber> findByCallbackUrl(String callbackUrl);
}
package com.dgraphtech.repositories;
import com.dgraphtech.entities.Event;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EventRepository extends JpaRepository<Event, Long> {
}
Step 4: Create Webhook Controller
Create a controller to handle webhook registration, event publishing.
package com.dgraphtech.controller;
import com.dgraphtech.entities.Event;
import com.dgraphtech.entities.Subscriber;
import com.dgraphtech.models.Notification;
import com.dgraphtech.repositories.EventRepository;
import com.dgraphtech.repositories.SubscriberRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Optional;
@RestController
@Slf4j
public class WebhookController {
@Autowired
private SubscriberRepository subscriberRepository;
@Autowired
private EventRepository eventRepository;
@Autowired
private RestTemplate restTemplate;
/**
* This enpoint is used when subscriber subcribes with the webhook provider.
* @param subscriber
* @return
*/
@PostMapping("/subscribe")
public ResponseEntity<String> subscribe(@RequestBody Subscriber subscriber) {
Optional<Subscriber> existingSubscriber = subscriberRepository.findByCallbackUrl(subscriber.getCallbackUrl());
if (existingSubscriber.isPresent()) {
return ResponseEntity.badRequest().body("Subscriber already exists with the same callback URL");
}
subscriberRepository.save(subscriber);
return ResponseEntity.ok("Subscribed successfully");
}
/**
* This endpoint is used by webhook provider. When an event occurs, the event listener at the webhook provider will trigger this API
* to publish/notify the event to all the subscriber's subscribed to that event. Some times we don't need this endpoint, we can directly notify the
* event from the event listeners. For example, if we got event on to a queue, the queue listener can notify the event to susbscribers (Push)
* @param event
* @return
*/
@PostMapping("/publish")
public ResponseEntity<String> publishEvent(@RequestBody Event event) {
// Save the event to the database and notify the subscriber
eventRepository.save(event);
notifySubscriber(event);
return ResponseEntity.ok("Event published successfully");
}
private void notifySubscriber(Event event) {
sendHttpRequest(event);
}
private void sendHttpRequest(Event event) {
//As we are building a simple notification application, we are getting all the susbscribers to notify.
// In a robust implementation we will get only the subscribers subscribed for a specific event.
List<Subscriber> subscribers = subscriberRepository.findAll();
for (Subscriber subscriber : subscribers) {
HttpEntity<Notification> request = new HttpEntity<>(new Notification(event.getId(), event.getEventType(), event.getEventData()));
ResponseEntity<Notification> response = restTemplate.exchange(subscriber.getCallbackUrl(), HttpMethod.POST, request, Notification.class);
if (response.getStatusCode() == HttpStatus.OK) {
log.info("The notification is sent successfully!! to Subscriber :::" + subscriber.getCallbackUrl());
}
}
}
}
Step 5: Run the Application
Run the Spring Boot application. The endpoints /subscribe and /publish are now available.
package com.dgraphtech;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
@SpringBootApplication
public class WebhookExmapleApplication {
public static void main(String[] args) {
SpringApplication.run(WebhookExmapleApplication.class, args);
}
@Bean
public RestTemplate getRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
messageConverters.add(converter);
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
}
Step 6: Test Webhook Subscription and Event Publishing
Subscribe a Subscriber:
Use Postman or any HTTP client to send a POST request to http://localhost:8080/subscribe with a JSON body containing subscriber information (name and callbackUrl). As I haven't developed subscriber system, I have used request.bin for the same.
curl -XPOST -H "Content-type: application/json" -d '{
"name": "Subscriber 1",
"callbackUrl": "https://enpzzkdhln92d.x.pipedream.net/"
}' 'http://localhost:8080/subscribe'
curl -XPOST -H "Content-type: application/json" -d '{
"name": "Subscriber 2",
"callbackUrl": "https://en1wqcsn72bsxh.x.pipedream.net/"
}' 'http://localhost:8080/subscribe'
Publish an Event:
Send a POST request to http://localhost:8080/publish with a JSON body containing event information (eventType, eventData).
curl -XPOST -H "Content-type: application/json" -d '{
"eventType": "ORDER_PLACED",
"eventData": "Order ID: 12345, Total Amount: $100"
}' 'http://localhost:8080/publish'
The below picture shows that the subscriber endpoints received the event published by provider.
Step 7: Enhance Security
Implement security measures as follows:
Authentication for Subscription Endpoint:
Add authentication mechanisms (e.g., API keys, OAuth) to secure the /subscribe endpoint and ensure that only authorized entities can register as subscribers.
Payload Verification:
When sending events, sign the payload using a shared secret or HMAC to ensure the integrity and authenticity of the data.
In conclusion, the world of real-time communication is ever-evolving, and your contributions to the discussion can shape the future of how applications interact and respond to dynamic events. Thank you for joining us on this exploration, and we look forward to hearing your perspectives in the ongoing conversation around webhooks and instant communication.
The code used to demonstrate in this article is available on our GitHub repo.