WebhookFlow —
Architecture & Implementation Roadmap
A precise map of what is already built (Phase 1 — a self-contained webhook microservice with all real class names) and a phase-by-phase plan for growing it into the full distributed platform.
This microservice handles five concerns inside one deployable unit
| Job | How it's done | Key classes |
|---|---|---|
| Receive & route | HTTP POST lands on /api/events/receive/{source} |
EventController |
| Validate HMAC | HMAC-SHA256 signature check before any DB write | HmacValidationService |
| Persist events | Save raw payload + metadata to H2 (dev) / PostgreSQL (prod) | WebhookEventService · WebhookEventRepository |
| Async delivery | Spring @Async thread pool — HTTP POST to each subscriber, exponential backoff retries |
EventDeliveryService · SubscriptionService |
| Observe & manage | REST endpoints for events, subscriptions, sources, analytics, and health | AnalyticsController · AnalyticsService |
Spring Boot enforces a layered architecture. Each layer has one responsibility and depends only on the layer below it. HTTP never talks directly to the database.
HTTP Clients
Razorpay · GitHub · Stripe · Postman · Your own services — any system that sends an HTTP POST
REST API layer — parse HTTP, call service, return JSON
Receives HTTP requests. Reads headers, path variables, request body. Calls the service layer. Never contains business logic.
Business logic layer — where all decisions are made
All rules live here: signature validation, idempotency check, retry logic, delivery matching, analytics aggregation.
Data access layer — Spring Data JPA interfaces
Declare method signatures; Spring generates the SQL. No implementation code needed for standard CRUD and filtered queries.
Domain entities — JPA-mapped database tables
Each class maps to one database table. Fields become columns. Relationships use JPA annotations.
Azure SQL Database (SQL Server engine)
Managed cloud database at webhookflow-server.database.windows.net:1433. JPA creates tables on startup (ddl-auto=update). Connect from Windows via Azure Data Studio. Credentials in environment variables.
Cross-cutting layers (used by all layers)
Request / Response objects
Decouple what goes over HTTP from the internal entity model.
Error handling
Typed exceptions bubble up; one global handler maps them to the right HTTP status codes.
payment.captured webhook to POST /api/events/receive/razorpay. Here is the exact method chain through the codebase.① Razorpay HTTP POST
Headers: X-Signature, X-Event-Type: payment.captured, X-External-Id: pay_abc123
Body: raw JSON payload
② EventController.receive()
Extracts source = "razorpay" from path, reads raw body as String (preserves bytes for HMAC), reads headers
③ WebhookEventService.receiveEvent()
Orchestrates the full receive pipeline — source lookup → HMAC → idempotency → persist → trigger delivery
④a HmacValidationService.validate()
Computes HMAC-SHA256 of raw body using the source's secret key. Throws InvalidSignatureException if mismatch → 401
④b Idempotency check
If X-External-Id already in DB, throws DuplicateEventException → 409. Prevents double-processing of retried webhooks.
⑤ Persist WebhookEvent
Saves entity with status RECEIVED, raw payload, source name, event type, timestamp. JPA auto-generates id.
⑥ Return 200 OK immediately
Controller returns ReceiveEventResponse with the new event ID. External system gets its response fast — under 50ms typically.
⑦ @Async delivery fires
Spring's thread pool picks up EventDeliveryService.deliverEvent() in a background thread. The HTTP response is already sent.
① Match subscriptions
SubscriptionService.findActive(source, eventType) — finds all active WebhookSubscription rows that match this source and event type.
② HTTP POST to targetUrl
Java's HttpClient sends the original payload to the subscriber's registered URL. Timeout: 10 seconds.
③ Write DeliveryLog
Records: attempt number, HTTP status code, response body, latency, timestamp. Every attempt — success and failure — is logged.
④ Exponential backoff retry
Attempt 1 → wait 1s → Attempt 2 → wait 5s → Attempt 3 → wait 30s → mark FAILED. Total: 3 retries max.
④ Update event status
After all subscribers: DELIVERED (all succeeded), PARTIAL (some failed), or FAILED (all failed).
@Async uses an in-process thread pool. If the JVM crashes mid-delivery, in-flight events are lost. Phase 3 replaces this with Azure Service Bus — a durable message queue that survives restarts.| Table / Entity | Key Fields | Purpose |
|---|---|---|
| webhook_events WebhookEvent |
id, source, eventType, rawPayload, externalId, status, receivedAt |
The central log of every event received. externalId is the idempotency key. status tracks delivery lifecycle. |
| webhook_subscriptions WebhookSubscription |
id, targetUrl, source, eventType, secretKey, active |
Who wants to receive which events. active flag allows pause without deletion. secretKey used to sign outbound deliveries. |
| delivery_logs DeliveryLog |
id, eventId (FK), subscriptionId (FK), attemptNumber, statusCode, responseBody, success, attemptedAt |
Audit trail of every delivery attempt. One row per attempt per subscription. Join to WebhookEvent and WebhookSubscription. |
| event_sources EventSource |
id, name, secretKey, description, active |
Registered sources (Razorpay, GitHub, etc.). The secretKey is the HMAC validation key. Must be registered before events can be received. |
Entity relationships
Event endpoints — EventController
| Method | Path | What it does |
|---|---|---|
| POST | /api/events/receive/{source} | Receive webhook from external system — the main entry point |
| GET | /api/events | List events with optional filters: ?source=&status=&from= |
| GET | /api/events/{id} | Get single event with all its delivery logs |
| GET | /api/events/{id}/deliveries | All delivery attempts for one event |
| POST | /api/events/{id}/retry | Re-trigger delivery for FAILED or PARTIAL events |
Subscription endpoints — SubscriptionController
| Method | Path | What it does |
|---|---|---|
| POST | /api/subscriptions | Register a new subscriber (targetUrl + source + eventType) |
| GET | /api/subscriptions | List all subscriptions |
| PUT | /api/subscriptions/{id} | Update subscription config (URL, secret, filters) |
| PATCH | /api/subscriptions/{id}/toggle | Enable or disable a subscription without deleting it |
| DELETE | /api/subscriptions/{id} | Remove subscription permanently |
Source, Analytics & Health
| Method | Path | Controller / What it does |
|---|---|---|
| POST | /api/sources | SourceController — register a new event source with HMAC secret |
| GET | /api/sources | SourceController — list all registered sources |
| GET | /api/analytics/summary | AnalyticsController — event counts by status and source |
| GET | /api/analytics/delivery-rate | AnalyticsController — success % per subscription |
| GET | /health | Health — status + total event count (no Spring Actuator dependency) |
- Full layered architecture: Controller → Service → Repository → JPA → H2
- HMAC-SHA256 signature validation on all incoming events
- Idempotency via
externalIddeduplication @Asyncdelivery engine with exponential backoff (1s → 5s → 30s)- DeliveryLog audit trail for every attempt
- Full REST API: 15 endpoints across 4 controllers
- AnalyticsService: delivery rates, status counts
- Swap H2 → PostgreSQL (3-line change in
application.properties+ add driver dependency) - Add Redis for idempotency key cache (sub-millisecond duplicate check vs. DB query)
- Add Spring Actuator:
/actuator/health,/actuator/metrics,/actuator/info - Structured JSON logging with correlation IDs (trace a single event across all log lines)
- Add Flyway or Liquibase for database migrations
- Replace
@Asyncin-process delivery with Azure Service Bus queue - Receiver publishes event message to queue → returns 200 OK immediately
- New event-processor service subscribes to queue and handles delivery independently
- Events are durable: if processor crashes, messages wait in queue and replay on restart
- Scale receiver and processor independently (receive spikes ≠ processing spikes)
- New notification-service (port 8082) — email via Amazon SES, SMS via Twilio, Slack via webhooks
- Event processor publishes a notification event after FAILED delivery
- Operators get alerted when a subscriber's delivery keeps failing
- Subscribers can self-configure notification preferences via the API
- Write a
Dockerfilefor each service (multi-stage build: compile → minimal JRE image) docker-compose.ymlspins up all services + PostgreSQL + Redis + Service Bus emulator locally- No more "it works on my machine" — same container image runs locally and in Azure
- Add GitHub Actions CI/CD: build → test → push image to Azure Container Registry on merge to main
- Create AKS cluster on Azure; write Kubernetes YAML manifests (Deployment, Service, Ingress, ConfigMap, Secret)
- API Gateway (NGINX Ingress or Azure API Management) routes external traffic to services
- Horizontal Pod Autoscaler: scale receiver pods up during traffic spikes, down during quiet periods
- Rolling deployments — zero downtime upgrades; rollback with one
kubectlcommand - Azure Monitor + Application Insights for dashboards and alerting
- React + TypeScript SPA (port 3000) — consumes the existing REST API
- Live event feed using Server-Sent Events (SSE) or polling every 3 seconds
- Subscription manager: create, toggle, delete subscriptions without Postman
- Delivery status timeline: see every retry attempt for any event
- Analytics charts: delivery rate per subscription, events per source over time
- New ai-service (port 8083): LangChain4j + Azure OpenAI GPT-4o
- Natural language queries: "Show me all failed payments from Razorpay in the last hour" → translates to API call
- Automatic event classification: inspect payload → label event type + severity + suggested action
- Anomaly detection: flag unusual event patterns (spike in failures, unknown source IDs)
- LLM-generated delivery failure summaries: "5 of 8 subscribers failed due to connection timeout on targetUrl X"
EventController they wrote in Phase 1 — they have just surrounded it with a production-grade system that handles real-world scale.