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:
-
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.
-
Switch Statements (if-elif chains): The pricing type dispatch, category adjustments, loyalty discounts, promo codes, and tax calculations are all separate
if-elifchains jammed together. Each is an independent concern tangled into one function. -
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.
-
Primitive Obsession: Products, customers, and contexts are all untyped dictionaries. Misspelling a key (e.g.,
"loyaltiy_tier") produces a silent bug—dict.get()returnsNoneand the discount is simply skipped. -
Violation of Open/Closed Principle: Adding subscription pricing requires modifying this function—adding another
elifbranch. Every developer on the team must understand the entire function to make a change safely. -
Hidden import: The
import mathinside 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
-
Open/Closed Principle compliance: New pricing types are added by creating new classes and registering them. No existing code is modified.
-
Testability: Each strategy can be tested independently with focused unit tests. The tax calculator is tested separately. The engine is tested with mock strategies.
-
Readability: Each class fits on a single screen. A developer can understand
VolumePricingwithout reading any other pricing code. -
Team scalability: Two developers can work on different pricing strategies simultaneously without merge conflicts.
What It Cost
-
More files and classes: The refactored version has more structural elements. For a two-person startup, this might feel like overhead.
-
Indirection: Following the code path requires understanding the strategy dispatch mechanism. However, this indirection is well-established and recognizable to experienced developers.
-
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.