Microservices with NestJS: Integrating Multi-Gateway Payments

Building payments across Stripe and PayPal in NestJS microservices: idempotency, webhooks, and keeping money flows reliable.

Payments are where architectural shortcuts come back to bite you. A dropped webhook or a double-charged customer isn't a bug report — it's a refund, a chargeback, and a trust problem. Here's how I structure a reliable, multi-gateway payments layer in NestJS.

Treat the gateway as a detail, not the design

Your domain shouldn't know whether a payment went through Stripe or PayPal. Define an interface and let each gateway implement it:

ts
export interface PaymentGateway {
  createIntent(order: Order): Promise<PaymentIntent>;
  capture(intentId: string): Promise<PaymentResult>;
  refund(paymentId: string, amount: number): Promise<RefundResult>;
}

A PaymentService selects the right provider at runtime. Adding a third gateway later means writing one adapter, not rewriting the booking flow.

Make every operation idempotent

Networks retry. Users double-click. Webhooks fire twice. Every money operation needs an idempotency key so a retry is a no-op rather than a second charge:

ts
await this.payments.createIntent(order, {
  idempotencyKey: `order:${order.id}:intent`,
});

Store the key with the result. If the same key arrives again, return the stored result instead of calling the gateway.

Webhooks are the source of truth

Never mark an order paid because the client said so. The client can lie, lose connection, or close the tab. The webhook from the gateway is authoritative.

  1. Verify the signature on every webhook. Reject anything unsigned.
  2. Make the handler idempotent — gateways deliver "at least once."
  3. Acknowledge fast, process async. Push the work onto a queue.

Treat the client confirmation as a UX hint and the webhook as the ledger. Reconcile to the webhook, always.

Use a message broker between services

In a microservices setup, the payment service shouldn't synchronously call the order service and the notification service. Emit an event:

ts
this.events.emit("payment.succeeded", {
  orderId: order.id,
  amount: order.total,
});

Orders, notifications and analytics each subscribe. Services stay decoupled, and a slow email provider can't block a checkout. NestJS makes this clean with its built-in microservice transport (Kafka, RabbitMQ, Redis).

Log money like an accountant

Every state transition — intent created, captured, refunded, failed — gets a durable, append-only record. When a customer disputes a charge six weeks later, you want a timeline, not a guess.

Reliable payments aren't about clever code. They're about assuming everything will be retried, every message will arrive twice, and the network will fail at the worst moment — and designing so none of that costs anyone money.

Ajinthan Thavendrarajah

Associate Tech Lead in Colombo, Sri Lanka, building scalable web & mobile products with .NET, NestJS, Next.js and React Native. Work with me →

$ ./start-project.sh

Have a product to build or a system to scale? Let's talk.

I usually reply within a day. Tell me what you're building, where it's stuck, and when you need it.