Building Event-Driven Microservices with Webhooks
Webhooks are the glue that connects microservices. Learn how to design event-driven architectures that are scalable, resilient, and easy to maintain.
HookWatch Team
February 3, 2026
Microservices need to communicate. While synchronous REST calls create tight coupling, webhooks enable loose, event-driven communication that scales gracefully and handles failures naturally.
The Problem with Synchronous Communication
In a traditional microservices setup, services call each other directly:
[Order Service] → REST → [Payment Service] → REST → [Inventory Service]
↓
REST → [Shipping Service]
This creates problems:
- Tight coupling: Every service knows about its downstream dependencies
- Cascading failures: If Payment Service is down, Order Service fails too
- Latency chains: Total response time is the sum of all service calls
- Deployment complexity: Changes ripple across services
Event-Driven Architecture with Webhooks
Instead, services publish events and subscribe to events they care about:
[Order Service] → event: order.created
↓
[Event Bus]
↓ ↓ ↓
[Payment] [Inventory] [Notification]
Each service is independent. It publishes events when something happens and subscribes to events it needs to react to.
Designing Your Event Schema
Event Structure
Use a consistent envelope for all events:
{
"id": "evt_a1b2c3d4",
"type": "order.created",
"version": "1.0",
"timestamp": "2026-02-03T10:30:00Z",
"source": "order-service",
"data": {
"orderId": "ord_789",
"customerId": "cust_456",
"items": [
{ "sku": "WIDGET-001", "quantity": 2, "price": 29.99 }
],
"total": 59.98
},
"metadata": {
"correlationId": "req_xyz",
"userId": "user_123"
}
}
Naming Conventions
Use a consistent naming pattern for events:
resource.actionformat:order.created,payment.completed,inventory.updated- Past tense for completed actions:
order.shipped, notorder.ship - Present tense for state changes:
order.processing
Versioning Events
Plan for schema evolution from the start:
// Version 1
{
"type": "order.created",
"version": "1.0",
"data": {
"orderId": "ord_789",
"total": 59.98
}
}
// Version 2 - added shipping address
{
"type": "order.created",
"version": "2.0",
"data": {
"orderId": "ord_789",
"total": 59.98,
"shippingAddress": {
"street": "123 Main St",
"city": "Portland"
}
}
}
Handle multiple versions in your consumers:
function handleOrderCreated(event) {
const order = event.data;
// Handle both v1 and v2
const address = order.shippingAddress || null;
processOrder(order.orderId, order.total, address);
}
Implementing the Pattern
Service A: Publishing Events
// order-service/webhooks.js
class EventPublisher {
constructor(subscribers) {
this.subscribers = subscribers; // Loaded from config/database
}
async publish(eventType, data) {
const event = {
id: crypto.randomUUID(),
type: eventType,
version: '1.0',
timestamp: new Date().toISOString(),
source: 'order-service',
data
};
// Store event for replay capability
await db.events.create(event);
// Deliver to all subscribers
const deliveries = this.subscribers
.filter(sub => sub.events.includes(eventType))
.map(sub => this.deliver(sub.url, event));
await Promise.allSettled(deliveries);
}
async deliver(url, event) {
const body = JSON.stringify(event);
const signature = this.sign(body);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Event-Signature': signature,
'X-Event-ID': event.id
},
body
});
if (!response.ok) {
// Queue for retry
await retryQueue.add({ url, event });
}
}
}
// Usage in order creation
async function createOrder(orderData) {
const order = await db.orders.create(orderData);
await publisher.publish('order.created', {
orderId: order.id,
customerId: order.customerId,
items: order.items,
total: order.total
});
return order;
}
Service B: Consuming Events
// payment-service/webhooks.js
app.post('/events', async (req, res) => {
const event = req.body;
// Acknowledge immediately
res.status(200).json({ received: true });
// Route to handler
switch (event.type) {
case 'order.created':
await handleOrderCreated(event);
break;
case 'order.cancelled':
await handleOrderCancelled(event);
break;
default:
console.log(`Unhandled event: ${event.type}`);
}
});
async function handleOrderCreated(event) {
const { orderId, customerId, total } = event.data;
// Create payment intent
const payment = await createPaymentIntent({
orderId,
customerId,
amount: total
});
// Publish our own event
await publisher.publish('payment.initiated', {
paymentId: payment.id,
orderId,
amount: total
});
}
Handling Failures Gracefully
Dead Letter Queue
Events that fail repeatedly go to a dead letter queue for investigation:
async function processWithRetry(event, handler, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
await handler(event);
return; // Success
} catch (error) {
console.error(`Attempt ${attempt + 1} failed:`, error.message);
if (attempt === maxRetries - 1) {
// Send to dead letter queue
await deadLetterQueue.add({
event,
error: error.message,
attempts: maxRetries,
lastAttempt: new Date()
});
await alertOps(`Event ${event.id} moved to DLQ`);
}
// Exponential backoff
await sleep(Math.pow(2, attempt) * 1000);
}
}
}
Saga Pattern for Multi-Step Workflows
When multiple services must coordinate, use the saga pattern:
order.created → [Payment Service] → payment.completed
↓
[Inventory Service] → inventory.reserved
↓
[Shipping Service] → shipment.created
If any step fails, publish compensating events:
// If inventory reservation fails
async function handleInventoryFailed(event) {
const { orderId, paymentId } = event.data;
// Reverse the payment
await refundPayment(paymentId);
// Update order status
await publisher.publish('order.failed', {
orderId,
reason: 'Insufficient inventory'
});
}
Observability
Correlation IDs
Pass correlation IDs through the event chain for end-to-end tracing:
async function publish(eventType, data, correlationId) {
const event = {
id: crypto.randomUUID(),
type: eventType,
data,
metadata: {
correlationId: correlationId || crypto.randomUUID()
}
};
// Correlation ID follows the event through all services
await deliver(event);
}
Event Flow Dashboard
Track events as they flow through your system:
// Log every event transition
async function logEventFlow(event, service, action) {
await metrics.record({
eventId: event.id,
eventType: event.type,
correlationId: event.metadata.correlationId,
service,
action, // received, processed, published, failed
timestamp: Date.now()
});
}
Why HookWatch for Microservice Events
HookWatch acts as a reliable event bus for your microservices:
- Guaranteed delivery: Events are stored and retried until delivered
- Full audit trail: Every event logged with headers, body, and response
- Replay capability: Re-process events after fixing bugs
- Real-time monitoring: See event flow across all services instantly
- Failure alerts: Know immediately when a service stops processing events
Build resilient microservices by treating events as first-class infrastructure.