Architecture 9 min read

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.

H

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:

Code
[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:

Code
[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:

Json
{
  "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.action format: order.created, payment.completed, inventory.updated
  • Past tense for completed actions: order.shipped, not order.ship
  • Present tense for state changes: order.processing

Versioning Events

Plan for schema evolution from the start:

Javascript
// 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:

Javascript
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

Javascript
// 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

Javascript
// 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:

Javascript
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:

Code
order.created → [Payment Service] → payment.completed
                                          ↓
                                    [Inventory Service] → inventory.reserved
                                                              ↓
                                                        [Shipping Service] → shipment.created

If any step fails, publish compensating events:

Javascript
// 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:

Javascript
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:

Javascript
// 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.

Tags: microservicesevent-drivenwebhooksarchitecturedistributed-systems

Share this article

Ready to try HookWatch?

Start monitoring your webhooks in minutes. No credit card required.

Start Free Today