SOLID principles for a trading system
SOLID can feel like abstract acronym soup until a system grows complex enough to punish you for ignoring it. A war story applying the five principles to an algorithmic trading system — where each one earned its keep, and where dogmatic application would have hurt.
SOLID — the five object-oriented design principles — can feel like abstract acronym soup when you read about it. It comes alive when you build something complex enough that ignoring it hurts. A trading system is exactly that: many strategies, multiple exchanges, market data, order execution, risk checks — interacting parts that change at different rates, with real money on the outcome. This is a war story about applying SOLID to one, where each principle genuinely earned its place, and where applying it dogmatically would have been a mistake.
Single Responsibility: separate the concerns that change separately
The Single Responsibility Principle says a class should have one reason to change. In a trading
system the temptation is a TradingBot god-class that fetches market data, runs the strategy,
checks risk, places orders, and records results. It works briefly and then becomes unmaintainable,
because all those concerns change for different reasons — the strategy changes when you have a new
idea, execution changes when an exchange API changes, risk changes when your risk appetite does.
So you split them: a MarketDataFeed, a Strategy, a RiskCheck, an OrderExecutor, a
TradeRecorder. Each has one job and one reason to change. The payoff is concrete: when an
exchange changes its API you touch only the executor; when you tweak a strategy you touch only the
strategy; and each piece is testable in isolation (you can test a strategy with fake data and no
exchange). SRP here is not abstract tidiness — it is what lets you change one aspect of a
money-handling system without fear of breaking the others.
Open/Closed: add strategies without touching the engine
The Open/Closed Principle — open for extension, closed for modification — is the one that paid off most. A trading system’s whole point is that you keep adding and changing strategies. If adding a strategy means editing the core engine, every new idea risks breaking the working ones. Instead, the engine depends on a strategy interface and you add new strategies as new classes:
class Strategy # the contract every strategy implements
def signal(market) = raise NotImplementedError # => :buy / :sell / :hold
end
class MeanReversion < Strategy
def signal(market) = market.price < market.moving_average ? :buy : :sell
end
class Momentum < Strategy
def signal(market) = market.trend_up? ? :buy : :hold
end
# the engine is closed to modification, open to new strategies
class Engine
def initialize(strategy) = @strategy = strategy
def tick(market) = execute(@strategy.signal(market))
end
Adding a new strategy is writing a new class that implements signal — the engine never changes, so
the tested, working code is never touched to support a new idea. For a system whose central activity
is experimenting with strategies, this is the difference between safe iteration and a codebase that
gets riskier with every addition.
Dependency Inversion: depend on abstractions, swap the exchange
The Dependency Inversion Principle — depend on abstractions, not concretions — is what kept the system from being welded to one exchange. The engine and strategies must not depend on the specific exchange (Binance, Kraken, whatever); they depend on an abstract exchange interface, and concrete adapters implement it:
class Exchange # the abstraction the system depends on
def ticker(pair) = raise NotImplementedError
def place_order(side:, amount:, price:) = raise NotImplementedError
end
class BinanceAdapter < Exchange; end # concrete implementations
class KrakenAdapter < Exchange; end
class PaperExchange < Exchange; end # a fake for backtesting/testing!
Engine.new(strategy).run(exchange: BinanceAdapter.new)
This bought two huge things. First, you can support multiple exchanges without touching the
core. Second — and this is gold for a trading system — you can inject a PaperExchange (a fake)
for backtesting and testing, running the entire system against simulated market data and orders with
no real money. DIP is what made the system testable at all; without it, every test would have hit
a real exchange. Depending on the abstraction, not the concrete exchange, is what let the dangerous
parts be exercised safely.
Interface Segregation and Liskov, briefly
The other two earned smaller but real keep. Interface Segregation (don’t force clients to depend
on methods they don’t use) meant a read-only market-data consumer didn’t depend on the
order-placing methods — a strategy that only reads the market can’t accidentally place an order,
because its interface doesn’t include that. Liskov Substitution (subtypes must be usable wherever
the base type is) meant every Exchange adapter and every Strategy truly behaved as its
interface promised, so the PaperExchange could stand in for a real one in tests with confidence —
if a fake didn’t honour the contract, the tests would lie.
The honest caveat: SOLID is a guide, not a religion
Having praised it, the war-story lesson includes the counter-lesson. SOLID applied dogmatically — an interface for everything, every class split to atoms, abstractions for things with one implementation that will never have another — produces its own mess: a fog of tiny classes and indirection that is as hard to follow as the god-class you were avoiding. The trading system benefited from SOLID where complexity and change actually lived — strategies (many, always changing → OCP), exchanges (several, swappable, must be fakeable → DIP), the distinct concerns that change separately (→ SRP). It would not have benefited from inverting a dependency that has exactly one implementation forever, or splitting a cohesive class because a rule said so. The judgment — the same one running through these architecture posts — is to apply the principle where it pays for the indirection it costs, and not where it is ceremony.
Verdict
SOLID stops being acronym soup the moment a system is complex enough to punish you for ignoring it, and an algorithmic trading system — many strategies, multiple exchanges, money on the line — is a perfect teacher. Single Responsibility let each concern change without endangering the others; Open/Closed let new strategies be added without touching the tested engine; Dependency Inversion decoupled the system from any one exchange and, crucially, made it testable with a paper exchange; and ISP and LSP kept the interfaces honest. But the deepest lesson is the caveat: SOLID is a guide to be applied where complexity and change genuinely live, not a religion to impose everywhere. Applied with that judgment, the five principles turned a system that had to be both flexible and trustworthy into one that actually was — which, when real money rides on every decision, is the whole point.