You start simple.

A small feature. A couple of conditions.

Then it grows.

And suddenly, your code looks like this:

def process_payment(method, amount):
    if method == "credit_card":
        return f"Processing {amount} via Credit Card"

    elif method == "paypal":
        return f"Processing {amount} via PayPal"

    elif method == "upi":
        return f"Processing {amount} via UPI"

    elif method == "crypto":
        return f"Processing {amount} via Crypto"

    else:
        raise ValueError("Unsupported payment method")

Looks fine… until it doesn’t.


The Problem

At first glance, this works.

But the real issue isn’t the if-else.

It’s this:

Every new behavior forces you to modify existing logic.

And that has consequences:

  • Every new payment method → changes a critical function
  • Higher risk of breaking existing flows
  • More regression testing
  • Tighter coupling between teams
  • Slower feature delivery

This isn’t just a code smell.

It’s an architecture problem.


Why This Breaks in Real Systems

Now imagine:

  • You’re building a FastAPI service
  • Multiple teams add payment providers
  • Each provider has different validation, retries, logging

Now this one function becomes:

  • a hotspot for changes
  • a merge conflict zone
  • a source of production bugs

The problem is no longer syntax.

It’s change amplification.


What We Actually Want

We don’t want:

“Pick a method and run logic inside one function”

We want:

“Pick a behavior and delegate execution to it”

Even more precisely:

Separate what to do from how it’s done

That’s the Strategy Pattern.


The Idea

Instead of writing all logic in one place:

  • You create separate “strategies”
  • Each handles one behavior
  • You select the strategy at runtime

At a deeper level:

We’re programming to a contract, not a concrete implementation.


Refactoring with Strategy Pattern

Step 1: Define a common contract

from abc import ABC, abstractmethod

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

Step 2: Create concrete strategies

class CreditCardPayment(PaymentStrategy):
    def pay(self, amount):
        return f"Processing {amount} via Credit Card"


class PayPalPayment(PaymentStrategy):
    def pay(self, amount):
        return f"Processing {amount} via PayPal"


class UPIPayment(PaymentStrategy):
    def pay(self, amount):
        return f"Processing {amount} via UPI"

Step 3: Execute without conditionals

def process_payment(strategy: PaymentStrategy, amount):
    return strategy.pay(amount)

Selecting the Right Strategy (Real Systems)

STRATEGY_MAP = {
    "credit_card": CreditCardPayment(),
    "paypal": PayPalPayment(),
    "upi": UPIPayment(),
}

def get_strategy(method: str) -> PaymentStrategy:
    try:
        return STRATEGY_MAP[method]
    except KeyError:
        raise ValueError("Unsupported payment method")

Real-World Note: Strategies Aren’t Always Stateless

class StripePayment(PaymentStrategy):
    def __init__(self, client):
        self.client = client

    def pay(self, amount):
        return self.client.charge(amount)

FastAPI Example

from fastapi import FastAPI

app = FastAPI()

@app.post("/pay")
def pay(method: str, amount: int):
    strategy = get_strategy(method)
    return process_payment(strategy, amount)

Mental Model

Separate what to do from how it’s done, and make behavior replaceable at runtime.