Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tutorial: Microservice Event Bus

This tutorial shows how to use pg_tide as a reliable event bus between microservices. Instead of direct service-to-service HTTP calls (which create tight coupling and cascade failures), services publish events to their local outbox and subscribe to events from other services via inboxes.

Architecture

┌─────────────────┐         ┌──────────────────┐         ┌───────────────────┐
│  Order Service  │         │  Payment Service │         │  Shipping Service │
│                 │         │                  │         │                   │
│  outbox: orders │──┐  ┌──→│  inbox: payments │    ┌──→│  inbox: shipping  │
└─────────────────┘  │  │   └──────────────────┘    │   └───────────────────┘
                     │  │                            │
                     ↓  │                            │
              ┌─────────────┐                        │
              │    NATS     │────────────────────────┘
              │  (or Kafka) │
              └─────────────┘
                     ↑
                     │
┌─────────────────┐  │
│ Inventory Svc   │  │
│                 │  │
│ outbox: stock   │──┘
└─────────────────┘

What You'll Build

  • Order Service publishes order.created, order.cancelled events
  • Payment Service subscribes to order events and processes payments
  • Shipping Service subscribes to order events and initiates fulfillment
  • Each service has its own PostgreSQL database with pg_tide

Step 1: Order Service Setup

-- In the order service's database
CREATE EXTENSION pg_tide;
SELECT tide.outbox_create('order_events');

Application code publishes events within the order transaction:

BEGIN;
INSERT INTO orders (id, customer_id, total, status)
VALUES ('ORD-001', 'CUST-42', 149.99, 'created');

SELECT tide.outbox_publish('order_events', 'orders', jsonb_build_object(
    'event_type', 'order.created',
    'order_id', 'ORD-001',
    'customer_id', 'CUST-42',
    'total', 149.99,
    'items', jsonb_build_array(
        jsonb_build_object('sku', 'WIDGET-A', 'qty', 2),
        jsonb_build_object('sku', 'GADGET-B', 'qty', 1)
    )
));
COMMIT;

Configure the relay to publish to NATS:

SELECT tide.relay_set_outbox(
    'orders-to-nats',
    'order_events',
    '{
        "sink_type": "nats",
        "url": "nats://nats:4222",
        "subject_template": "events.orders.{op}"
    }'::jsonb
);

Step 2: Payment Service Setup

-- In the payment service's database
CREATE EXTENSION pg_tide;
SELECT tide.inbox_create('payment_triggers');

Configure an inbox pipeline that subscribes to order events:

SELECT tide.relay_set_inbox(
    'orders-for-payments',
    'payment_triggers',
    '{
        "source_type": "nats",
        "url": "nats://nats:4222",
        "subject": "events.orders.>",
        "consumer_group": "payment-service",
        "durable_name": "payment-service"
    }'::jsonb
);

Process incoming events:

-- Payment service worker queries pending inbox messages
SELECT id, payload
FROM tide.inbox_pending('payment_triggers')
LIMIT 10;

-- After processing, mark as done
SELECT tide.inbox_mark_processed('payment_triggers', 42);

Step 3: Shipping Service Setup

-- In the shipping service's database
CREATE EXTENSION pg_tide;
SELECT tide.inbox_create('shipping_triggers');

SELECT tide.relay_set_inbox(
    'orders-for-shipping',
    'shipping_triggers',
    '{
        "source_type": "nats",
        "url": "nats://nats:4222",
        "subject": "events.orders.>",
        "consumer_group": "shipping-service",
        "durable_name": "shipping-service",
        "transform": {
            "filter": "payload.event_type == '"'"'order.created'"'"'"
        }
    }'::jsonb
);

The shipping service only processes order.created events (not cancellations) thanks to the transform filter.

Step 4: Start Relays

Each service runs its own relay instance:

# Order service relay
pg-tide --postgres-url "postgres://user:pass@orders-db/orders"

# Payment service relay
pg-tide --postgres-url "postgres://user:pass@payments-db/payments"

# Shipping service relay
pg-tide --postgres-url "postgres://user:pass@shipping-db/shipping"

Benefits of This Architecture

Loose coupling: Services don't know about each other. The order service publishes events without knowing who consumes them. New consumers can subscribe without changing the producer.

Reliability: The transactional outbox guarantees events are published if and only if the business transaction commits. No dual-write problems.

Independent scaling: Each service scales independently. The payment service can process events at its own pace without affecting the order service.

Resilience: If the payment service is down, events queue in NATS (with durable consumers) and are delivered when it recovers. No lost events, no cascading failures.

Further Reading