Multi-Service Orchestration

Coordinate operations across multiple microservices using QueueBear workflows.

Orchestration vs Choreography

Pattern Description Pros Cons
Orchestration Central coordinator directs the flow Easy to understand, centralized logic Single point of coordination
Choreography Services react to events autonomously Decoupled services Flow harder to trace

QueueBear workflows excel at orchestration - using a central workflow to coordinate multiple services.

Basic Orchestration Pattern

import { serve } from "queuebear";

export const POST = serve<CheckoutInput>(async (ctx) => {
  const { orderId, customerId, items, paymentMethod, shippingAddress } = ctx.input;

  // 1. Validate order with Inventory Service
  const inventory = await ctx.run("validate-inventory", async () => {
    const response = await fetch("https://inventory.internal/validate", {
      method: "POST",
      body: JSON.stringify({ items }),
    });
    if (!response.ok) throw new Error("Inventory validation failed");
    return response.json();
  });

  // 2. Calculate pricing with Pricing Service
  const pricing = await ctx.run("calculate-pricing", async () => {
    const response = await fetch("https://pricing.internal/calculate", {
      method: "POST",
      body: JSON.stringify({ items, customerId }),
    });
    return response.json();
  });

  // 3. Process payment with Payment Service
  const payment = await ctx.run("process-payment", async () => {
    const response = await fetch("https://payments.internal/charge", {
      method: "POST",
      body: JSON.stringify({
        customerId,
        amount: pricing.total,
        paymentMethod,
        orderId,
      }),
    });
    if (!response.ok) throw new Error("Payment failed");
    return response.json();
  });

  // 4. Create shipment with Shipping Service
  const shipment = await ctx.run("create-shipment", async () => {
    const response = await fetch("https://shipping.internal/create", {
      method: "POST",
      body: JSON.stringify({ orderId, items, address: shippingAddress }),
    });
    return response.json();
  });

  // 5. Send confirmation with Notification Service
  await ctx.run("send-confirmation", async () => {
    await fetch("https://notifications.internal/send", {
      method: "POST",
      body: JSON.stringify({
        type: "order_confirmed",
        customerId,
        data: { orderId, shipmentId: shipment.id },
      }),
    });
  });

  return {
    success: true,
    orderId,
    paymentId: payment.id,
    shipmentId: shipment.id,
  };
});

Parallel Service Calls

When services are independent, call them in parallel for better performance:

export const POST = serve<DashboardInput>(async (ctx) => {
  const { userId } = ctx.input;

  // Fetch from multiple services in parallel
  const [user, orders, recommendations, notifications] = await ctx.parallel([
    {
      name: "fetch-user",
      fn: async () => {
        const res = await fetch(`https://users.internal/users/${userId}`);
        return res.json();
      },
    },
    {
      name: "fetch-orders",
      fn: async () => {
        const res = await fetch(`https://orders.internal/users/${userId}/orders`);
        return res.json();
      },
    },
    {
      name: "fetch-recommendations",
      fn: async () => {
        const res = await fetch(`https://recommendations.internal/users/${userId}`);
        return res.json();
      },
    },
    {
      name: "fetch-notifications",
      fn: async () => {
        const res = await fetch(`https://notifications.internal/users/${userId}`);
        return res.json();
      },
    },
  ]);

  return { user, orders, recommendations, notifications };
});

Hybrid: Orchestration with Event Coordination

Combine orchestration with events for complex flows that depend on external triggers:

export const POST = serve<SubscriptionInput>(async (ctx) => {
  const { customerId, planId } = ctx.input;

  // Step 1: Create subscription
  const subscription = await ctx.run("create-subscription", async () => {
    return await billingService.createSubscription(customerId, planId);
  });

  // Step 2: Wait for first payment (event from payment processor webhook)
  const paymentEvent = await ctx.waitForEvent(
    "wait-first-payment",
    "payment.completed",
    {
      eventKey: subscription.id,
      timeoutSeconds: 3600,  // 1 hour for payment to process
    }
  );

  // Step 3: Activate subscription
  await ctx.run("activate-subscription", async () => {
    await billingService.activate(subscription.id);
  });

  // Step 4: Provision resources
  await ctx.run("provision-resources", async () => {
    await provisioningService.setup(customerId, planId);
  });

  return { subscriptionId: subscription.id, status: "active" };
});

