Home About Services Work Blog Contact
connect@ajinthan.com · Colombo, Sri Lanka
Backend & Architecture

Microservices with NestJS: Integrating Multi-Gateway Payments

Lessons from building a payments layer across Stripe and PayPal in a NestJS microservices architecture — idempotency, webhooks, and keeping money flows reliable.

Microservices with NestJS: Integrating Multi-Gateway Payments

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:

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:

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:

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.

NestJSMicroservicesPaymentsStripeArchitecture
AT
Ajinthan Thavendrarajah

Based in Colombo, Sri Lanka, building scalable web & mobile products with .NET, NestJS, Next.js and Flutter.

Keep reading

Related articles.