Webhooks: Complete Guide with Java Implementation - Event-Driven Architecture, Real-Time Integrations, and Best Practices

๐ŸŽฏ 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

AspectWebhooksHTTP PollingServer-Sent EventsWebSockets
DirectionPush (Serverโ†’Client)Pull (Clientโ†’Server)Push (Serverโ†’Client)Bidirectional
Real-TimeImmediateDelayedImmediateImmediate
Resource UsageLowHighMediumMedium
ReliabilityGood with retriesHighMediumMedium
ComplexityMediumLowMediumHigh
Firewall IssuesYesNoNoYes
ScalabilityExcellentPoorGoodGood
BandwidthEfficientInefficientEfficientEfficient
Connection StateStatelessStatelessStatefulStateful

๐Ÿ”„ 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

  1. Payment Processing

    • Real-time payment status updates
    • Subscription lifecycle events
    • Fraud detection notifications
  2. E-commerce Integration

    • Order status changes
    • Inventory updates
    • Customer lifecycle events
  3. CI/CD Pipelines

    • Code repository changes
    • Build completion notifications
    • Deployment status updates
  4. Communication Systems

    • Message delivery status
    • User presence updates
    • Chat events
  5. IoT and Monitoring

    • Device status changes
    • Alert notifications
    • Metric threshold breaches

โŒ When NOT to Use Webhooks

  1. High-Frequency Updates (>1000/second)
  2. Request-Response Patterns requiring immediate responses
  3. Client-Initiated Queries for specific data
  4. Environments with strict firewall restrictions
  5. 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

  1. Real-Time Processing

    • Immediate event notification
    • No polling delays
    • Instant business logic execution
  2. Resource Efficiency

    • Lower server load
    • Reduced bandwidth usage
    • No unnecessary API calls
  3. Scalability

    • Event-driven architecture
    • Horizontal scaling support
    • Decoupled systems
  4. Developer Experience

    • Simple HTTP endpoints
    • Standard REST patterns
    • Rich ecosystem support

โš ๏ธ Webhooks Disadvantages

  1. Network Dependencies

    • Firewall restrictions
    • NAT traversal issues
    • Internet connectivity requirements
  2. Reliability Challenges

    • Delivery failures
    • Retry complexity
    • Ordering guarantees
  3. Security Concerns

    • Signature validation required
    • Replay attack prevention
    • Endpoint exposure
  4. 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:

  1. Choose Webhooks When: Real-time processing is critical, events are infrequent, and you need efficient resource usage
  2. Security First: Always validate signatures, implement proper authentication, and protect against replay attacks
  3. Handle Failures: Implement robust retry mechanisms, dead letter queues, and comprehensive monitoring
  4. Monitor Everything: Track webhook performance, failure rates, and processing times

๐Ÿ“‹ Best Practices:

  1. Idempotency: Design webhook handlers to be idempotent
  2. Validation: Always verify webhook signatures and payloads
  3. Async Processing: Use asynchronous processing for complex operations
  4. Monitoring: Implement comprehensive logging and metrics
  5. 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.