Support UPI, Card & Wallet Payments
When we stuff UPI, Card, and Wallet logic into a single `if / else if` chain inside `Checkout::pay()`, every new payment method (like Buy-Now-Pay-Later or NetBanking) forces us to open, edit, and re-test `Checkout` β violating the **Open-Closed Principle (OCP)**.
Adding Buy-Now-Pay-Later or NetBanking forces you to open Checkout::pay and add another else if branch β violating the Open-Closed Principle (OCP).
Put each interchangeable algorithm in its own class behind a shared interface, so the caller talks to the interface and never branches on concrete types.
Instead of Checkout knowing how UPI, Card, and Wallet work, we define a tiny PaymentStrategy interface with one pure virtual method: pay(int amountRupees). Each payment method (UpiPayment, CardPayment, WalletPayment) is its own class holding only its own fields (vpa_, cardNumber_, balanceRupees_). Checkout holds a std::unique_ptr<PaymentStrategy> and delegates to strategy_->pay(amount). Now Checkout is closed for modification (we never touch it again) and open for extension (a new PayLaterPayment class plugs right in).
Day 1 of building our Food Delivery platform (Zomato / Swiggy clone). We have a basic cart with food items and a total bill in rupees, and we are writing our very first backend module: the Checkout payment processor.
We need Checkout to charge the customer's chosen payment method (UPI, Credit Card, or In-App Wallet) and return a structured PaymentReceipt.
When we stuff UPI, Card, and Wallet logic into a single if / else if chain inside Checkout::pay(), every new payment method (like Buy-Now-Pay-Later or NetBanking) forces us to open, edit, and re-test Checkout β violating the Open-Closed Principle (OCP).
Our Checkout class can now charge any PaymentStrategy without knowing its concrete type. Next in Sprint 2 (A2 Β· SRP), we build the OrderService that takes a paid checkout and processes the order β where we'll hit a 500-line God-class problem.
The product ticket
Our food delivery app launched with UPI only. Now product wants Credit Cards (with a 2% gateway fee) and our In-App Wallet (which must check and deduct the user's wallet balance).
- β’
Support UPI (validate
@in VPA, 0% fee), Card (validate 16-digit card number, add 2% fee), and Wallet (check sufficient balance and deduct). - β’
Return a structured
PaymentReceipt(ok,totalChargedRupees,summary) so the order screen can show what happened. - β’
Let a customer switch their chosen payment method on the checkout screen before tapping Pay.
Next sprint, the payments team is adding Buy-Now-Pay-Later (Simpl / LazyPay) and NetBanking. They want to ship new payment methods without editing or re-testing the core Checkout class.
Step 1 β The naive code & why it breaks
The first instinct is to pass a type string ("UPI", "CARD", "WALLET") into Checkout::pay and branch with an if / else if chain.
#include <print>
#include <string>
// Naive Checkout: every payment method is a branch inside one growing if/else chain.
class NaiveCheckout {
public:
bool pay(const std::string& type, int amountRupees, const std::string& accountId) {
if (type == "UPI") {
if (accountId.find('@') == std::string::npos) return false;
std::println("[UPI] Charged Rs.{} from VPA {}", amountRupees, accountId);
return true;
} else if (type == "CARD") {
if (accountId.size() != 16) return false;
int fee = (amountRupees * 2) / 100; // 2% MDR fee
std::println("[CARD] Charged Rs.{} (+Rs.{} fee) on card *{}",
amountRupees, fee, accountId.substr(12));
return true;
} else if (type == "WALLET") {
// Hardcoded wallet balance check mixed right into Checkout!
int walletBalance = 500;
if (walletBalance < amountRupees) return false;
std::println("[WALLET] Deducted Rs.{} from wallet {}", amountRupees, accountId);
return true;
}
// Adding "BNPL" or "NETBANKING" forces us to edit this file again.
return false;
}
};
int main() {
NaiveCheckout checkout;
checkout.pay("UPI", 340, "naman@okaxis");
checkout.pay("CARD", 850, "4111222233334444");
checkout.pay("WALLET", 220, "wal_99");
}Adding Buy-Now-Pay-Later or NetBanking forces you to open Checkout::pay and add another else if branch β violating the Open-Closed Principle (OCP).
UPI needs a VPA (naman@okaxis), Card needs a 16-digit PAN + expiry, and Wallet needs a walletBalance. Stuffing all of them into pay(type, amount, accountId) forces awkward string hacks or dummy parameters.
Three engineers working on UPI, Card, and Wallet edit the exact same function. A typo in the Card branch can break UPI checkout.
Step 2 β The fix: Strategy + Open-Closed Principle (OCP)
Put each interchangeable algorithm in its own class behind a shared interface, so the caller talks to the interface and never branches on concrete types.
Instead of Checkout knowing how UPI, Card, and Wallet work, we define a tiny PaymentStrategy interface with one pure virtual method: pay(int amountRupees). Each payment method (UpiPayment, CardPayment, WalletPayment) is its own class holding only its own fields (vpa_, cardNumber_, balanceRupees_). Checkout holds a std::unique_ptr<PaymentStrategy> and delegates to strategy_->pay(amount). Now Checkout is closed for modification (we never touch it again) and open for extension (a new PayLaterPayment class plugs right in).
PaymentStrategy β declares virtual PaymentReceipt pay(int amountRupees) = 0; and a virtual ~PaymentStrategy() = default;.
UpiPayment, CardPayment, WalletPayment β each stores its own credentials/state and implements pay(...).
Checkout β owns std::unique_ptr<PaymentStrategy> strategy_ and calls strategy_->pay(amountRupees) without knowing which concrete class is inside.
Unlock Full UML Diagram & C++23 Solution
The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.