2Brute2Best
πŸ”₯0
A1

Support UPI, Card & Wallet Payments

High-frequency interview topic Pro OnlyStrategyOpen-Closed Principle (OCP) concepts (C++20) std::print (C++23)
⚑ 60-Second Architecture SummaryA1 · Strategy + Open-Closed Principle (OCP)
Naive Bottleneck→Strategy + Open-Closed Principle (OCP) Cure
The Naive Bottleneck: Every new payment method edits Checkout

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).

The Pattern Cure: 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).

Sprint 1 of 9 Β· Checkout Payment Engine
Story A β€” Food Delivery System β€” Progressive System Build
1. Where our codebase left off

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.

2. What we are building now

We need Checkout to charge the customer's chosen payment method (UPI, Credit Card, or In-App Wallet) and return a structured PaymentReceipt.

3. The pain point we hit

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).

4. What this unlocks next
Next: A2 β†’

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.

01

The product ticket

Ticket Β· A1FOOD-101 Β· Support UPI, Credit Card, and In-App Wallet at Checkout

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).

Requirements
  • β€’

    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.

The next requirement that breaks naive code

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.

02

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.

Naive design (what not to ship) Β· naive.cpp
#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");
}
Every new payment method edits Checkout

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).

Payment-specific state leaks into one signature

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.

Merge conflicts and fragile tests

Three engineers working on UPI, Card, and Wallet edit the exact same function. A typo in the Card branch can break UPI checkout.

03

Step 2 β€” The fix: Strategy + Open-Closed Principle (OCP)

Core idea in one sentence

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).

Pattern roles in our story
Strategy (Interface)

PaymentStrategy β€” declares virtual PaymentReceipt pay(int amountRupees) = 0; and a virtual ~PaymentStrategy() = default;.

Concrete Strategies

UpiPayment, CardPayment, WalletPayment β€” each stores its own credentials/state and implements pay(...).

Context (Caller)

Checkout β€” owns std::unique_ptr<PaymentStrategy> strategy_ and calls strategy_->pay(amountRupees) without knowing which concrete class is inside.

Brute2Best Pro Content

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.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers