Skip to content

Payment Flow Example

This example demonstrates a resilient payment flow with stock reservation, payment processing, and compensation.

The Scenario

  1. Reserve Stock: Call an inventory service to hold the items.
  2. Process Payment: Call a payment gateway (Stripe/PayPal).
  3. Confirm Order: Update the database status.
  4. Notify User: Send a confirmation email.

If the payment fails, we must release the reserved stock.

The Code

ts
import { create } from "@eddiecbrl/orchestrix";
import { z } from "zod";

// 1. Define input schema
const PaymentSchema = z.object({
  orderId: z.string(),
  items: z.array(z.object({ sku: z.string(), qty: z.number() })),
  amount: z.number(),
  userId: z.string(),
});

type PaymentInput = z.infer<typeof PaymentSchema>;

// 2. Create the flow
const paymentFlow = create<PaymentInput>("payment-saga", { schema: PaymentSchema })
  
  // Step 1: Reserve Stock
  .step("reserve-stock", async (ctx) => {
    const { items } = ctx.input;
    const reservationIds = await inventoryService.reserve(items);
    ctx.set("reservationIds", reservationIds);
  }, {
    compensate: async (ctx) => {
      const ids = ctx.get<string[]>("reservationIds");
      await inventoryService.release(ids);
    }
  })

  // Step 2: Process Payment
  .step("process-payment", async (ctx) => {
    const { amount, userId, orderId } = ctx.input;
    const transactionId = await paymentGateway.charge({
      amount,
      userId,
      idempotencyKey: `pay_${orderId}`
    });
    ctx.set("transactionId", transactionId);
  }, {
    retries: 3,
    retryDelayMs: 2000,
    backoffFactor: 'exponential'
  })

  // Step 3: Confirm Order
  .step("confirm-order", async (ctx) => {
    const { orderId } = ctx.input;
    const transactionId = ctx.get<string>("transactionId");
    await db.orders.update(orderId, { status: 'paid', transactionId });
  })

  // Step 4: Notify
  .step("send-email", async (ctx) => {
    await emailService.send(ctx.input.userId, "Order Confirmed!");
  }, {
    retries: 5 // We really want to send this email
  });

// 3. Run the flow
const result = await paymentFlow.run({
  orderId: "ORD-123",
  items: [{ sku: "LAPTOP-01", qty: 1 }],
  amount: 1200,
  userId: "user_99"
});

if (result.status === 'completed') {
  console.log("Payment successful!");
} else {
  console.error("Payment failed. Compensations were executed.");
}

Why this is professional

  • Type Safety: Uses Zod for runtime validation and TypeScript for compile-time safety.
  • Resilience: Retries on the payment gateway and email service handle transient network issues.
  • Consistency: The compensate function ensures we don't leave items reserved if the payment fails.
  • Idempotency: We pass an idempotencyKey to the payment gateway (derived from our order ID).

Released under the MIT License.