Bird
Raised Fist0
LLDsystem_design~7 mins

Payment strategy pattern in LLD - System Design Guide

Choose your learning style10 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Problem Statement
When a payment system supports multiple payment methods, hardcoding each method's logic in one place causes the code to become messy and difficult to maintain. Adding or changing payment methods requires modifying existing code, risking bugs and slowing development.
Solution
The payment strategy pattern solves this by defining a common interface for all payment methods and implementing each method as a separate class. The system selects the appropriate payment strategy at runtime, allowing easy addition or modification of payment methods without changing the core logic.
Architecture
PaymentClient
PaymentStrategy (I)
CreditCardPay
implements I

This diagram shows a PaymentClient using a PaymentStrategy interface to interact with different payment method implementations like CreditCardPay, PayPalPay, and BitcoinPay.

Trade-offs
✓ Pros
Enables adding new payment methods without changing existing code.
Improves code organization by separating payment logic into distinct classes.
Supports runtime selection of payment methods, increasing flexibility.
Facilitates testing each payment method independently.
✗ Cons
Increases the number of classes, which can complicate the codebase if overused.
Requires upfront design to define the common interface properly.
May introduce slight overhead in method calls due to abstraction.
Use when your system supports multiple payment methods and you expect to add or change them frequently, especially if you want to keep the codebase clean and maintainable.
Avoid when the system only supports one payment method or when payment logic is very simple and unlikely to change, as the pattern adds unnecessary complexity.
Real World Examples
Amazon
Amazon uses a strategy-like pattern to support multiple payment options such as credit cards, gift cards, and digital wallets, allowing seamless addition of new payment methods.
Uber
Uber applies the payment strategy pattern to switch between payment methods like credit cards, PayPal, and Uber credits dynamically during ride payments.
Shopify
Shopify uses this pattern to integrate various payment gateways, enabling merchants to choose and add payment options without changing core checkout logic.
Code Example
The before code uses if-else to handle payment methods, making it hard to add new methods. The after code defines a PaymentStrategy interface and separate classes for each payment method. PaymentProcessor uses a strategy instance to perform payment, enabling easy extension and cleaner code.
LLD
### Before: Without Strategy Pattern
class PaymentProcessor:
    def pay(self, method, amount):
        if method == 'credit_card':
            print(f"Processing credit card payment of ${amount}")
        elif method == 'paypal':
            print(f"Processing PayPal payment of ${amount}")
        else:
            print("Payment method not supported")

processor = PaymentProcessor()
processor.pay('credit_card', 100)


### After: With Strategy Pattern
from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCardPay(PaymentStrategy):
    def pay(self, amount):
        print(f"Processing credit card payment of ${amount}")

class PayPalPay(PaymentStrategy):
    def pay(self, amount):
        print(f"Processing PayPal payment of ${amount}")

class PaymentProcessor:
    def __init__(self, strategy: PaymentStrategy):
        self.strategy = strategy

    def pay(self, amount):
        self.strategy.pay(amount)

processor = PaymentProcessor(CreditCardPay())
processor.pay(100)

processor = PaymentProcessor(PayPalPay())
processor.pay(200)
OutputSuccess
Alternatives
Simple Conditional Logic
Uses if-else or switch statements to handle payment methods in one place instead of separate classes.
Use when: When the number of payment methods is very small and unlikely to grow.
Factory Pattern
Focuses on creating payment method objects, while strategy focuses on executing payment behavior.
Use when: When object creation logic is complex and needs central management.
Summary
The payment strategy pattern prevents messy code by separating payment methods into distinct classes.
It allows adding or changing payment methods without modifying existing code.
This pattern improves flexibility and maintainability in systems with multiple payment options.

Practice

(1/5)
1. What is the main benefit of using the Payment Strategy Pattern in a payment system?
easy
A. It allows switching between different payment methods without changing the main code.
B. It forces all payment methods to use the same currency.
C. It stores all payment data in a single database table.
D. It encrypts payment information automatically.

Solution

  1. Step 1: Understand the purpose of the Payment Strategy Pattern

    The pattern is designed to let the system switch payment methods easily without modifying the main logic.
  2. Step 2: Analyze the options

    Only It allows switching between different payment methods without changing the main code. describes this benefit correctly. Other options describe unrelated features.
  3. Final Answer:

    It allows switching between different payment methods without changing the main code. -> Option A
  4. Quick Check:

    Payment Strategy Pattern = Switch payment methods easily [OK]
Hint: Focus on flexibility to switch payment methods without code changes [OK]
Common Mistakes:
  • Confusing strategy pattern with data storage or encryption
  • Thinking it enforces currency or database rules
  • Assuming it handles security automatically
2. Which of the following is the correct way to define a payment strategy interface in a typical object-oriented language?
easy
A. interface PaymentStrategy { void pay(double amount); }
B. class PaymentStrategy { void pay(amount); }
C. function PaymentStrategy(amount) { return pay; }
D. var PaymentStrategy = pay => amount;

