Case Study 01: Refactoring Spaghetti to Strategy Pattern

Overview

Context: A startup building an e-commerce platform used an AI coding assistant to generate their pricing engine. The initial version worked for their launch, handling three pricing models: standard markup, volume discounts, and promotional pricing. Six months later, the team needs to add subscription pricing, dynamic pricing based on demand, and regional pricing. Every addition requires modifying a single, sprawling function that has grown to over 200 lines.

Challenge: Take the messy, AI-generated pricing function and refactor it into a clean, extensible system using the Strategy pattern.

Patterns Applied: Strategy, Factory (for strategy selection), and elements of the Template Method.


The Original Code

The AI assistant generated the following function during an early sprint. It worked at launch and passed all tests. But it has become a maintenance nightmare.

def calculate_price(product, customer, context):
    base_price = product["base_price"]
    quantity = context.get("quantity", 1)

    # Standard pricing
    if context.get("pricing_type") == "standard":
        markup = product.get("markup_pct", 0.3)
        price = base_price * (1 + markup)

        # Apply category adjustments
        if product["category"] == "electronics":
            price *= 1.05  # Electronics surcharge
        elif product["category"] == "clothing":
            if context.get("season") == "clearance":
                price *= 0.7
            else:
                price *= 0.95
        elif product["category"] == "food":
            days_to_expiry = product.get("days_to_expiry", 30)
            if days_to_expiry < 7:
                price *= 0.5
            elif days_to_expiry < 14:
                price *= 0.75

        # Loyalty discount
        if customer.get("loyalty_tier") == "gold":
            price *= 0.95
        elif customer.get("loyalty_tier") == "platinum":
            price *= 0.90

        total = price * quantity

    # Volume pricing
    elif context.get("pricing_type") == "volume":
        if quantity >= 1000:
            discount = 0.25
        elif quantity >= 500:
            discount = 0.20
        elif quantity >= 100:
            discount = 0.15
        elif quantity >= 50:
            discount = 0.10
        elif quantity >= 10:
            discount = 0.05
        else:
            discount = 0

        price = base_price * (1 - discount)

        # Loyalty on top of volume
        if customer.get("loyalty_tier") == "gold":
            price *= 0.97
        elif customer.get("loyalty_tier") == "platinum":
            price *= 0.95

        total = price * quantity

    # Promotional pricing
    elif context.get("pricing_type") == "promo":
        promo_code = context.get("promo_code", "")

        if promo_code == "SUMMER20":
            price = base_price * 0.80
        elif promo_code == "WELCOME10":
            price = base_price * 0.90
        elif promo_code == "FLASH50":
            price = base_price * 0.50
        elif promo_code == "BOGO":
            price = base_price
            # Buy one get one: charge for ceil(quantity / 2)
            import math
            quantity = math.ceil(quantity / 2)
        else:
            price = base_price  # Unknown promo, no discount

        total = price * quantity

        # Minimum price floor
        if total < 0.99:
            total = 0.99

    else:
        # Default to standard
        price = base_price * 1.3
        total = price * quantity

    # Tax calculation (duplicated logic)
    tax_rate = 0.0
    if context.get("region") == "US":
        if context.get("state") in ("CA", "NY", "TX"):
            tax_rate = 0.08
        else:
            tax_rate = 0.06
    elif context.get("region") == "EU":
        tax_rate = 0.20
    elif context.get("region") == "UK":
        tax_rate = 0.20

    total_with_tax = total * (1 + tax_rate)

    # Rounding
    total_with_tax = round(total_with_tax, 2)

    return {
        "base_price": base_price,
        "unit_price": round(price, 2),
        "quantity": quantity,
        "subtotal": round(total, 2),
        "tax_rate": tax_rate,
        "tax_amount": round(total * tax_rate, 2),
        "total": total_with_tax,
        "pricing_type": context.get("pricing_type", "standard"),
    }

Identifying the Problems

Before refactoring, let us catalog the code smells:

  1. Long Method: The function is a monolithic block with deeply nested conditionals spanning well over 80 lines. Reading it requires holding the entire structure in memory.

  2. Switch Statements (if-elif chains): The pricing type dispatch, category adjustments, loyalty discounts, promo codes, and tax calculations are all separate if-elif chains jammed together. Each is an independent concern tangled into one function.

  3. Duplicated Logic: Loyalty discount calculation appears in both "standard" and "volume" branches with slightly different values. Tax calculation at the bottom applies to all pricing types but could vary by type in the future.

  4. Primitive Obsession: Products, customers, and contexts are all untyped dictionaries. Misspelling a key (e.g., "loyaltiy_tier") produces a silent bug—dict.get() returns None and the discount is simply skipped.

  5. Violation of Open/Closed Principle: Adding subscription pricing requires modifying this function—adding another elif branch. Every developer on the team must understand the entire function to make a change safely.

  6. Hidden import: The import math inside the promo branch is a code smell indicating the function has grown incrementally without reorganization.


