← Course Index | WebhookFlow — Visual Architecture Diagrams
Phase 1 Diagrams
Visual Diagrams · Read Like a Picture

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.

📐 System Architecture 🔄 Request Lifecycle ⚙️ Delivery Flowchart 🔍 Layer Zoom-In
🏗️
Diagram 1
System Architecture — The Big Picture
All components, all layers, and how data flows through them
Controller Layer
Service Layer
Repository Layer
Model Layer
Delivery Targets (external)
Database
EXTERNAL SYSTEMS — services that send webhooks to WebhookFlow 💳 Razorpay payment.captured · payment.failed 🐙 GitHub push · pull_request · release 💰 Stripe invoice.paid · subscription.ended 🔧 Any HTTP Client Postman / your own services POST /api/events/receive/{source} + X-Signature header WEBHOOKFLOW MICROSERVICE · Java 25 · port 8080 · single responsibility: webhook platform CONTROLLER LAYER — parse HTTP, call service, return JSON response 📥 EventController /api/events/* · 5 endpoints 🔔 SubscriptionController /api/subscriptions/* · 5 endpoints 🌐 SourceController /api/sources/* · 2 endpoints 📊 AnalyticsController /api/analytics/* · Health method calls SERVICE LAYER — all business logic lives here WebhookEventService receive · validate · persist entry point for all events EventDeliveryService HTTP deliver · retry backoff @Async background thread HmacValidationService HMAC-SHA256 signature check 401 if signature mismatch SubscriptionService CRUD · active lookups EventSourceService source registration AnalyticsService delivery rates · counts DELIVERY TARGETS subscribed external services 🏪 Order Service (targetUrl A) 📧 Email Service (targetUrl B) 📊 Analytics App (targetUrl C) @Async HTTP POST JPA queries REPOSITORY LAYER — Spring Data JPA interfaces · auto-generated SQL · no implementation needed WebhookEvent Repository findBySource, status, date existsByExternalId Subscription Repository findActiveBySource findByEventType DeliveryLog Repository findByEventId findBySubscriptionId EventSource Repository findByName findByActive MODEL LAYER — @Entity classes · each maps to one database table WebhookEvent Subscription DeliveryLog EventSource EventStatus JDBC / SQL DATABASE LAYER ☁️ Azure SQL Database webhookflow-server.database.windows.net:1433 · database: webhookflow · SQL Server engine mssql-jdbc driver · credentials via env vars · Azure Data Studio on Windows · JPA creates tables on startup ✓ Phase 1 — All Phases 200 OK returned immediately
How to read this diagram: Follow the solid arrows top-to-bottom. A Razorpay POST enters at the top, passes through each layer, and hits the database. The dashed green arrow going RIGHT from the Service Layer shows the async delivery path — it fires after the 200 OK is already sent back. The dashed blue arrow going up shows when the 200 OK returns.
🔄
Diagram 2
Request Lifecycle — Every Step, Every Method Call
Trace one Razorpay payment.captured event from arrival to 200 OK + async delivery
Scenario: Razorpay sends POST /api/events/receive/razorpay with a JSON body and an X-Signature header. Follow each step.
1

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

External System
2

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.

Controller Layer
3

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 InvalidSignatureException401 Unauthorized returned. No further processing.

Security Check
4

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 DuplicateEventException409 Conflict returned. Safe to ignore.

Service Layer
5

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.

Repository / DB
── Step 6 forks into two simultaneous paths ──

⑥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.

200 OK · {"eventId":"…"}
⑥b Async path — @Async fires
7

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 Pool
8

SubscriptionService.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.

HTTP Delivery + Exponential Backoff
9

DeliveryLogRepository.save() + EventStatus update

Every attempt (success or fail) is logged in DeliveryLog. After all subscribers: updates WebhookEvent.status to DELIVERED, PARTIAL, or FAILED.

Audit Log · Final Status
⚙️
Diagram 3
Async Delivery Engine — Flowchart
What EventDeliveryService does for every event, step by step with all decision points
START: WebhookEvent saved status = RECEIVED · @Async fires in thread pool SubscriptionService.findActive( source, eventType) Any active subscriptions? YES NO Keep status = RECEIVED FOR EACH active subscription... HTTP POST to subscription.targetUrl timeout: 10s · sends original raw payload HTTP 2xx response? YES DeliveryLog success=true NO Attempt < 3? YES Wait (backoff) Attempt 1 → 1s Attempt 2 → 5s Attempt 3 → 30s NO DeliveryLog success=false records statusCode, responseBody, attemptNumber More subscribers? YES next subscriber NO
DELIVERED — all subscribers returned 2xx
PARTIAL — at least one succeeded, at least one failed after 3 retries
FAILED — all subscribers failed after 3 retries each
🔍
Diagram 4
Layer Zoom-In — Controller → Service → Repository
The three core layers of Spring Boot with their rules and how they interact
CONTROLLER @RestController · @RequestMapping RULES FOR THIS LAYER: ✓ Parse HTTP (headers, path, body) ✓ Call exactly one service method ✓ Return ResponseEntity / DTO ✗ No business logic here ✗ No direct database access @PostMapping ("/receive/{source}") public ResponseEntity<...> receive( @PathVariable src, @RequestBody String body, @RequestHeader sig) // just calls service: Talks only to: Service layer classes ⬆ HTTP In / Out ⬆ calls returns SERVICE @Service · business logic home RULES FOR THIS LAYER: ✓ All business rules live here ✓ Validate inputs, check state ✓ Orchestrate multiple repos ✗ No HTTP knowledge here ✗ No raw SQL strings @Service public class WebhookEventService // 1. validate HMAC // 2. check idempotency // 3. save event // 4. @Async deliver Talks only to: Repository layer classes @Transactional boundary calls returns REPOSITORY @Repository · Spring Data JPA RULES FOR THIS LAYER: ✓ Interface only, no impl code ✓ Extend JpaRepository<T, ID> ✓ Method name → auto SQL ✗ No business logic ✗ No HTTP references public interface WebhookEvent Repository extends JpaRepository <WebhookEvent,Long> // Spring writes the SQL // for you automatically Talks only to: Database (via JPA / Hibernate) ⬇ H2 / PostgreSQL ⬇ One job: handle HTTP thin as possible — delegate logic One job: business rules fat — all the interesting code One job: data access thin — Spring writes the SQL
The golden rule of layered architecture: Each layer knows about the layer directly below it — and nothing else. The Controller knows about Service classes. The Service knows about Repository interfaces. The Repository knows about entity classes. None of them know about each other's internal implementation. This is how you make code testable, swappable, and understandable.