Solution

  1. Step 1: Identify the correct syntax for an interface

    In object-oriented languages, interfaces declare method signatures without implementation. interface PaymentStrategy { void pay(double amount); } uses 'interface' and a method signature correctly.
  2. Step 2: Check other options

    class PaymentStrategy { void pay(amount); } is a class, not an interface. Options C and D use function syntax, not interface definitions.
  3. Final Answer:

    interface PaymentStrategy { void pay(double amount); } -> Option A
  4. Quick Check:

    Interface syntax = interface PaymentStrategy { void pay(double amount); } [OK]
Hint: Look for 'interface' keyword and method signature format [OK]
Common Mistakes:
  • Using class instead of interface for strategy definition
  • Confusing function syntax with interface
  • Missing method parameter types
3. Given the following code snippet implementing the Payment Strategy Pattern, what will be the output?
class PaymentStrategy {
  pay(amount) { throw 'Not implemented'; }
}

class CreditCardPayment extends PaymentStrategy {
  pay(amount) { return `Paid ${amount} with Credit Card`; }
}

class PayPalPayment extends PaymentStrategy {
  pay(amount) { return `Paid ${amount} with PayPal`; }
}

class PaymentContext {
  constructor(strategy) { this.strategy = strategy; }
  executePayment(amount) { return this.strategy.pay(amount); }
}

const context = new PaymentContext(new PayPalPayment());
console.log(context.executePayment(100));
medium
A. Paid 100 with Credit Card
B. Not implemented
C. Paid 100 with PayPal
D. Error: strategy.pay is not a function

Solution

  1. Step 1: Trace the object creation and method calls

    The PaymentContext is created with a PayPalPayment strategy. Calling executePayment(100) calls PayPalPayment's pay method.
  2. Step 2: Understand the pay method output

    PayPalPayment's pay returns 'Paid 100 with PayPal'. This string is printed.
  3. Final Answer:

    Paid 100 with PayPal -> Option C
  4. Quick Check:

    Context uses PayPalPayment = Output with PayPal [OK]
Hint: Check which strategy instance is passed to context [OK]
Common Mistakes:
  • Assuming default or CreditCardPayment is used
  • Expecting an error from base class
  • Confusing method override behavior
4. Identify the error in the following Payment Strategy Pattern implementation:
class PaymentStrategy {
  pay(amount) { console.log('Paying ' + amount); }
}

class BitcoinPayment extends PaymentStrategy {
  pay() { console.log('Paying with Bitcoin'); }
}

const payment = new BitcoinPayment();
payment.pay(50);
medium
A. PaymentStrategy should not have a pay method implementation.
B. BitcoinPayment's pay method does not accept the amount parameter.
C. BitcoinPayment should not extend PaymentStrategy.
D. Calling pay with 50 causes a syntax error.

Solution

  1. Step 1: Compare method signatures in base and subclass

    PaymentStrategy's pay expects an amount parameter, but BitcoinPayment's pay method does not accept any parameters.
  2. Step 2: Understand the impact of signature mismatch

    Calling payment.pay(50) passes an argument, but BitcoinPayment's pay ignores it, causing unexpected behavior or errors.
  3. Final Answer:

    BitcoinPayment's pay method does not accept the amount parameter. -> Option B
  4. Quick Check:

    Method signature mismatch = BitcoinPayment's pay method does not accept the amount parameter. [OK]
Hint: Check if subclass methods match base method parameters [OK]
Common Mistakes:
  • Thinking base class should not implement pay
  • Assuming inheritance is wrong
  • Confusing runtime error with syntax error
5. You are designing a payment system that must support credit cards, PayPal, and a new cryptocurrency payment method. Using the Payment Strategy Pattern, which design approach best supports adding the new method with minimal changes?
hard
A. Use a global variable to switch payment methods inside the main payment function.
B. Modify the existing CreditCardPayment class to handle cryptocurrency payments.
C. Add cryptocurrency payment logic inside the PaymentContext class directly.
D. Create a new class implementing the PaymentStrategy interface for cryptocurrency and pass it to the payment context.

Solution

  1. Step 1: Understand the open/closed principle in design

    The system should be open for extension but closed for modification. Adding a new payment method should not require changing existing classes.
  2. Step 2: Evaluate each option

    Create a new class implementing the PaymentStrategy interface for cryptocurrency and pass it to the payment context. creates a new class implementing the interface, fitting the pattern and minimizing changes. Options B and C modify existing classes, violating the principle. Use a global variable to switch payment methods inside the main payment function. uses a global variable, which is poor design.
  3. Final Answer:

    Create a new class implementing the PaymentStrategy interface for cryptocurrency and pass it to the payment context. -> Option D
  4. Quick Check:

    New class for new method = Create a new class implementing the PaymentStrategy interface for cryptocurrency and pass it to the payment context. [OK]
Hint: Add new payment as new class, avoid changing existing code [OK]
Common Mistakes:
  • Modifying existing payment classes
  • Adding logic inside context class
  • Using global variables for strategy switching