WebhookFlow — Architecture
in Pictures
Four visual diagrams that show how the system works — from the big picture down to individual method calls. No walls of text. Read the arrows.
POST /api/events/receive/razorpay with a JSON body and an X-Signature header. Follow each step.Razorpay sends HTTP POST
Headers: X-Signature: sha256=abc… · X-Event-Type: payment.captured · X-External-Id: pay_OEjXYZ
Body: raw JSON payload with payment details
EventController.receive()
Spring extracts source = "razorpay" from the URL path. Reads raw body as String (not parsed — raw bytes needed for HMAC). Reads three headers. Calls the service layer.
HmacValidationService.validate()
Looks up the EventSource by name → gets its secretKey. Computes HMAC-SHA256 of the raw body using that key. Compares result to the incoming X-Signature header.
🛑 If signature mismatch → throws InvalidSignatureException → 401 Unauthorized returned. No further processing.
WebhookEventService.receiveEvent() — idempotency check
Checks if X-External-Id already exists in the database. Razorpay retries failed deliveries — without this check we'd process the same payment twice.
🔁 If duplicate → throws DuplicateEventException → 409 Conflict returned. Safe to ignore.
WebhookEventRepository.save(WebhookEvent)
Creates a WebhookEvent entity: sets source, eventType, rawPayload, externalId, status = RECEIVED, receivedAt = now(). Spring Data JPA writes to the database. Auto-generated id returned.
⑥a Sync path — 200 OK
EventController returns ReceiveEventResponse{eventId}. Razorpay receives its response in under 50ms. This is the end of the synchronous HTTP request.
EventDeliveryService.deliverEvent()
Runs in a background thread from Spring's async thread pool. The HTTP response to Razorpay is already sent — this runs independently.
@Async Thread PoolSubscriptionService.findActive() + HTTP POST
Finds all active subscriptions matching this source and eventType. For each: sends HTTP POST to its targetUrl. Timeout: 10 seconds. Retries on failure: wait 1s → 5s → 30s.
DeliveryLogRepository.save() + EventStatus update
Every attempt (success or fail) is logged in DeliveryLog. After all subscribers: updates WebhookEvent.status to DELIVERED, PARTIAL, or FAILED.