In this second article of series on designing payment system, we shall look at some more aspects of system design. In the previous article we had seen how to structure low level design of payment system, and how shall the user interact with the payment system.

As a next step we want the user not to directly instantiate any of the payment types like CreditCardPayment, DebitCardPayment objects manually, instead user should just be conveying his need, and the payment module should itself build the appropriate object and pass on to the user.

Carrying on with the initial design, we can make use of the factory pattern in this case and complete the use case we discussed above. Our factory class would look somewhat like as follows:

class PaymentSystem{
public Payments getPayment(Enum paymentType){
Payments payment = null;
switch (paymentType) {
case CREDIT_CARD:
System.out.println("Processing Credit Card payment...");
payments = CreditCardPayment();
break;
case DEBIT_CARD:
System.out.println("Processing Debit Card payment...");
payments = DebitCardPayment();
break;
case PAYPAL:
System.out.println("Processing PayPal payment...");
payments = PaypalPayment();
break;
case BANK_TRANSFER:
System.out.println("Processing Bank Transfer...");
payments = BankTransferPayment();
break;
case CASH:
System.out.println("Processing Cash payment...");
payments = CashPayment();
break;
case UPI:
System.out.println("Processing UPI payment...");
payments = UPIPayment();
break;
default:
System.out.println("Unknown payment type!");
}
return payments;
}
}

Here is how the user code wanting to do a payment would look like:

class PaymentService {
Payments payments;
public PaymentService(Enum paymentType) {
payments = PaymentSystem.getPayment(paymentType);
}
public boolean pay(PaymentDetails detail) {
boolean wasPaymentSuccessful = payments.pay(detail);
//other piece of code
}
}

So, as we can see factory design pattern simplifies the overall design by taking on the responsibility of actually creating the payments object based on user choice. This allows user to focus on his part of the code rather than understanding the payment module library and building solution over it.

Next tweak to this design is that, suppose we also want to handle payment failures. Consider that user has already added his payment details for each of the payment type in some configuration page of the application and hence they are available to the payment module. How then can we design the system wherein the user initiates the payment with one payment type, and later if that payment type has failures automatically the next configured payment type pitches in, continuing in that order.

class CreditCardPayments {
public void pay() {
try {
// Code to pay via credit card
} catch (PaymentException ex) {
new DebitCardPayments().pay()
}
}
}

In this example, if CreditCardPayments pay() method throws an error due to payment issue, it is handled in the catch block and a new DebitCardPayments object is created and pay() method is called on it. Though programmatically this is quite possible, but design wise this is bad and violates the Single Responsibility principle out of our famous SOLID principles.

There are many ways to achieve this, but following two ways majorly come to the top of my mind:

1.  Creating a payment aggregator.

2.  Making use of Chain of responsibility design pattern.’

Initially we started off as single Payments interface. On similar lines we can define an aggregator class implementing from the Payments interface, which can initialize all payment types. In the pay() method of the aggregator class, we can handle failure of each of the payment type and accordingly instantiate the next payment type object to make the payment. User can then just instantiate an object of this class and call the pay() method and wait for the payment to be successful.

 class PaymentAggregator {
    List < Payments > paymentTypeList;

    public boolean pay(PaymentDetails details) {
        int i = 0;
        Payments paymentType = null;
        boolean isPaymentSuccessful = false;

        do {
            paymentType = paymentTypeList.get(i++);
            isPaymentSuccessful = paymentType.pay()
        } while (!isPaymentSuccessful && i < paymentTypeList.size());

        return isPaymentSuccessful;
    }
}

Here the aggregator class has a list of payment types and iterates over it inside the do-while loop until either any of the payment is successful or entire paymenttype list is exhausted.

This somewhat follows the Composite design pattern but not fit in completely but this handles all payment type failures and also ensure that payment is done by any one of the types. Some candidates compare this solution to a type of SAGA design pattern used in microservices for distributed transaction handling and it indeed quite resembles the same.

Now let’s focus on the second solution using Chain of Responsibility, which also is a very elegant solution to this problem. In this case we change our Payments interface slightly as follows:

interface Payments{
boolean pay(PaymentDetails detail);
boolean nextHandler(PaymentDetails detail);
}
class CreditCardPayments{
private Payments nextPaymentType;
public CreditCardPayments(){
//Initialization
}
public CreditCardPayments(Payments nextPaymentType){
this.nextPaymentType = nextPaymentType;
}
public boolean pay(PaymentDetails detail){
//Credit Card related payment code.
//isSucessful variable contains true if payment successful and vice versa.
if(!isSucessful) return nextHandler(PaymentDetails detail);
}
public boolean nextHandler(PaymentDetails detail){
return nextPaymentType.pay();
}
public void setNext(Payments next) {
this.nextPaymentType = next;
}
}

It is quite clear from the above piece of code about how we shall structure our code to cater to different payment types and payment failures. There will be a nextPaymentType that would be passed to each of the payment type classes in their constructors and then on any event of the failure of that type of payment, simply a call to nextHandler() method would be done. Inside of nextHandler() method we shall simply call the pay() method from the nextPaymentType object. Quite simple and elegant!!!

Regarding the factory class instance for this:

class PaymentSystem {
public Payments getPayment(PaymentType type) {
CreditCardPayments cc = new CreditCardPayments();
DebitCardPayments dc = new DebitCardPayments();
UPIPayments upi = new UPIPayments();
// Wire cyclic chain
cc.setNext(dc);
dc.setNext(upi);
upi.setNext(cc);
switch (type) {
case CREDIT_CARD: return cc;
case DEBIT_CARD: return dc;
case UPI: return upi;
default:
System.out.println("Unknown payment type!");
return null;
}
}
}

In this new avatar of factory class, it sets the next payment type of each of the payment type and passes the object back to the user.

As we have seen, there are countless interesting scenarios and creative tweaks we can explore with our payment module — from chaining handlers to experimenting with cyclic flows. The beauty of such design patterns is that they open doors to flexibility, scalability, and even playful experimentation in real-world systems. I’d love to hear your thoughts: which variation did you find most exciting, and what other ideas would you try? Drop your comments below and stay tuned — I’ll be sharing more fun tech explorations in upcoming blogs that dive into quirky, practical, and sometimes surprising ways to rethink everyday code.

Please find the code for this entry here On Github :