๐ฏ Introduction
Webhooks are HTTP callbacks that enable real-time, event-driven communication between applications. Instead of continuously polling for updates, webhooks allow systems to push data immediately when events occur. This comprehensive guide explores webhook architecture, compares different integration approaches, and provides production-ready Java implementations with real-world examples from Stripe, Shopify, and GitHub.
Webhooks have become essential for modern distributed systems, enabling efficient, scalable, and responsive integrations that power everything from payment processing to CI/CD pipelines and e-commerce automation.
๐ Understanding Webhooks
๐ก What are Webhooks?
sequenceDiagram
participant E as External Service
participant W as Webhook Endpoint
participant A as Your Application
participant D as Database
Note over E,D: Event-Driven Flow
E->>E: Event occurs
E->>W: HTTP POST with event data
W->>W: Validate signature
W->>A: Process event
A->>D: Update data
W->>E: Return 200 OK
Note over E,D: Real-time notification completed
Webhooks are user-defined HTTP callbacks triggered by specific events in external systems. When an event occurs, the source application makes an HTTP POST request to a configured URL endpoint with event details.
๐๏ธ Webhook Architecture Components
graph TD
A[Event Source<br/>Stripe, GitHub, etc.] --> B[Webhook Payload]
B --> C[HTTP POST Request]
C --> D[Your Webhook Endpoint]
D --> E[Signature Validation]
E --> F[Event Processing]
F --> G[Business Logic]
G --> H[Database Updates]
G --> I[External API Calls]
G --> J[Queue Messages]
K[Error Handling] --> L[Retry Logic]
K --> M[Dead Letter Queue]
K --> N[Monitoring/Alerts]
style A fill:#ff6b6b
style D fill:#4ecdc4
style G fill:#feca57
style K fill:#45b7d1
โ๏ธ Technology Comparison: Webhooks vs Alternatives
๐ Comparison Matrix
Aspect | Webhooks | HTTP Polling | Server-Sent Events | WebSockets |
---|---|---|---|---|
Direction | Push (ServerโClient) | Pull (ClientโServer) | Push (ServerโClient) | Bidirectional |
Real-Time | Immediate | Delayed | Immediate | Immediate |
Resource Usage | Low | High | Medium | Medium |
Reliability | Good with retries | High | Medium | Medium |
Complexity | Medium | Low | Medium | High |
Firewall Issues | Yes | No | No | Yes |
Scalability | Excellent | Poor | Good | Good |
Bandwidth | Efficient | Inefficient | Efficient | Efficient |
Connection State | Stateless | Stateless | Stateful | Stateful |
๐ Communication Pattern Comparison
graph TD
subgraph "HTTP Polling"
P1[Client] -->|1. Request| P2[Server]
P2 -->|2. Response| P1
P1 -->|3. Wait...| P3[Delay]
P3 -->|4. Request Again| P2
end
subgraph "Webhooks"
W1[Server] -->|Event Occurs| W2[HTTP POST]
W2 -->|Immediate| W3[Client Endpoint]
W3 -->|Process| W4[Business Logic]
end
subgraph "WebSockets"
WS1[Client] <-->|Persistent Connection| WS2[Server]
WS2 -->|Real-time Data| WS1
WS1 -->|Commands| WS2
end
style P1 fill:#ff6b6b
style W1 fill:#4ecdc4
style WS1 fill:#feca57
โก Performance Comparison
HTTP Polling Implementation (Inefficient)
1@Service
2@Slf4j
3public class PollingService {
4
5 private final RestTemplate restTemplate;
6 private final ScheduledExecutorService scheduler;
7
8 public PollingService(RestTemplate restTemplate) {
9 this.restTemplate = restTemplate;
10 this.scheduler = Executors.newScheduledThreadPool(5);
11 }
12
13 // Inefficient polling approach
14 @PostConstruct
15 public void startPolling() {
16 scheduler.scheduleAtFixedRate(() -> {
17 try {
18 // Poll every 30 seconds
19 checkForUpdates();
20 } catch (Exception e) {
21 log.error("Polling failed", e);
22 }
23 }, 0, 30, TimeUnit.SECONDS);
24 }
25
26 private void checkForUpdates() {
27 try {
28 ResponseEntity<PaymentUpdate[]> response = restTemplate.getForEntity(
29 "https://api.example.com/payments/updates?since=" + getLastCheckTime(),
30 PaymentUpdate[].class
31 );
32
33 if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
34 Arrays.stream(response.getBody())
35 .forEach(this::processUpdate);
36 }
37
38 } catch (Exception e) {
39 log.error("Failed to poll for updates", e);
40 }
41 }
42
43 private void processUpdate(PaymentUpdate update) {
44 log.info("Processing polled update: {}", update.getId());
45 // Process update...
46 }
47
48 private String getLastCheckTime() {
49 return Instant.now().minus(Duration.ofSeconds(30)).toString();
50 }
51
52 public static class PaymentUpdate {
53 private String id;
54 private String status;
55 private Instant timestamp;
56
57 // Constructors, getters, setters...
58 public PaymentUpdate() {}
59
60 public String getId() { return id; }
61 public void setId(String id) { this.id = id; }
62 public String getStatus() { return status; }
63 public void setStatus(String status) { this.status = status; }
64 public Instant getTimestamp() { return timestamp; }
65 public void setTimestamp(Instant timestamp) { this.timestamp = timestamp; }
66 }
67}
Webhook Implementation (Efficient)
1@RestController
2@RequestMapping("/webhooks")
3@Slf4j
4public class WebhookController {
5
6 private final PaymentService paymentService;
7 private final WebhookValidator webhookValidator;
8
9 public WebhookController(PaymentService paymentService, WebhookValidator webhookValidator) {
10 this.paymentService = paymentService;
11 this.webhookValidator = webhookValidator;
12 }
13
14 // Efficient webhook approach - immediate processing
15 @PostMapping("/payments")
16 public ResponseEntity<Void> handlePaymentWebhook(
17 @RequestBody String payload,
18 @RequestHeader("X-Signature") String signature,
19 @RequestHeader(value = "X-Event-Type", required = false) String eventType) {
20
21 try {
22 // Validate webhook signature
23 if (!webhookValidator.validateSignature(payload, signature)) {
24 log.warn("Invalid webhook signature received");
25 return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
26 }
27
28 // Parse and process immediately
29 PaymentWebhookEvent event = parseWebhookEvent(payload);
30 paymentService.processPaymentUpdate(event);
31
32 log.info("Successfully processed webhook event: {}", event.getId());
33 return ResponseEntity.ok().build();
34
35 } catch (Exception e) {
36 log.error("Failed to process webhook", e);
37 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
38 }
39 }
40
41 private PaymentWebhookEvent parseWebhookEvent(String payload) {
42 try {
43 ObjectMapper mapper = new ObjectMapper();
44 return mapper.readValue(payload, PaymentWebhookEvent.class);
45 } catch (Exception e) {
46 throw new IllegalArgumentException("Invalid webhook payload", e);
47 }
48 }
49}
๐ฏ When to Use Webhooks
โ Ideal Use Cases
Payment Processing
- Real-time payment status updates
- Subscription lifecycle events
- Fraud detection notifications
E-commerce Integration
- Order status changes
- Inventory updates
- Customer lifecycle events
CI/CD Pipelines
- Code repository changes
- Build completion notifications
- Deployment status updates
Communication Systems
- Message delivery status
- User presence updates
- Chat events
IoT and Monitoring
- Device status changes
- Alert notifications
- Metric threshold breaches
โ When NOT to Use Webhooks
- High-Frequency Updates (>1000/second)
- Request-Response Patterns requiring immediate responses
- Client-Initiated Queries for specific data
- Environments with strict firewall restrictions
- When polling frequency is very low (daily/weekly)
๐ ๏ธ Production-Ready Java Implementation
1. Spring Boot Webhook Framework
Maven Dependencies:
1<dependencies>
2 <dependency>
3 <groupId>org.springframework.boot</groupId>
4 <artifactId>spring-boot-starter-web</artifactId>
5 </dependency>
6
7 <dependency>
8 <groupId>org.springframework.boot</groupId>
9 <artifactId>spring-boot-starter-security</artifactId>
10 </dependency>
11
12 <dependency>
13 <groupId>org.springframework.boot</groupId>
14 <artifactId>spring-boot-starter-validation</artifactId>
15 </dependency>
16
17 <dependency>
18 <groupId>org.springframework.boot</groupId>
19 <artifactId>spring-boot-starter-data-jpa</artifactId>
20 </dependency>
21
22 <dependency>
23 <groupId>com.fasterxml.jackson.core</groupId>
24 <artifactId>jackson-databind</artifactId>
25 </dependency>
26
27 <!-- For webhook signature validation -->
28 <dependency>
29 <groupId>commons-codec</groupId>
30 <artifactId>commons-codec</artifactId>
31 </dependency>
32
33 <!-- For retry mechanisms -->
34 <dependency>
35 <groupId>org.springframework.retry</groupId>
36 <artifactId>spring-retry</artifactId>
37 </dependency>
38
39 <!-- For monitoring -->
40 <dependency>
41 <groupId>org.springframework.boot</groupId>
42 <artifactId>spring-boot-starter-actuator</artifactId>
43 </dependency>
44
45 <dependency>
46 <groupId>io.micrometer</groupId>
47 <artifactId>micrometer-registry-prometheus</artifactId>
48 </dependency>
49</dependencies>
2. Webhook Security and Validation
Signature Validation Service:
1@Service
2@Slf4j
3public class WebhookValidator {
4
5 private final Map<String, WebhookConfig> webhookConfigs;
6
7 public WebhookValidator() {
8 this.webhookConfigs = new HashMap<>();
9 initializeWebhookConfigs();
10 }
11
12 private void initializeWebhookConfigs() {
13 // Stripe configuration
14 webhookConfigs.put("stripe", new WebhookConfig(
15 "stripe_webhook_secret",
16 "sha256",
17 "Stripe-Signature",
18 "v1"
19 ));
20
21 // GitHub configuration
22 webhookConfigs.put("github", new WebhookConfig(
23 "github_webhook_secret",
24 "sha1",
25 "X-Hub-Signature",
26 null
27 ));
28
29 // Shopify configuration
30 webhookConfigs.put("shopify", new WebhookConfig(
31 "shopify_webhook_secret",
32 "sha256",
33 "X-Shopify-Hmac-Sha256",
34 null
35 ));
36 }
37
38 public boolean validateStripeSignature(String payload, String signatureHeader) {
39 try {
40 WebhookConfig config = webhookConfigs.get("stripe");
41 String secret = getSecretFromConfig(config.getSecretKey());
42
43 // Stripe signature format: t=timestamp,v1=signature
44 Map<String, String> signatureParts = parseStripeSignature(signatureHeader);
45 String timestamp = signatureParts.get("t");
46 String signature = signatureParts.get("v1");
47
48 if (timestamp == null || signature == null) {
49 log.warn("Invalid Stripe signature format");
50 return false;
51 }
52
53 // Check timestamp (prevent replay attacks)
54 long webhookTime = Long.parseLong(timestamp);
55 long currentTime = System.currentTimeMillis() / 1000;
56 if (Math.abs(currentTime - webhookTime) > 300) { // 5 minute tolerance
57 log.warn("Stripe webhook timestamp too old: {}", webhookTime);
58 return false;
59 }
60
61 // Validate signature
62 String expectedSignature = computeHmacSha256(timestamp + "." + payload, secret);
63 return MessageDigest.isEqual(signature.getBytes(), expectedSignature.getBytes());
64
65 } catch (Exception e) {
66 log.error("Stripe signature validation failed", e);
67 return false;
68 }
69 }
70
71 public boolean validateGitHubSignature(String payload, String signatureHeader) {
72 try {
73 WebhookConfig config = webhookConfigs.get("github");
74 String secret = getSecretFromConfig(config.getSecretKey());
75
76 // GitHub signature format: sha1=<signature>
77 if (!signatureHeader.startsWith("sha1=")) {
78 return false;
79 }
80
81 String signature = signatureHeader.substring(5);
82 String expectedSignature = computeHmacSha1(payload, secret);
83
84 return MessageDigest.isEqual(signature.getBytes(), expectedSignature.getBytes());
85
86 } catch (Exception e) {
87 log.error("GitHub signature validation failed", e);
88 return false;
89 }
90 }
91
92 public boolean validateShopifySignature(String payload, String signatureHeader) {
93 try {
94 WebhookConfig config = webhookConfigs.get("shopify");
95 String secret = getSecretFromConfig(config.getSecretKey());
96
97 String expectedSignature = computeHmacSha256Base64(payload, secret);
98 return MessageDigest.isEqual(signatureHeader.getBytes(), expectedSignature.getBytes());
99
100 } catch (Exception e) {
101 log.error("Shopify signature validation failed", e);
102 return false;
103 }
104 }
105
106 private Map<String, String> parseStripeSignature(String signatureHeader) {
107 Map<String, String> result = new HashMap<>();
108 String[] elements = signatureHeader.split(",");
109
110 for (String element : elements) {
111 String[] keyValue = element.split("=", 2);
112 if (keyValue.length == 2) {
113 result.put(keyValue[0], keyValue[1]);
114 }
115 }
116
117 return result;
118 }
119
120 private String computeHmacSha256(String data, String secret) throws Exception {
121 Mac sha256Hmac = Mac.getInstance("HmacSHA256");
122 SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256");
123 sha256Hmac.init(secretKey);
124 byte[] hash = sha256Hmac.doFinal(data.getBytes("UTF-8"));
125 return Hex.encodeHexString(hash);
126 }
127
128 private String computeHmacSha1(String data, String secret) throws Exception {
129 Mac sha1Hmac = Mac.getInstance("HmacSHA1");
130 SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA1");
131 sha1Hmac.init(secretKey);
132 byte[] hash = sha1Hmac.doFinal(data.getBytes("UTF-8"));
133 return Hex.encodeHexString(hash);
134 }
135
136 private String computeHmacSha256Base64(String data, String secret) throws Exception {
137 Mac sha256Hmac = Mac.getInstance("HmacSHA256");
138 SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256");
139 sha256Hmac.init(secretKey);
140 byte[] hash = sha256Hmac.doFinal(data.getBytes("UTF-8"));
141 return Base64.getEncoder().encodeToString(hash);
142 }
143
144 private String getSecretFromConfig(String configKey) {
145 // In production, load from secure configuration
146 return System.getenv(configKey.toUpperCase().replace("_", "_"));
147 }
148
149 // Configuration class
150 private static class WebhookConfig {
151 private final String secretKey;
152 private final String algorithm;
153 private final String headerName;
154 private final String version;
155
156 public WebhookConfig(String secretKey, String algorithm, String headerName, String version) {
157 this.secretKey = secretKey;
158 this.algorithm = algorithm;
159 this.headerName = headerName;
160 this.version = version;
161 }
162
163 // Getters
164 public String getSecretKey() { return secretKey; }
165 public String getAlgorithm() { return algorithm; }
166 public String getHeaderName() { return headerName; }
167 public String getVersion() { return version; }
168 }
169}
3. Comprehensive Webhook Controller
1@RestController
2@RequestMapping("/api/webhooks")
3@Validated
4@Slf4j
5public class WebhookController {
6
7 private final WebhookValidator webhookValidator;
8 private final WebhookEventProcessor eventProcessor;
9 private final WebhookAuditService auditService;
10 private final MeterRegistry meterRegistry;
11
12 public WebhookController(WebhookValidator webhookValidator,
13 WebhookEventProcessor eventProcessor,
14 WebhookAuditService auditService,
15 MeterRegistry meterRegistry) {
16 this.webhookValidator = webhookValidator;
17 this.eventProcessor = eventProcessor;
18 this.auditService = auditService;
19 this.meterRegistry = meterRegistry;
20 }
21
22 // Stripe Payment Webhooks
23 @PostMapping("/stripe/payments")
24 public ResponseEntity<Void> handleStripeWebhook(
25 @RequestBody String payload,
26 @RequestHeader("Stripe-Signature") String signature,
27 HttpServletRequest request) {
28
29 Timer.Sample sample = Timer.start(meterRegistry);
30 String webhookId = generateWebhookId();
31
32 try {
33 // Log incoming webhook
34 log.info("Received Stripe webhook: {}", webhookId);
35 auditService.logIncomingWebhook("stripe", webhookId, payload, request.getRemoteAddr());
36
37 // Validate signature
38 if (!webhookValidator.validateStripeSignature(payload, signature)) {
39 meterRegistry.counter("webhook.validation.failed", "provider", "stripe").increment();
40 auditService.logValidationFailure(webhookId, "Invalid signature");
41 return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
42 }
43
44 // Parse event
45 StripeWebhookEvent event = parseStripeEvent(payload);
46
47 // Process based on event type
48 switch (event.getType()) {
49 case "payment_intent.succeeded":
50 eventProcessor.processPaymentSuccess(event);
51 break;
52 case "payment_intent.payment_failed":
53 eventProcessor.processPaymentFailure(event);
54 break;
55 case "customer.subscription.created":
56 eventProcessor.processSubscriptionCreated(event);
57 break;
58 case "customer.subscription.updated":
59 eventProcessor.processSubscriptionUpdated(event);
60 break;
61 case "invoice.payment_succeeded":
62 eventProcessor.processInvoicePaymentSuccess(event);
63 break;
64 default:
65 log.info("Unhandled Stripe event type: {}", event.getType());
66 meterRegistry.counter("webhook.events.unhandled", "provider", "stripe", "type", event.getType()).increment();
67 }
68
69 meterRegistry.counter("webhook.events.processed", "provider", "stripe", "type", event.getType()).increment();
70 auditService.logSuccessfulProcessing(webhookId);
71
72 return ResponseEntity.ok().build();
73
74 } catch (Exception e) {
75 log.error("Failed to process Stripe webhook: {}", webhookId, e);
76 meterRegistry.counter("webhook.processing.failed", "provider", "stripe").increment();
77 auditService.logProcessingFailure(webhookId, e.getMessage());
78 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
79
80 } finally {
81 sample.stop(Timer.builder("webhook.processing.duration")
82 .tag("provider", "stripe")
83 .register(meterRegistry));
84 }
85 }
86
87 // GitHub Repository Webhooks
88 @PostMapping("/github/repository")
89 public ResponseEntity<Void> handleGitHubWebhook(
90 @RequestBody String payload,
91 @RequestHeader("X-Hub-Signature") String signature,
92 @RequestHeader("X-GitHub-Event") String eventType,
93 @RequestHeader(value = "X-GitHub-Delivery", required = false) String deliveryId,
94 HttpServletRequest request) {
95
96 Timer.Sample sample = Timer.start(meterRegistry);
97 String webhookId = deliveryId != null ? deliveryId : generateWebhookId();
98
99 try {
100 log.info("Received GitHub webhook: {} ({})", webhookId, eventType);
101 auditService.logIncomingWebhook("github", webhookId, payload, request.getRemoteAddr());
102
103 // Validate signature
104 if (!webhookValidator.validateGitHubSignature(payload, signature)) {
105 meterRegistry.counter("webhook.validation.failed", "provider", "github").increment();
106 return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
107 }
108
109 // Parse event
110 GitHubWebhookEvent event = parseGitHubEvent(payload, eventType);
111
112 // Process based on event type
113 switch (eventType) {
114 case "push":
115 eventProcessor.processGitHubPush(event);
116 break;
117 case "pull_request":
118 eventProcessor.processGitHubPullRequest(event);
119 break;
120 case "issues":
121 eventProcessor.processGitHubIssue(event);
122 break;
123 case "release":
124 eventProcessor.processGitHubRelease(event);
125 break;
126 case "workflow_run":
127 eventProcessor.processGitHubWorkflowRun(event);
128 break;
129 default:
130 log.info("Unhandled GitHub event type: {}", eventType);
131 meterRegistry.counter("webhook.events.unhandled", "provider", "github", "type", eventType).increment();
132 }
133
134 meterRegistry.counter("webhook.events.processed", "provider", "github", "type", eventType).increment();
135 return ResponseEntity.ok().build();
136
137 } catch (Exception e) {
138 log.error("Failed to process GitHub webhook: {}", webhookId, e);
139 meterRegistry.counter("webhook.processing.failed", "provider", "github").increment();
140 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
141
142 } finally {
143 sample.stop(Timer.builder("webhook.processing.duration")
144 .tag("provider", "github")
145 .register(meterRegistry));
146 }
147 }
148
149 // Shopify E-commerce Webhooks
150 @PostMapping("/shopify/orders")
151 public ResponseEntity<Void> handleShopifyWebhook(
152 @RequestBody String payload,
153 @RequestHeader("X-Shopify-Hmac-Sha256") String signature,
154 @RequestHeader("X-Shopify-Topic") String topic,
155 @RequestHeader("X-Shopify-Shop-Domain") String shopDomain,
156 HttpServletRequest request) {
157
158 Timer.Sample sample = Timer.start(meterRegistry);
159 String webhookId = generateWebhookId();
160
161 try {
162 log.info("Received Shopify webhook: {} ({}) from {}", webhookId, topic, shopDomain);
163 auditService.logIncomingWebhook("shopify", webhookId, payload, request.getRemoteAddr());
164
165 // Validate signature
166 if (!webhookValidator.validateShopifySignature(payload, signature)) {
167 meterRegistry.counter("webhook.validation.failed", "provider", "shopify").increment();
168 return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
169 }
170
171 // Parse event
172 ShopifyWebhookEvent event = parseShopifyEvent(payload, topic, shopDomain);
173
174 // Process based on topic
175 switch (topic) {
176 case "orders/create":
177 eventProcessor.processShopifyOrderCreate(event);
178 break;
179 case "orders/updated":
180 eventProcessor.processShopifyOrderUpdate(event);
181 break;
182 case "orders/paid":
183 eventProcessor.processShopifyOrderPaid(event);
184 break;
185 case "orders/cancelled":
186 eventProcessor.processShopifyOrderCancelled(event);
187 break;
188 case "customers/create":
189 eventProcessor.processShopifyCustomerCreate(event);
190 break;
191 case "products/create":
192 eventProcessor.processShopifyProductCreate(event);
193 break;
194 case "products/update":
195 eventProcessor.processShopifyProductUpdate(event);
196 break;
197 default:
198 log.info("Unhandled Shopify event topic: {}", topic);
199 meterRegistry.counter("webhook.events.unhandled", "provider", "shopify", "topic", topic).increment();
200 }
201
202 meterRegistry.counter("webhook.events.processed", "provider", "shopify", "topic", topic).increment();
203 return ResponseEntity.ok().build();
204
205 } catch (Exception e) {
206 log.error("Failed to process Shopify webhook: {}", webhookId, e);
207 meterRegistry.counter("webhook.processing.failed", "provider", "shopify").increment();
208 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
209
210 } finally {
211 sample.stop(Timer.builder("webhook.processing.duration")
212 .tag("provider", "shopify")
213 .register(meterRegistry));
214 }
215 }
216
217 // Generic webhook endpoint for testing
218 @PostMapping("/generic")
219 public ResponseEntity<Map<String, Object>> handleGenericWebhook(
220 @RequestBody(required = false) String payload,
221 HttpServletRequest request,
222 HttpServletResponse response) {
223
224 String webhookId = generateWebhookId();
225 Map<String, Object> responseData = new HashMap<>();
226
227 try {
228 log.info("Received generic webhook: {}", webhookId);
229
230 // Collect headers
231 Map<String, String> headers = new HashMap<>();
232 Enumeration<String> headerNames = request.getHeaderNames();
233 while (headerNames.hasMoreElements()) {
234 String headerName = headerNames.nextElement();
235 headers.put(headerName, request.getHeader(headerName));
236 }
237
238 // Collect request info
239 Map<String, Object> requestInfo = new HashMap<>();
240 requestInfo.put("method", request.getMethod());
241 requestInfo.put("contentType", request.getContentType());
242 requestInfo.put("contentLength", request.getContentLength());
243 requestInfo.put("remoteAddr", request.getRemoteAddr());
244 requestInfo.put("userAgent", request.getHeader("User-Agent"));
245
246 responseData.put("webhookId", webhookId);
247 responseData.put("timestamp", Instant.now().toString());
248 responseData.put("headers", headers);
249 responseData.put("request", requestInfo);
250 responseData.put("payloadLength", payload != null ? payload.length() : 0);
251
252 if (payload != null && !payload.isEmpty()) {
253 try {
254 // Try to parse as JSON
255 ObjectMapper mapper = new ObjectMapper();
256 Object parsedPayload = mapper.readValue(payload, Object.class);
257 responseData.put("payload", parsedPayload);
258 } catch (Exception e) {
259 responseData.put("payload", payload);
260 }
261 }
262
263 auditService.logIncomingWebhook("generic", webhookId, payload, request.getRemoteAddr());
264 meterRegistry.counter("webhook.events.processed", "provider", "generic").increment();
265
266 return ResponseEntity.ok(responseData);
267
268 } catch (Exception e) {
269 log.error("Failed to process generic webhook: {}", webhookId, e);
270 responseData.put("error", e.getMessage());
271 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(responseData);
272 }
273 }
274
275 // Utility methods
276 private String generateWebhookId() {
277 return "wh_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8);
278 }
279
280 private StripeWebhookEvent parseStripeEvent(String payload) throws Exception {
281 ObjectMapper mapper = new ObjectMapper();
282 return mapper.readValue(payload, StripeWebhookEvent.class);
283 }
284
285 private GitHubWebhookEvent parseGitHubEvent(String payload, String eventType) throws Exception {
286 ObjectMapper mapper = new ObjectMapper();
287 GitHubWebhookEvent event = mapper.readValue(payload, GitHubWebhookEvent.class);
288 event.setEventType(eventType);
289 return event;
290 }
291
292 private ShopifyWebhookEvent parseShopifyEvent(String payload, String topic, String shopDomain) throws Exception {
293 ObjectMapper mapper = new ObjectMapper();
294 ShopifyWebhookEvent event = mapper.readValue(payload, ShopifyWebhookEvent.class);
295 event.setTopic(topic);
296 event.setShopDomain(shopDomain);
297 return event;
298 }
299}
4. Event Processing Service
1@Service
2@Slf4j
3public class WebhookEventProcessor {
4
5 private final PaymentService paymentService;
6 private final OrderService orderService;
7 private final CustomerService customerService;
8 private final ProductService productService;
9 private final NotificationService notificationService;
10 private final WebhookRetryService retryService;
11
12 public WebhookEventProcessor(PaymentService paymentService,
13 OrderService orderService,
14 CustomerService customerService,
15 ProductService productService,
16 NotificationService notificationService,
17 WebhookRetryService retryService) {
18 this.paymentService = paymentService;
19 this.orderService = orderService;
20 this.customerService = customerService;
21 this.productService = productService;
22 this.notificationService = notificationService;
23 this.retryService = retryService;
24 }
25
26 // Stripe Event Processors
27 @Async
28 @Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
29 public void processPaymentSuccess(StripeWebhookEvent event) {
30 try {
31 log.info("Processing payment success: {}", event.getId());
32
33 PaymentIntent paymentIntent = extractPaymentIntent(event);
34
35 // Update payment status in database
36 paymentService.markPaymentAsSucceeded(paymentIntent.getId(), paymentIntent.getAmount());
37
38 // Send confirmation email
39 String customerEmail = paymentIntent.getCustomerEmail();
40 if (customerEmail != null) {
41 notificationService.sendPaymentSuccessEmail(customerEmail, paymentIntent);
42 }
43
44 // Update order status if applicable
45 String orderId = paymentIntent.getMetadata().get("order_id");
46 if (orderId != null) {
47 orderService.markOrderAsPaid(orderId);
48 }
49
50 log.info("Successfully processed payment success: {}", event.getId());
51
52 } catch (Exception e) {
53 log.error("Failed to process payment success event: {}", event.getId(), e);
54 retryService.scheduleRetry("payment_success", event.getId(), event);
55 throw e;
56 }
57 }
58
59 @Async
60 @Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
61 public void processPaymentFailure(StripeWebhookEvent event) {
62 try {
63 log.info("Processing payment failure: {}", event.getId());
64
65 PaymentIntent paymentIntent = extractPaymentIntent(event);
66
67 // Update payment status
68 paymentService.markPaymentAsFailed(paymentIntent.getId(), paymentIntent.getLastPaymentError());
69
70 // Send failure notification
71 String customerEmail = paymentIntent.getCustomerEmail();
72 if (customerEmail != null) {
73 notificationService.sendPaymentFailureEmail(customerEmail, paymentIntent);
74 }
75
76 // Handle order implications
77 String orderId = paymentIntent.getMetadata().get("order_id");
78 if (orderId != null) {
79 orderService.handlePaymentFailure(orderId, paymentIntent.getLastPaymentError());
80 }
81
82 log.info("Successfully processed payment failure: {}", event.getId());
83
84 } catch (Exception e) {
85 log.error("Failed to process payment failure event: {}", event.getId(), e);
86 throw e;
87 }
88 }
89
90 @Async
91 public void processSubscriptionCreated(StripeWebhookEvent event) {
92 try {
93 log.info("Processing subscription created: {}", event.getId());
94
95 Subscription subscription = extractSubscription(event);
96
97 // Create subscription record
98 customerService.createSubscription(
99 subscription.getCustomer(),
100 subscription.getId(),
101 subscription.getPlan(),
102 subscription.getStatus()
103 );
104
105 // Send welcome email
106 String customerEmail = customerService.getCustomerEmail(subscription.getCustomer());
107 if (customerEmail != null) {
108 notificationService.sendSubscriptionWelcomeEmail(customerEmail, subscription);
109 }
110
111 log.info("Successfully processed subscription created: {}", event.getId());
112
113 } catch (Exception e) {
114 log.error("Failed to process subscription created event: {}", event.getId(), e);
115 throw e;
116 }
117 }
118
119 // GitHub Event Processors
120 @Async
121 public void processGitHubPush(GitHubWebhookEvent event) {
122 try {
123 log.info("Processing GitHub push: {}", event.getRepository().getFullName());
124
125 String repository = event.getRepository().getFullName();
126 String branch = event.getRef().replace("refs/heads/", "");
127 List<Commit> commits = event.getCommits();
128
129 // Trigger CI/CD pipeline if push to main branch
130 if ("main".equals(branch) || "master".equals(branch)) {
131 triggerCICD(repository, branch, commits);
132 }
133
134 // Update deployment status
135 updateDeploymentTracking(repository, branch, commits);
136
137 // Send notifications to development team
138 notificationService.sendPushNotification(repository, branch, commits);
139
140 log.info("Successfully processed GitHub push for {}", repository);
141
142 } catch (Exception e) {
143 log.error("Failed to process GitHub push event", e);
144 throw e;
145 }
146 }
147
148 @Async
149 public void processGitHubPullRequest(GitHubWebhookEvent event) {
150 try {
151 log.info("Processing GitHub pull request: {}", event.getPullRequest().getNumber());
152
153 PullRequest pr = event.getPullRequest();
154 String action = event.getAction();
155
156 switch (action) {
157 case "opened":
158 // Trigger automated testing
159 triggerPRTests(pr);
160
161 // Assign reviewers
162 assignReviewers(pr);
163
164 // Send notifications
165 notificationService.sendPROpenedNotification(pr);
166 break;
167
168 case "closed":
169 if (pr.isMerged()) {
170 // Handle merge
171 handlePRMerge(pr);
172 }
173 break;
174
175 case "review_requested":
176 notificationService.sendReviewRequestNotification(pr);
177 break;
178 }
179
180 log.info("Successfully processed GitHub PR: {}", pr.getNumber());
181
182 } catch (Exception e) {
183 log.error("Failed to process GitHub pull request event", e);
184 throw e;
185 }
186 }
187
188 // Shopify Event Processors
189 @Async
190 public void processShopifyOrderCreate(ShopifyWebhookEvent event) {
191 try {
192 log.info("Processing Shopify order create: {}", event.getId());
193
194 ShopifyOrder order = extractShopifyOrder(event);
195
196 // Create order record
197 orderService.createShopifyOrder(order);
198
199 // Update inventory
200 updateInventoryForOrder(order);
201
202 // Send order confirmation
203 notificationService.sendOrderConfirmation(order.getCustomer().getEmail(), order);
204
205 // Trigger fulfillment process
206 triggerFulfillment(order);
207
208 log.info("Successfully processed Shopify order create: {}", order.getId());
209
210 } catch (Exception e) {
211 log.error("Failed to process Shopify order create event", e);
212 throw e;
213 }
214 }
215
216 @Async
217 public void processShopifyOrderPaid(ShopifyWebhookEvent event) {
218 try {
219 log.info("Processing Shopify order paid: {}", event.getId());
220
221 ShopifyOrder order = extractShopifyOrder(event);
222
223 // Update order payment status
224 orderService.markShopifyOrderAsPaid(order.getId());
225
226 // Send payment confirmation
227 notificationService.sendPaymentConfirmation(order.getCustomer().getEmail(), order);
228
229 // Trigger shipping process
230 triggerShipping(order);
231
232 log.info("Successfully processed Shopify order paid: {}", order.getId());
233
234 } catch (Exception e) {
235 log.error("Failed to process Shopify order paid event", e);
236 throw e;
237 }
238 }
239
240 // Utility methods
241 private PaymentIntent extractPaymentIntent(StripeWebhookEvent event) {
242 return (PaymentIntent) event.getData().getObject();
243 }
244
245 private Subscription extractSubscription(StripeWebhookEvent event) {
246 return (Subscription) event.getData().getObject();
247 }
248
249 private ShopifyOrder extractShopifyOrder(ShopifyWebhookEvent event) {
250 // Parse Shopify order from event data
251 return event.getOrder();
252 }
253
254 private void triggerCICD(String repository, String branch, List<Commit> commits) {
255 // Implementation would trigger CI/CD pipeline
256 log.info("Triggering CI/CD for {}/{} with {} commits", repository, branch, commits.size());
257 }
258
259 private void updateDeploymentTracking(String repository, String branch, List<Commit> commits) {
260 // Implementation would update deployment tracking
261 log.info("Updating deployment tracking for {}/{}", repository, branch);
262 }
263
264 private void triggerPRTests(PullRequest pr) {
265 // Implementation would trigger automated tests
266 log.info("Triggering tests for PR #{}", pr.getNumber());
267 }
268
269 private void assignReviewers(PullRequest pr) {
270 // Implementation would assign reviewers based on code changes
271 log.info("Assigning reviewers for PR #{}", pr.getNumber());
272 }
273
274 private void handlePRMerge(PullRequest pr) {
275 // Implementation would handle post-merge actions
276 log.info("Handling merge for PR #{}", pr.getNumber());
277 }
278
279 private void updateInventoryForOrder(ShopifyOrder order) {
280 // Implementation would update inventory levels
281 log.info("Updating inventory for order {}", order.getId());
282 }
283
284 private void triggerFulfillment(ShopifyOrder order) {
285 // Implementation would trigger fulfillment process
286 log.info("Triggering fulfillment for order {}", order.getId());
287 }
288
289 private void triggerShipping(ShopifyOrder order) {
290 // Implementation would trigger shipping process
291 log.info("Triggering shipping for order {}", order.getId());
292 }
293}
5. Event Models and DTOs
1// Stripe Event Models
2public class StripeWebhookEvent {
3 private String id;
4 private String object;
5 private String type;
6 private boolean livemode;
7 private long created;
8 private EventData data;
9 private String pendingWebhooks;
10 private EventRequest request;
11
12 // Constructors, getters, setters...
13 public StripeWebhookEvent() {}
14
15 public static class EventData {
16 private Object object;
17 private Object previousAttributes;
18
19 // Constructors, getters, setters...
20 public EventData() {}
21 public Object getObject() { return object; }
22 public void setObject(Object object) { this.object = object; }
23 public Object getPreviousAttributes() { return previousAttributes; }
24 public void setPreviousAttributes(Object previousAttributes) { this.previousAttributes = previousAttributes; }
25 }
26
27 public static class EventRequest {
28 private String id;
29 private String idempotencyKey;
30
31 // Constructors, getters, setters...
32 public EventRequest() {}
33 public String getId() { return id; }
34 public void setId(String id) { this.id = id; }
35 public String getIdempotencyKey() { return idempotencyKey; }
36 public void setIdempotencyKey(String idempotencyKey) { this.idempotencyKey = idempotencyKey; }
37 }
38
39 // Getters and setters
40 public String getId() { return id; }
41 public void setId(String id) { this.id = id; }
42 public String getObject() { return object; }
43 public void setObject(String object) { this.object = object; }
44 public String getType() { return type; }
45 public void setType(String type) { this.type = type; }
46 public boolean isLivemode() { return livemode; }
47 public void setLivemode(boolean livemode) { this.livemode = livemode; }
48 public long getCreated() { return created; }
49 public void setCreated(long created) { this.created = created; }
50 public EventData getData() { return data; }
51 public void setData(EventData data) { this.data = data; }
52 public String getPendingWebhooks() { return pendingWebhooks; }
53 public void setPendingWebhooks(String pendingWebhooks) { this.pendingWebhooks = pendingWebhooks; }
54 public EventRequest getRequest() { return request; }
55 public void setRequest(EventRequest request) { this.request = request; }
56}
57
58public class PaymentIntent {
59 private String id;
60 private String object;
61 private long amount;
62 private String currency;
63 private String status;
64 private String customerEmail;
65 private Map<String, String> metadata;
66 private PaymentError lastPaymentError;
67
68 // Constructors, getters, setters...
69 public PaymentIntent() {
70 this.metadata = new HashMap<>();
71 }
72
73 public static class PaymentError {
74 private String type;
75 private String code;
76 private String message;
77
78 // Constructors, getters, setters...
79 public PaymentError() {}
80 public String getType() { return type; }
81 public void setType(String type) { this.type = type; }
82 public String getCode() { return code; }
83 public void setCode(String code) { this.code = code; }
84 public String getMessage() { return message; }
85 public void setMessage(String message) { this.message = message; }
86 }
87
88 // Getters and setters
89 public String getId() { return id; }
90 public void setId(String id) { this.id = id; }
91 public String getObject() { return object; }
92 public void setObject(String object) { this.object = object; }
93 public long getAmount() { return amount; }
94 public void setAmount(long amount) { this.amount = amount; }
95 public String getCurrency() { return currency; }
96 public void setCurrency(String currency) { this.currency = currency; }
97 public String getStatus() { return status; }
98 public void setStatus(String status) { this.status = status; }
99 public String getCustomerEmail() { return customerEmail; }
100 public void setCustomerEmail(String customerEmail) { this.customerEmail = customerEmail; }
101 public Map<String, String> getMetadata() { return metadata; }
102 public void setMetadata(Map<String, String> metadata) { this.metadata = metadata; }
103 public PaymentError getLastPaymentError() { return lastPaymentError; }
104 public void setLastPaymentError(PaymentError lastPaymentError) { this.lastPaymentError = lastPaymentError; }
105}
106
107// GitHub Event Models
108public class GitHubWebhookEvent {
109 private String eventType;
110 private String action;
111 private Repository repository;
112 private String ref;
113 private List<Commit> commits;
114 private PullRequest pullRequest;
115
116 // Constructors, getters, setters...
117 public GitHubWebhookEvent() {
118 this.commits = new ArrayList<>();
119 }
120
121 public static class Repository {
122 private long id;
123 private String name;
124 private String fullName;
125 private String url;
126 private String defaultBranch;
127
128 // Constructors, getters, setters...
129 public Repository() {}
130 public long getId() { return id; }
131 public void setId(long id) { this.id = id; }
132 public String getName() { return name; }
133 public void setName(String name) { this.name = name; }
134 public String getFullName() { return fullName; }
135 public void setFullName(String fullName) { this.fullName = fullName; }
136 public String getUrl() { return url; }
137 public void setUrl(String url) { this.url = url; }
138 public String getDefaultBranch() { return defaultBranch; }
139 public void setDefaultBranch(String defaultBranch) { this.defaultBranch = defaultBranch; }
140 }
141
142 public static class Commit {
143 private String id;
144 private String message;
145 private String url;
146 private Author author;
147 private List<String> added;
148 private List<String> removed;
149 private List<String> modified;
150
151 // Constructors, getters, setters...
152 public Commit() {
153 this.added = new ArrayList<>();
154 this.removed = new ArrayList<>();
155 this.modified = new ArrayList<>();
156 }
157
158 public static class Author {
159 private String name;
160 private String email;
161 private String username;
162
163 // Constructors, getters, setters...
164 public Author() {}
165 public String getName() { return name; }
166 public void setName(String name) { this.name = name; }
167 public String getEmail() { return email; }
168 public void setEmail(String email) { this.email = email; }
169 public String getUsername() { return username; }
170 public void setUsername(String username) { this.username = username; }
171 }
172
173 // Getters and setters
174 public String getId() { return id; }
175 public void setId(String id) { this.id = id; }
176 public String getMessage() { return message; }
177 public void setMessage(String message) { this.message = message; }
178 public String getUrl() { return url; }
179 public void setUrl(String url) { this.url = url; }
180 public Author getAuthor() { return author; }
181 public void setAuthor(Author author) { this.author = author; }
182 public List<String> getAdded() { return added; }
183 public void setAdded(List<String> added) { this.added = added; }
184 public List<String> getRemoved() { return removed; }
185 public void setRemoved(List<String> removed) { this.removed = removed; }
186 public List<String> getModified() { return modified; }
187 public void setModified(List<String> modified) { this.modified = modified; }
188 }
189
190 public static class PullRequest {
191 private long number;
192 private String title;
193 private String body;
194 private String state;
195 private boolean merged;
196 private String url;
197 private User user;
198 private String head;
199 private String base;
200
201 public static class User {
202 private String login;
203 private String email;
204
205 // Constructors, getters, setters...
206 public User() {}
207 public String getLogin() { return login; }
208 public void setLogin(String login) { this.login = login; }
209 public String getEmail() { return email; }
210 public void setEmail(String email) { this.email = email; }
211 }
212
213 // Constructors, getters, setters...
214 public PullRequest() {}
215 public long getNumber() { return number; }
216 public void setNumber(long number) { this.number = number; }
217 public String getTitle() { return title; }
218 public void setTitle(String title) { this.title = title; }
219 public String getBody() { return body; }
220 public void setBody(String body) { this.body = body; }
221 public String getState() { return state; }
222 public void setState(String state) { this.state = state; }
223 public boolean isMerged() { return merged; }
224 public void setMerged(boolean merged) { this.merged = merged; }
225 public String getUrl() { return url; }
226 public void setUrl(String url) { this.url = url; }
227 public User getUser() { return user; }
228 public void setUser(User user) { this.user = user; }
229 public String getHead() { return head; }
230 public void setHead(String head) { this.head = head; }
231 public String getBase() { return base; }
232 public void setBase(String base) { this.base = base; }
233 }
234
235 // Getters and setters
236 public String getEventType() { return eventType; }
237 public void setEventType(String eventType) { this.eventType = eventType; }
238 public String getAction() { return action; }
239 public void setAction(String action) { this.action = action; }
240 public Repository getRepository() { return repository; }
241 public void setRepository(Repository repository) { this.repository = repository; }
242 public String getRef() { return ref; }
243 public void setRef(String ref) { this.ref = ref; }
244 public List<Commit> getCommits() { return commits; }
245 public void setCommits(List<Commit> commits) { this.commits = commits; }
246 public PullRequest getPullRequest() { return pullRequest; }
247 public void setPullRequest(PullRequest pullRequest) { this.pullRequest = pullRequest; }
248}
249
250// Shopify Event Models
251public class ShopifyWebhookEvent {
252 private String topic;
253 private String shopDomain;
254 private ShopifyOrder order;
255 private ShopifyCustomer customer;
256 private ShopifyProduct product;
257
258 // Constructors, getters, setters...
259 public ShopifyWebhookEvent() {}
260
261 public static class ShopifyOrder {
262 private long id;
263 private String name;
264 private String email;
265 private String financialStatus;
266 private String fulfillmentStatus;
267 private String totalPrice;
268 private String currency;
269 private ShopifyCustomer customer;
270 private List<LineItem> lineItems;
271
272 public static class LineItem {
273 private long id;
274 private long productId;
275 private String title;
276 private int quantity;
277 private String price;
278
279 // Constructors, getters, setters...
280 public LineItem() {}
281 public long getId() { return id; }
282 public void setId(long id) { this.id = id; }
283 public long getProductId() { return productId; }
284 public void setProductId(long productId) { this.productId = productId; }
285 public String getTitle() { return title; }
286 public void setTitle(String title) { this.title = title; }
287 public int getQuantity() { return quantity; }
288 public void setQuantity(int quantity) { this.quantity = quantity; }
289 public String getPrice() { return price; }
290 public void setPrice(String price) { this.price = price; }
291 }
292
293 // Constructors, getters, setters...
294 public ShopifyOrder() {
295 this.lineItems = new ArrayList<>();
296 }
297 public long getId() { return id; }
298 public void setId(long id) { this.id = id; }
299 public String getName() { return name; }
300 public void setName(String name) { this.name = name; }
301 public String getEmail() { return email; }
302 public void setEmail(String email) { this.email = email; }
303 public String getFinancialStatus() { return financialStatus; }
304 public void setFinancialStatus(String financialStatus) { this.financialStatus = financialStatus; }
305 public String getFulfillmentStatus() { return fulfillmentStatus; }
306 public void setFulfillmentStatus(String fulfillmentStatus) { this.fulfillmentStatus = fulfillmentStatus; }
307 public String getTotalPrice() { return totalPrice; }
308 public void setTotalPrice(String totalPrice) { this.totalPrice = totalPrice; }
309 public String getCurrency() { return currency; }
310 public void setCurrency(String currency) { this.currency = currency; }
311 public ShopifyCustomer getCustomer() { return customer; }
312 public void setCustomer(ShopifyCustomer customer) { this.customer = customer; }
313 public List<LineItem> getLineItems() { return lineItems; }
314 public void setLineItems(List<LineItem> lineItems) { this.lineItems = lineItems; }
315 }
316
317 public static class ShopifyCustomer {
318 private long id;
319 private String email;
320 private String firstName;
321 private String lastName;
322 private String phone;
323
324 // Constructors, getters, setters...
325 public ShopifyCustomer() {}
326 public long getId() { return id; }
327 public void setId(long id) { this.id = id; }
328 public String getEmail() { return email; }
329 public void setEmail(String email) { this.email = email; }
330 public String getFirstName() { return firstName; }
331 public void setFirstName(String firstName) { this.firstName = firstName; }
332 public String getLastName() { return lastName; }
333 public void setLastName(String lastName) { this.lastName = lastName; }
334 public String getPhone() { return phone; }
335 public void setPhone(String phone) { this.phone = phone; }
336 }
337
338 public static class ShopifyProduct {
339 private long id;
340 private String title;
341 private String handle;
342 private String productType;
343 private String vendor;
344
345 // Constructors, getters, setters...
346 public ShopifyProduct() {}
347 public long getId() { return id; }
348 public void setId(long id) { this.id = id; }
349 public String getTitle() { return title; }
350 public void setTitle(String title) { this.title = title; }
351 public String getHandle() { return handle; }
352 public void setHandle(String handle) { this.handle = handle; }
353 public String getProductType() { return productType; }
354 public void setProductType(String productType) { this.productType = productType; }
355 public String getVendor() { return vendor; }
356 public void setVendor(String vendor) { this.vendor = vendor; }
357 }
358
359 // Getters and setters
360 public String getTopic() { return topic; }
361 public void setTopic(String topic) { this.topic = topic; }
362 public String getShopDomain() { return shopDomain; }
363 public void setShopDomain(String shopDomain) { this.shopDomain = shopDomain; }
364 public ShopifyOrder getOrder() { return order; }
365 public void setOrder(ShopifyOrder order) { this.order = order; }
366 public ShopifyCustomer getCustomer() { return customer; }
367 public void setCustomer(ShopifyCustomer customer) { this.customer = customer; }
368 public ShopifyProduct getProduct() { return product; }
369 public void setProduct(ShopifyProduct product) { this.product = product; }
370 public String getId() { return order != null ? String.valueOf(order.getId()) : null; }
371}
๐ Monitoring and Observability
1. Webhook Audit Service
1@Service
2@Slf4j
3public class WebhookAuditService {
4
5 private final WebhookAuditRepository auditRepository;
6 private final MeterRegistry meterRegistry;
7
8 public WebhookAuditService(WebhookAuditRepository auditRepository, MeterRegistry meterRegistry) {
9 this.auditRepository = auditRepository;
10 this.meterRegistry = meterRegistry;
11 }
12
13 @Async
14 public void logIncomingWebhook(String provider, String webhookId, String payload, String remoteAddr) {
15 try {
16 WebhookAuditLog auditLog = new WebhookAuditLog();
17 auditLog.setWebhookId(webhookId);
18 auditLog.setProvider(provider);
19 auditLog.setPayload(truncatePayload(payload));
20 auditLog.setRemoteAddr(remoteAddr);
21 auditLog.setStatus(WebhookStatus.RECEIVED);
22 auditLog.setCreatedAt(Instant.now());
23
24 auditRepository.save(auditLog);
25
26 meterRegistry.counter("webhook.audit.logged", "provider", provider, "status", "received").increment();
27
28 } catch (Exception e) {
29 log.error("Failed to log incoming webhook: {}", webhookId, e);
30 }
31 }
32
33 @Async
34 public void logValidationFailure(String webhookId, String reason) {
35 try {
36 updateWebhookStatus(webhookId, WebhookStatus.VALIDATION_FAILED, reason);
37 meterRegistry.counter("webhook.audit.validation_failed").increment();
38 } catch (Exception e) {
39 log.error("Failed to log validation failure: {}", webhookId, e);
40 }
41 }
42
43 @Async
44 public void logSuccessfulProcessing(String webhookId) {
45 try {
46 updateWebhookStatus(webhookId, WebhookStatus.PROCESSED, null);
47 meterRegistry.counter("webhook.audit.processed").increment();
48 } catch (Exception e) {
49 log.error("Failed to log successful processing: {}", webhookId, e);
50 }
51 }
52
53 @Async
54 public void logProcessingFailure(String webhookId, String errorMessage) {
55 try {
56 updateWebhookStatus(webhookId, WebhookStatus.PROCESSING_FAILED, errorMessage);
57 meterRegistry.counter("webhook.audit.processing_failed").increment();
58 } catch (Exception e) {
59 log.error("Failed to log processing failure: {}", webhookId, e);
60 }
61 }
62
63 private void updateWebhookStatus(String webhookId, WebhookStatus status, String errorMessage) {
64 WebhookAuditLog auditLog = auditRepository.findByWebhookId(webhookId);
65 if (auditLog != null) {
66 auditLog.setStatus(status);
67 auditLog.setErrorMessage(errorMessage);
68 auditLog.setUpdatedAt(Instant.now());
69 auditRepository.save(auditLog);
70 }
71 }
72
73 private String truncatePayload(String payload) {
74 if (payload != null && payload.length() > 10000) {
75 return payload.substring(0, 10000) + "... [TRUNCATED]";
76 }
77 return payload;
78 }
79
80 // Supporting classes
81 @Entity
82 @Table(name = "webhook_audit_logs")
83 public static class WebhookAuditLog {
84 @Id
85 @GeneratedValue(strategy = GenerationType.IDENTITY)
86 private Long id;
87
88 @Column(name = "webhook_id", nullable = false)
89 private String webhookId;
90
91 @Column(name = "provider", nullable = false)
92 private String provider;
93
94 @Column(name = "payload", columnDefinition = "TEXT")
95 private String payload;
96
97 @Column(name = "remote_addr")
98 private String remoteAddr;
99
100 @Enumerated(EnumType.STRING)
101 @Column(name = "status", nullable = false)
102 private WebhookStatus status;
103
104 @Column(name = "error_message")
105 private String errorMessage;
106
107 @Column(name = "created_at", nullable = false)
108 private Instant createdAt;
109
110 @Column(name = "updated_at")
111 private Instant updatedAt;
112
113 // Constructors, getters, setters...
114 public WebhookAuditLog() {}
115
116 // Getters and setters
117 public Long getId() { return id; }
118 public void setId(Long id) { this.id = id; }
119 public String getWebhookId() { return webhookId; }
120 public void setWebhookId(String webhookId) { this.webhookId = webhookId; }
121 public String getProvider() { return provider; }
122 public void setProvider(String provider) { this.provider = provider; }
123 public String getPayload() { return payload; }
124 public void setPayload(String payload) { this.payload = payload; }
125 public String getRemoteAddr() { return remoteAddr; }
126 public void setRemoteAddr(String remoteAddr) { this.remoteAddr = remoteAddr; }
127 public WebhookStatus getStatus() { return status; }
128 public void setStatus(WebhookStatus status) { this.status = status; }
129 public String getErrorMessage() { return errorMessage; }
130 public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
131 public Instant getCreatedAt() { return createdAt; }
132 public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
133 public Instant getUpdatedAt() { return updatedAt; }
134 public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
135 }
136
137 public enum WebhookStatus {
138 RECEIVED,
139 VALIDATION_FAILED,
140 PROCESSED,
141 PROCESSING_FAILED,
142 RETRYING
143 }
144
145 @Repository
146 public interface WebhookAuditRepository extends JpaRepository<WebhookAuditLog, Long> {
147 WebhookAuditLog findByWebhookId(String webhookId);
148
149 List<WebhookAuditLog> findByProviderAndCreatedAtBetween(
150 String provider, Instant startTime, Instant endTime);
151
152 List<WebhookAuditLog> findByStatusAndCreatedAtBefore(
153 WebhookStatus status, Instant before);
154 }
155}
๐ง Testing Webhooks
1. Integration Tests
1@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
2@TestPropertySource(properties = {
3 "webhook.stripe.secret=test_secret",
4 "webhook.github.secret=test_secret",
5 "webhook.shopify.secret=test_secret"
6})
7class WebhookControllerIntegrationTest {
8
9 @Autowired
10 private TestRestTemplate restTemplate;
11
12 @MockBean
13 private WebhookEventProcessor eventProcessor;
14
15 @Test
16 void testStripePaymentSuccessWebhook() throws Exception {
17 // Prepare test data
18 String payload = createStripePaymentSuccessPayload();
19 String signature = createStripeSignature(payload, "test_secret");
20
21 HttpHeaders headers = new HttpHeaders();
22 headers.set("Stripe-Signature", signature);
23 headers.setContentType(MediaType.APPLICATION_JSON);
24
25 HttpEntity<String> request = new HttpEntity<>(payload, headers);
26
27 // Send webhook
28 ResponseEntity<Void> response = restTemplate.postForEntity(
29 "/api/webhooks/stripe/payments", request, Void.class);
30
31 // Verify response
32 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
33
34 // Verify event processor was called
35 verify(eventProcessor, times(1)).processPaymentSuccess(any(StripeWebhookEvent.class));
36 }
37
38 @Test
39 void testInvalidSignatureReturnsUnauthorized() {
40 String payload = createStripePaymentSuccessPayload();
41 String invalidSignature = "t=1234567890,v1=invalid_signature";
42
43 HttpHeaders headers = new HttpHeaders();
44 headers.set("Stripe-Signature", invalidSignature);
45 headers.setContentType(MediaType.APPLICATION_JSON);
46
47 HttpEntity<String> request = new HttpEntity<>(payload, headers);
48
49 ResponseEntity<Void> response = restTemplate.postForEntity(
50 "/api/webhooks/stripe/payments", request, Void.class);
51
52 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
53 verifyNoInteractions(eventProcessor);
54 }
55
56 @Test
57 void testGitHubPushWebhook() throws Exception {
58 String payload = createGitHubPushPayload();
59 String signature = createGitHubSignature(payload, "test_secret");
60
61 HttpHeaders headers = new HttpHeaders();
62 headers.set("X-Hub-Signature", signature);
63 headers.set("X-GitHub-Event", "push");
64 headers.set("X-GitHub-Delivery", "test-delivery-id");
65 headers.setContentType(MediaType.APPLICATION_JSON);
66
67 HttpEntity<String> request = new HttpEntity<>(payload, headers);
68
69 ResponseEntity<Void> response = restTemplate.postForEntity(
70 "/api/webhooks/github/repository", request, Void.class);
71
72 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
73 verify(eventProcessor, times(1)).processGitHubPush(any(GitHubWebhookEvent.class));
74 }
75
76 private String createStripePaymentSuccessPayload() {
77 return "{\n" +
78 " \"id\": \"evt_test_webhook\",\n" +
79 " \"object\": \"event\",\n" +
80 " \"type\": \"payment_intent.succeeded\",\n" +
81 " \"data\": {\n" +
82 " \"object\": {\n" +
83 " \"id\": \"pi_test_payment\",\n" +
84 " \"amount\": 2000,\n" +
85 " \"currency\": \"usd\",\n" +
86 " \"status\": \"succeeded\",\n" +
87 " \"metadata\": {\n" +
88 " \"order_id\": \"test_order_123\"\n" +
89 " }\n" +
90 " }\n" +
91 " }\n" +
92 "}";
93 }
94
95 private String createGitHubPushPayload() {
96 return "{\n" +
97 " \"ref\": \"refs/heads/main\",\n" +
98 " \"repository\": {\n" +
99 " \"id\": 123456789,\n" +
100 " \"name\": \"test-repo\",\n" +
101 " \"full_name\": \"testuser/test-repo\"\n" +
102 " },\n" +
103 " \"commits\": [\n" +
104 " {\n" +
105 " \"id\": \"abc123def456\",\n" +
106 " \"message\": \"Test commit\",\n" +
107 " \"author\": {\n" +
108 " \"name\": \"Test User\",\n" +
109 " \"email\": \"test@example.com\"\n" +
110 " }\n" +
111 " }\n" +
112 " ]\n" +
113 "}";
114 }
115
116 private String createStripeSignature(String payload, String secret) throws Exception {
117 long timestamp = System.currentTimeMillis() / 1000;
118 String signedPayload = timestamp + "." + payload;
119
120 Mac sha256Hmac = Mac.getInstance("HmacSHA256");
121 SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
122 sha256Hmac.init(secretKey);
123 byte[] hash = sha256Hmac.doFinal(signedPayload.getBytes());
124
125 String signature = Hex.encodeHexString(hash);
126 return String.format("t=%d,v1=%s", timestamp, signature);
127 }
128
129 private String createGitHubSignature(String payload, String secret) throws Exception {
130 Mac sha1Hmac = Mac.getInstance("HmacSHA1");
131 SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA1");
132 sha1Hmac.init(secretKey);
133 byte[] hash = sha1Hmac.doFinal(payload.getBytes());
134
135 return "sha1=" + Hex.encodeHexString(hash);
136 }
137}
2. Manual Testing with ngrok
Testing Script:
1#!/bin/bash
2
3# Webhook Testing Script with ngrok
4
5set -e
6
7echo "=== Webhook Testing Setup ==="
8
9# Check if ngrok is installed
10if ! command -v ngrok &> /dev/null; then
11 echo "Error: ngrok is not installed. Please install from https://ngrok.com/"
12 exit 1
13fi
14
15# Start ngrok tunnel
16echo "Starting ngrok tunnel..."
17ngrok http 8080 > /dev/null &
18NGROK_PID=$!
19
20# Wait for ngrok to start
21sleep 3
22
23# Get public URL
24WEBHOOK_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url')
25
26if [ "$WEBHOOK_URL" = "null" ]; then
27 echo "Error: Failed to get ngrok URL"
28 kill $NGROK_PID
29 exit 1
30fi
31
32echo "Webhook URL: $WEBHOOK_URL"
33
34# Test endpoints
35STRIPE_ENDPOINT="$WEBHOOK_URL/api/webhooks/stripe/payments"
36GITHUB_ENDPOINT="$WEBHOOK_URL/api/webhooks/github/repository"
37SHOPIFY_ENDPOINT="$WEBHOOK_URL/api/webhooks/shopify/orders"
38GENERIC_ENDPOINT="$WEBHOOK_URL/api/webhooks/generic"
39
40echo ""
41echo "=== Test Endpoints ==="
42echo "Stripe: $STRIPE_ENDPOINT"
43echo "GitHub: $GITHUB_ENDPOINT"
44echo "Shopify: $SHOPIFY_ENDPOINT"
45echo "Generic: $GENERIC_ENDPOINT"
46
47# Test generic endpoint
48echo ""
49echo "=== Testing Generic Endpoint ==="
50curl -X POST "$GENERIC_ENDPOINT" \
51 -H "Content-Type: application/json" \
52 -H "User-Agent: WebhookTester/1.0" \
53 -d '{
54 "test": true,
55 "message": "Hello from webhook test",
56 "timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
57 }' | jq .
58
59echo ""
60echo "=== Webhook URLs for External Services ==="
61echo "Configure these URLs in your webhook providers:"
62echo ""
63echo "Stripe Dashboard -> Webhooks -> Add endpoint:"
64echo " URL: $STRIPE_ENDPOINT"
65echo " Events: payment_intent.succeeded, payment_intent.payment_failed"
66echo ""
67echo "GitHub Repository -> Settings -> Webhooks -> Add webhook:"
68echo " URL: $GITHUB_ENDPOINT"
69echo " Events: push, pull_request, issues"
70echo ""
71echo "Shopify Admin -> Settings -> Notifications -> Webhooks -> Create webhook:"
72echo " URL: $SHOPIFY_ENDPOINT"
73echo " Events: Order creation, Order payment"
74echo ""
75
76# Keep ngrok running
77echo "Press Ctrl+C to stop ngrok and exit..."
78trap "kill $NGROK_PID" EXIT
79
80wait
โ Pros and Cons Summary
๐ฏ Webhooks Advantages
Real-Time Processing
- Immediate event notification
- No polling delays
- Instant business logic execution
Resource Efficiency
- Lower server load
- Reduced bandwidth usage
- No unnecessary API calls
Scalability
- Event-driven architecture
- Horizontal scaling support
- Decoupled systems
Developer Experience
- Simple HTTP endpoints
- Standard REST patterns
- Rich ecosystem support
โ ๏ธ Webhooks Disadvantages
Network Dependencies
- Firewall restrictions
- NAT traversal issues
- Internet connectivity requirements
Reliability Challenges
- Delivery failures
- Retry complexity
- Ordering guarantees
Security Concerns
- Signature validation required
- Replay attack prevention
- Endpoint exposure
Debugging Complexity
- Harder to troubleshoot
- Async processing issues
- Limited request tracing
๐ฏ Conclusion
Webhooks are essential for modern event-driven architectures, providing efficient, real-time integrations between systems. This comprehensive guide covered:
๐ Key Takeaways:
- Choose Webhooks When: Real-time processing is critical, events are infrequent, and you need efficient resource usage
- Security First: Always validate signatures, implement proper authentication, and protect against replay attacks
- Handle Failures: Implement robust retry mechanisms, dead letter queues, and comprehensive monitoring
- Monitor Everything: Track webhook performance, failure rates, and processing times
๐ Best Practices:
- Idempotency: Design webhook handlers to be idempotent
- Validation: Always verify webhook signatures and payloads
- Async Processing: Use asynchronous processing for complex operations
- Monitoring: Implement comprehensive logging and metrics
- Testing: Build robust testing strategies including signature validation
Webhooks enable building responsive, efficient, and scalable distributed systems that react to events in real-time, making them indispensable for modern application architectures.