// External: Payment processor webhook sends event
app.post("/webhooks/payment-processor", async (req, res) => {
  const { subscriptionId, status } = req.body;

  if (status === "succeeded") {
    await qb.workflows.sendEvent("payment.completed", {
      eventKey: subscriptionId,
      payload: { amount: req.body.amount },
    });
  }

  res.json({ received: true });
});

Example: E-Commerce with 5 Services

A complete checkout spanning Orders, Inventory, Payments, Shipping, and Notifications:

export const POST = serve<CheckoutInput>(async (ctx) => {
  const { orderId, customerId, items, payment, shipping } = ctx.input;

  // === PHASE 1: Validation (parallel) ===
  const [inventoryStatus, customerStatus] = await ctx.parallel([
    {
      name: "check-inventory",
      fn: async () => {
        const res = await fetch("https://inventory.internal/check", {
          method: "POST",
          body: JSON.stringify({ items }),
        });
        const data = await res.json();
        if (!data.available) throw new Error(`Out of stock: ${data.unavailable}`);
        return data;
      },
    },
    {
      name: "validate-customer",
      fn: async () => {
        const res = await fetch(`https://customers.internal/validate/${customerId}`);
        const data = await res.json();
        if (data.blocked) throw new Error("Customer account blocked");
        return data;
      },
    },
  ]);

  // === PHASE 2: Create Order ===
  const order = await ctx.run("create-order", async () => {
    const res = await fetch("https://orders.internal/create", {
      method: "POST",
      body: JSON.stringify({ orderId, customerId, items, status: "pending" }),
    });
    return res.json();
  });

  // === PHASE 3: Payment Processing ===
  const paymentResult = await ctx.run("process-payment", async () => {
    const res = await fetch("https://payments.internal/charge", {
      method: "POST",
      body: JSON.stringify({ customerId, orderId, amount: order.total }),
    });
    if (!res.ok) throw new Error("Payment failed");
    return res.json();
  });

  // === PHASE 4: Fulfillment (parallel) ===
  try {
    const [inventoryReservation, shipmentCreation] = await ctx.parallel([
      {
        name: "reserve-inventory",
        fn: async () => {
          const res = await fetch("https://inventory.internal/reserve", {
            method: "POST",
            body: JSON.stringify({ orderId, items }),
          });
          return res.json();
        },
      },
      {
        name: "create-shipment",
        fn: async () => {
          const res = await fetch("https://shipping.internal/create", {
            method: "POST",
            body: JSON.stringify({ orderId, items, address: shipping.address }),
          });
          return res.json();
        },
      },
    ]);

    // === PHASE 5: Send Notifications (parallel) ===
    await ctx.parallel([
      {
        name: "send-confirmation-email",
        fn: () => fetch("https://notifications.internal/email", {
          method: "POST",
          body: JSON.stringify({ type: "order_confirmation", customerId, orderId }),
        }),
      },
      {
        name: "send-sms",
        fn: () => customerStatus.phone && fetch("https://notifications.internal/sms", {
          method: "POST",
          body: JSON.stringify({ phone: customerStatus.phone, orderId }),
        }),
      },
    ]);

    return {
      success: true,
      orderId,
      paymentId: paymentResult.id,
      shipmentId: shipmentCreation.id,
    };

  } catch (fulfillmentError) {
    // === COMPENSATION: Refund on failure ===
    await ctx.run("refund-payment", async () => {
      await fetch("https://payments.internal/refund", {
        method: "POST",
        body: JSON.stringify({ paymentId: paymentResult.id }),
      });
    });
    throw fulfillmentError;
  }
});

Best Practices

1. Propagate Correlation IDs

export const POST = serve<Input>(async (ctx) => {
  const correlationId = ctx.runId;  // Use workflow run ID

  await ctx.run("call-service", async () => {
    await fetch(url, {
      headers: {
        "X-Correlation-ID": correlationId,
        "X-Workflow-Run-ID": ctx.runId,
      },
    });
  });
});

2. Use Timeouts

await ctx.run("call-external", async () => {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 10000);

  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timeout);
  }
});

3. Design for Idempotency

// Pass orderId as idempotency key to each service
await paymentService.charge({
  orderId,  // Service uses this to prevent duplicate charges
  amount,
});

Summary

Pattern Use When
Sequential orchestration Operations must happen in order
Parallel orchestration Independent operations
Event-based hybrid Complex flows with external triggers
Compensation Rollback on partial failures