Patterns Are Solutions, Not Rules
Design patterns are not academic exercises — they are battle-tested solutions to problems that show up repeatedly in software development. In financial systems, certain patterns appear over and over because the domain has specific characteristics: multiple data sources, complex business rules, high reliability requirements, and the need for flexibility as markets and regulations evolve.
You do not need to memorise every pattern in the Gang of Four book. Focus on the ones that solve problems you actually encounter.
Strategy Pattern: Swappable Algorithms
The Strategy pattern lets you define a family of algorithms and switch between them at runtime. In finance, this appears everywhere — different pricing models, risk calculation methods, order routing strategies.
from abc import ABC, abstractmethod class PricingStrategy(ABC): @abstractmethod def price(self, market_data: dict) -> float: pass class BlackScholesPricing(PricingStrategy): def price(self, market_data: dict) -> float: spot = market_data["spot"] strike = market_data["strike"] vol = market_data["vol"] # ... Black-Scholes formula ... return calculated_price class MonteCarloPricing(PricingStrategy): def __init__(self, n_simulations: int = 10_000): self.n_simulations = n_simulations def price(self, market_data: dict) -> float: # ... Monte Carlo simulation ... return simulated_price class BinomialTreePricing(PricingStrategy): def __init__(self, steps: int = 100): self.steps = steps def price(self, market_data: dict) -> float: # ... Binomial tree pricing ... return tree_price # The pricing engine does not care which model it uses class PricingEngine: def __init__(self, strategy: PricingStrategy): self.strategy = strategy def calculate_price(self, market_data: dict) -> float: return self.strategy.price(market_data) # Easy to swap strategies engine = PricingEngine(BlackScholesPricing()) price_bs = engine.calculate_price(data) engine = PricingEngine(MonteCarloPricing(n_simulations=50_000)) price_mc = engine.calculate_price(data)
The beauty is that adding a new pricing model requires zero changes to the PricingEngine — you just create a new class implementing PricingStrategy. This is the Open/Closed Principle in action.
Observer Pattern: React to Events
Financial systems are fundamentally event-driven. Prices update, orders fill, positions change, risk limits are breached. The Observer pattern decouples the thing that produces events from the things that react to them.
class MarketDataFeed: def __init__(self): self._subscribers = [] def subscribe(self, callback): self._subscribers.append(callback) def on_price_update(self, symbol: str, price: float): for subscriber in self._subscribers: subscriber(symbol, price) # Different components react to the same data feed = MarketDataFeed() feed.subscribe(lambda sym, px: update_portfolio_value(sym, px)) feed.subscribe(lambda sym, px: check_risk_limits(sym, px)) feed.subscribe(lambda sym, px: update_dashboard(sym, px)) feed.subscribe(lambda sym, px: log_price(sym, px))
When a price update arrives, all subscribers are notified automatically. The market data feed does not know or care what its subscribers do with the data. This loose coupling makes the system much easier to extend and maintain.
Factory Pattern: Create Objects Without Hardcoding
When the type of object you need depends on runtime data — different order types, different instrument classes, different report formats — the Factory pattern centralises creation logic:
class OrderFactory: @staticmethod def create(order_type: str, **kwargs): if order_type == "MARKET": return MarketOrder(**kwargs) elif order_type == "LIMIT": return LimitOrder(**kwargs) elif order_type == "STOP": return StopOrder(**kwargs) elif order_type == "ICEBERG": return IcebergOrder(**kwargs) else: raise ValueError(f"Unknown order type: {order_type}") # Parsing orders from an API or message queue order_data = {"type": "LIMIT", "symbol": "AAPL", "qty": 100, "price": 150.0} order = OrderFactory.create(order_data["type"], **order_data)
This is cleaner than scattering if/elif chains throughout your codebase, and it gives you a single place to modify when you add a new order type.
Decorator Pattern: Add Behaviour Dynamically
Not to be confused with Python's @decorator syntax (though they are related concepts). The Decorator pattern wraps an object to add functionality without modifying the original:
class BasicOrderValidator: def validate(self, order): if order.quantity <= 0: raise ValueError("Quantity must be positive") if order.price < 0: raise ValueError("Price cannot be negative") return True class RiskCheckDecorator: def __init__(self, validator): self.validator = validator def validate(self, order): self.validator.validate(order) if order.notional > 1_000_000: raise ValueError("Order exceeds single-order risk limit") return True class ComplianceDecorator: def __init__(self, validator): self.validator = validator def validate(self, order): self.validator.validate(order) if order.symbol in restricted_list: raise ValueError(f"{order.symbol} is on the restricted list") return True # Stack validations as needed validator = BasicOrderValidator() validator = RiskCheckDecorator(validator) validator = ComplianceDecorator(validator) validator.validate(order) # Runs all three checks
This pattern is especially powerful in finance where validation rules differ by product type, client, and jurisdiction. You can compose exactly the right set of checks for each context.
For more on Python decorators and the functional programming approaches that complement these patterns, see our deeper dives on those topics.
Singleton: Global State (Use Sparingly)
The Singleton pattern ensures a class has only one instance. In finance, this appears in database connection pools, configuration managers, and logging:
class DatabasePool: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.pool = create_connection_pool() return cls._instance # Both references point to the same pool pool1 = DatabasePool() pool2 = DatabasePool() assert pool1 is pool2
Use this sparingly — Singletons make testing harder because they introduce hidden global state. Often, explicit dependency injection is a better approach.
Choosing the Right Pattern
Do not start with a pattern and look for a place to use it. Start with a problem and recognise when a pattern fits. If you find yourself writing the same structural code repeatedly, that is often a signal that a pattern would help.
The patterns above are not the only ones that matter, but they are the ones that appear most frequently in real financial codebases. Understanding them gives you a vocabulary for discussing software architecture and a toolkit for building systems that remain maintainable as they grow.
Want to go deeper on Design Patterns for Financial Software?
This article covers the essentials, but there's a lot more to learn. Inside Quantt, you'll find hands-on coding exercises, interactive quizzes, and structured lessons that take you from fundamentals to production-ready skills — across 50+ courses in technology, finance, and mathematics.
Free to get started · No credit card required