Step 1: Define the Domain Model

First, we replace the untyped dictionaries with dataclasses. This gives us type safety, IDE support, and self-documenting code.

from dataclasses import dataclass
from typing import Protocol

@dataclass(frozen=True)
class Product:
    id: str
    name: str
    base_price: float
    category: str
    markup_pct: float = 0.3
    days_to_expiry: int | None = None

@dataclass(frozen=True)
class Customer:
    id: str
    name: str
    loyalty_tier: str = "standard"  # standard, gold, platinum

@dataclass(frozen=True)
class PricingContext:
    quantity: int = 1
    region: str = "US"
    state: str = ""
    season: str = ""
    promo_code: str = ""

@dataclass(frozen=True)
class PriceResult:
    base_price: float
    unit_price: float
    quantity: int
    subtotal: float
    tax_rate: float
    tax_amount: float
    total: float
    pricing_type: str

Step 2: Extract the Strategy Interface

Each pricing model becomes a strategy. We define the interface using Protocol:

class PricingStrategy(Protocol):
    """Interface for all pricing strategies."""

    @property
    def name(self) -> str: ...

    def calculate_unit_price(
        self, product: Product, customer: Customer, context: PricingContext
    ) -> float: ...

The strategy is responsible only for calculating the unit price. Tax calculation, rounding, and result assembly are handled by the context (the PricingEngine).


Step 3: Implement Concrete Strategies

Each pricing model is extracted into its own class:

@dataclass
class StandardPricing:
    """Applies standard markup with category and loyalty adjustments."""
    name: str = "standard"

    _category_adjustments: dict = None

    def __post_init__(self):
        self._category_adjustments = {
            "electronics": 1.05,
            "clothing": 0.95,
            "food": 1.0,
        }

    def calculate_unit_price(
        self, product: Product, customer: Customer, context: PricingContext
    ) -> float:
        price = product.base_price * (1 + product.markup_pct)
        price *= self._get_category_factor(product, context)
        price *= self._get_loyalty_factor(customer)
        return price

    def _get_category_factor(self, product: Product, context: PricingContext) -> float:
        if product.category == "clothing" and context.season == "clearance":
            return 0.7
        if product.category == "food" and product.days_to_expiry is not None:
            if product.days_to_expiry < 7:
                return 0.5
            elif product.days_to_expiry < 14:
                return 0.75
        return self._category_adjustments.get(product.category, 1.0)

    def _get_loyalty_factor(self, customer: Customer) -> float:
        factors = {"gold": 0.95, "platinum": 0.90}
        return factors.get(customer.loyalty_tier, 1.0)


@dataclass
class VolumePricing:
    """Applies quantity-based volume discounts."""
    name: str = "volume"

    _tiers: list[tuple[int, float]] = None

    def __post_init__(self):
        # (minimum_quantity, discount_rate) — must be sorted descending
        self._tiers = [
            (1000, 0.25),
            (500, 0.20),
            (100, 0.15),
            (50, 0.10),
            (10, 0.05),
        ]

    def calculate_unit_price(
        self, product: Product, customer: Customer, context: PricingContext
    ) -> float:
        discount = self._get_volume_discount(context.quantity)
        price = product.base_price * (1 - discount)
        price *= self._get_loyalty_factor(customer)
        return price

    def _get_volume_discount(self, quantity: int) -> float:
        for min_qty, discount in self._tiers:
            if quantity >= min_qty:
                return discount
        return 0.0

    def _get_loyalty_factor(self, customer: Customer) -> float:
        factors = {"gold": 0.97, "platinum": 0.95}
        return factors.get(customer.loyalty_tier, 1.0)


@dataclass
class PromotionalPricing:
    """Applies promotional code discounts."""
    name: str = "promo"

    _promo_discounts: dict = None

    def __post_init__(self):
        self._promo_discounts = {
            "SUMMER20": 0.80,
            "WELCOME10": 0.90,
            "FLASH50": 0.50,
        }

    def calculate_unit_price(
        self, product: Product, customer: Customer, context: PricingContext
    ) -> float:
        factor = self._promo_discounts.get(context.promo_code, 1.0)
        return max(product.base_price * factor, 0.99)

Notice that each class is small, focused, and testable in isolation. The volume discount tiers are data, not hardcoded branches. New promo codes can be added to the dictionary without modifying logic.


