Implementing a Payment Gateway in Microservices and Monolithic Architectures: A Step-by-Step Guide
Introduction
Imagine you’re building the next big e-commerce platform. Customers are adding items to their cart, clicking “checkout,” but uh-oh — how do you actually process payments?
Payment gateways come to the rescue. In this guide, we’re going to learn everything from what a payment gateway is, how it works, and how to implement it in both microservices and monolithic architectures. Plus, we’ll cover concurrency issues, error handling, and retry mechanisms — all the good stuff that real developers need to know!
So, buckle up, because we’re about to dive into payment land. No more pretending; we’re getting real!
What is a Payment Gateway?
To keep things simple, a payment gateway is like the cashier at a store — responsible for securely processing payments from customers.
Here’s how a payment works at a high level:
- Customer inputs payment details (card, wallet, etc.).
- The payment gateway takes the data, encrypts it (security!).
- Sends the data to a payment processor or acquirer (like Visa, MasterCard, PayPal, etc.).
- The acquirer communicates with the customer’s bank to authorize or decline the payment.
- The result is sent back to the payment gateway, which tells your app: Success or Failure.
- If successful, your app processes the order.
Think of the payment gateway as the secure middleman who safely hands over the payment details to the processor and gets a result back.
Payment Gateway Types
- Hosted Payment Gateways: These take the user away from your site to process the payment (e.g., PayPal, GPay, Paytm). Easy to integrate and maintain.
- Direct or API-Based Gateways: Payments are processed on your site, and you handle the security. More control, but you’ll need to handle PCI compliance (security standards for card payments).
Payment Gateway Integration: Microservices vs Monolithic
Monolithic Architecture
In a monolithic system, everything (order management, payment processing, inventory) is part of a single application. For payment gateway integration, this typically means:
- Handling the payment logic in a single module.
- Payment data flows through the same request/response cycle as the rest of the app.
Example:
- User places an order in the monolithic app.
- The app sends a payment request to the payment gateway.
- Once confirmed, the app continues to process the order.
Microservices Architecture
In microservices, every function (payments, orders, users) is separated into different services. Here, the payment service is isolated from the rest of the system.
- Payment service: Independent service that processes payments.
- Communication: Other services (like order service) communicate with the payment service via HTTP requests or event-driven systems (Kafka, RabbitMQ).
Step-by-Step Payment Gateway Implementation
1. Sign Up and Get Your API Keys
You’ll need to sign up with a payment provider (e.g., GPay, Paytm, Stripe, Razorpay). After signing up, you’ll get:
- API Keys (public and private) — used to authenticate requests.
- Merchant ID — identifies you with the provider.
- Endpoint URLs — where you’ll send the payment request.
Example for Razorpay:
{
"merchant_id": "your_merchant_id",
"api_key": "your_public_key",
"api_secret": "your_private_key"
}
2. Add Dependencies to Your Project
Let’s assume you’re using Spring Boot for this implementation.
For Gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
For Maven:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
3. Creating the Payment Request
Now, let’s create the PaymentService to handle initiating a payment request. We’ll use WebClient to make a non-blocking HTTP call to the payment gateway.
Here’s the breakdown:
- WebClient: Handles the HTTP call asynchronously.
- PaymentRequest: Contains payment details.
- PaymentResponse: Contains the result.
@Service
public class PaymentService {
private final WebClient webClient; public PaymentService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.baseUrl("https://api.paymentprovider.com").build();
} public Mono<PaymentResponse> initiatePayment(PaymentRequest request) {
return webClient.post()
.uri("/payment")
.body(Mono.just(request), PaymentRequest.class)
.retrieve()
.bodyToMono(PaymentResponse.class)
.doOnError(error -> {
// Log the error
throw new PaymentFailedException("Payment failed: " + error.getMessage());
});
}
}
Explanation:
- WebClient: Calls the payment provider’s API.
- Mono.just(request): Creates a reactive stream containing the request data.
- .doOnError: Catches any errors, allowing us to handle failed payments.
4. Handling Callbacks (Webhooks)
Once the payment is processed, the payment provider sends a callback to your app. This is known as a webhook. You’ll create an endpoint to handle this.
@RestController
@RequestMapping("/payment")
public class PaymentController {
@PostMapping("/callback")
public ResponseEntity<String> handlePaymentCallback(@RequestBody PaymentResponse response) {
if (response.getStatus().equals("SUCCESS")) {
// Update order status, send confirmation to user
return new ResponseEntity<>("Payment Successful", HttpStatus.OK);
} else {
// Handle payment failure, log, etc.
return new ResponseEntity<>("Payment Failed", HttpStatus.BAD_REQUEST);
}
}
}
Explanation:
- The payment provider sends the result back to your app (usually with a POST request).
- You update the order status based on the response.
Common Problems and Their Solutions
Problem 1: Network Latency & Timeout
Your payment provider might take too long to respond, causing your app to freeze and leave users waiting.
Solution: Use timeouts and retries with Resilience4j or Hystrix.
@Retry(name = "paymentRetry", fallbackMethod = "fallbackPayment")
public Mono<PaymentResponse> initiatePayment(PaymentRequest request) {
return webClient.post()
.uri("/payment")
.body(Mono.just(request), PaymentRequest.class)
.retrieve()
.bodyToMono(PaymentResponse.class);
}
public Mono<PaymentResponse> fallbackPayment(PaymentRequest request, Throwable t) {
return Mono.just(new PaymentResponse("FAILED", "Retry attempts exceeded."));
}
Explanation:
- Retry: Automatically retries the payment request if it fails.
- Fallback: Provides a default response after max retries.
Problem 2: Concurrency Issues (Double Charging)
If two requests for the same order come in at the same time, you might charge the user twice.
Solution: Use pessimistic locking at the database level.
@Transactional
public void processPayment(Long orderId, PaymentRequest request) {
Order order = orderRepository.findByIdWithLock(orderId); // Lock the order row
if (order.isPaid()) {
throw new PaymentAlreadyProcessedException("Order is already paid.");
}
// Proceed with payment logic
}
Explanation:
- Locking: Ensures only one payment request can be processed for an order at a time.
Problem 3: Asynchronous Calls Freezing the App
Making external calls to payment providers can block your app, causing other requests to pile up.
Solution: Use CompletableFuture to handle the call asynchronously.
public CompletableFuture<String> processPaymentAsync(PaymentRequest request) {
return CompletableFuture.supplyAsync(() -> {
// External call to payment provider
return "Payment Processed Successfully";
}).exceptionally(ex -> {
return "Payment Failed: " + ex.getMessage();
});
}
Integrating Third-Party Payment Gateways (GPay, Paytm, etc.)
For integrating third-party payment providers like GPay or Paytm, the process is almost the same, but these providers handle a lot more behind the scenes, such as encrypting payment details and user authentication. You only have to worry about:
- Redirecting the user to their site for payment.
- Handling the callback (i.e., payment success or failure).
Example: GPay Integration
public String redirectToGPay(PaymentRequest request) {
String gpayUrl = "https://gpay.api/checkout?merchantId=" + request.getMerchantId();
return "redirect:" + gpayUrl;
}
Conclusion
By now, you should have a clear understanding of how to implement a payment gateway in both monolithic and microservices architectures. We covered everything from setting up your service, handling webhooks, dealing with errors and concurrency, to integrating popular third-party payment systems.
Whether you’re working with an e-commerce platform or any system that processes payments, the key takeaway here is to handle errors gracefully, ensure security, and implement proper retry and locking mechanisms to prevent issues like double-charging.
Now it’s your turn to get started! Ready to dive in? 💸
Let me know if you’d like any tweaks or more clarifications!