Step 4: Build the Pricing Engine (Context)

The PricingEngine is the context in the Strategy pattern. It handles the common logic—tax calculation, rounding, result assembly—and delegates the pricing-specific calculation to the current strategy.

class PricingEngine:
    """Coordinates pricing calculation using interchangeable strategies."""

    def __init__(self):
        self._strategies: dict[str, PricingStrategy] = {}
        self._tax_calculator = TaxCalculator()

    def register_strategy(self, strategy: PricingStrategy) -> None:
        self._strategies[strategy.name] = strategy

    def calculate(
        self,
        pricing_type: str,
        product: Product,
        customer: Customer,
        context: PricingContext,
    ) -> PriceResult:
        strategy = self._strategies.get(pricing_type)
        if strategy is None:
            raise ValueError(
                f"Unknown pricing type: {pricing_type}. "
                f"Available: {list(self._strategies.keys())}"
            )

        unit_price = strategy.calculate_unit_price(product, customer, context)
        subtotal = round(unit_price * context.quantity, 2)
        tax_rate = self._tax_calculator.get_rate(context.region, context.state)
        tax_amount = round(subtotal * tax_rate, 2)

        return PriceResult(
            base_price=product.base_price,
            unit_price=round(unit_price, 2),
            quantity=context.quantity,
            subtotal=subtotal,
            tax_rate=tax_rate,
            tax_amount=tax_amount,
            total=round(subtotal + tax_amount, 2),
            pricing_type=pricing_type,
        )


@dataclass
class TaxCalculator:
    """Extracted tax calculation logic."""

    _us_state_rates: dict = None
    _default_us_rate: float = 0.06
    _region_rates: dict = None

    def __post_init__(self):
        self._us_state_rates = {"CA": 0.08, "NY": 0.08, "TX": 0.08}
        self._region_rates = {"EU": 0.20, "UK": 0.20}

    def get_rate(self, region: str, state: str = "") -> float:
        if region == "US":
            return self._us_state_rates.get(state, self._default_us_rate)
        return self._region_rates.get(region, 0.0)

Step 5: Wire It Together

The setup code registers strategies with the engine:

def create_pricing_engine() -> PricingEngine:
    engine = PricingEngine()
    engine.register_strategy(StandardPricing())
    engine.register_strategy(VolumePricing())
    engine.register_strategy(PromotionalPricing())
    return engine

Adding the new subscription pricing model is now a single, isolated addition:

@dataclass
class SubscriptionPricing:
    """Applies recurring subscription discounts."""
    name: str = "subscription"

    _term_discounts: dict = None

    def __post_init__(self):
        self._term_discounts = {
            "monthly": 0.0,
            "quarterly": 0.05,
            "annual": 0.15,
        }

    def calculate_unit_price(
        self, product: Product, customer: Customer, context: PricingContext
    ) -> float:
        term = getattr(context, "subscription_term", "monthly")
        discount = self._term_discounts.get(term, 0.0)
        return product.base_price * (1 - discount)

# Registration — no existing code modified
engine.register_strategy(SubscriptionPricing())

Results and Analysis

Before vs. After Comparison

Metric Before After
Lines in main function 95+ 15 (engine.calculate)
Number of if-elif branches 15+ 0 in engine; small lookups in strategies
Files to modify for new pricing type 1 (the monolithic function) 0 existing files; 1 new strategy class
Testable units 1 (the whole function) 5+ (each strategy, tax calculator, engine)
Type safety None (raw dicts) Full (dataclasses with type hints)

What We Gained

  1. Open/Closed Principle compliance: New pricing types are added by creating new classes and registering them. No existing code is modified.

  2. Testability: Each strategy can be tested independently with focused unit tests. The tax calculator is tested separately. The engine is tested with mock strategies.

  3. Readability: Each class fits on a single screen. A developer can understand VolumePricing without reading any other pricing code.

  4. Team scalability: Two developers can work on different pricing strategies simultaneously without merge conflicts.

What It Cost

  1. More files and classes: The refactored version has more structural elements. For a two-person startup, this might feel like overhead.

  2. Indirection: Following the code path requires understanding the strategy dispatch mechanism. However, this indirection is well-established and recognizable to experienced developers.

  3. Time: The refactoring took approximately four hours, including writing tests for the new structure.

The Verdict

The refactoring was justified because the team had a concrete, immediate need to add new pricing types. The original code was already painful to modify. If the pricing logic had been stable and unlikely to change, the refactoring would have been premature.

This case study demonstrates the core lesson of Chapter 25: patterns are tools for managing complexity that has already materialized, not shields against complexity that might never arrive.