Software Design: DRY, Orthogonality, and Modularity
The Pragmatic Programmer’s core habits - don’t repeat knowledge, decouple components, and shoot tracer bullets - applied to a research codebase.
Leads to: The Phase 18 capstones require a modular, DRY architecture; this lesson is the design rubric.
Learning Objectives
Click a status chip to cycle: Not started → In progress → Studied → Practiced → Needs review → Mastered.
- State the DRY principle and refactor duplicated knowledge into a single source of truth.
- Explain orthogonality and identify a coupling that makes a change ripple across modules.
- Design a decoupled pricing/backtest architecture where components can be swapped independently.
- Explain tracer bullets and prototypes and when each is appropriate.
- Implement a refactor that removes duplication and verify behavior is unchanged.
Key Vocabulary
- DRY (Don’t Repeat Yourself)
- Every piece of knowledge has a single, authoritative representation; duplication of knowledge, not just code, is the enemy.
- Orthogonality
- Components are independent: changing one has no unintended effect on another; effects are ‘at right angles’.
- Coupling
- The degree to which one module depends on the internals of another; tight coupling makes changes ripple.
- Tracer bullet
- A thin end-to-end skeleton of the real system, built early to hit the target and then fleshed out.
- Prototype
- Throwaway code built to answer one question (is this feasible?), then discarded - unlike a tracer bullet.
- Single source of truth
- The one place a fact lives (a constant, a config, a function) so a change is made once and propagates.
Intuition & Motivation
A third idea governs how you build: fire a tracer bullet - a thin, working end-to-end path (data → signal → backtest → report) - before perfecting any piece. You see the whole system integrate early, and each component then grows in place. A prototype is different: throwaway code to answer one risky question.
DRY: don’t repeat knowledge
DRY is often misread as ‘never copy-paste code’. The real rule is deeper: every piece of knowledge should have one authoritative representation. If the annualization factor \(252\), the risk-free curve, or the definition of ‘a trade’ appears in many places, a change must be made in all of them - and you will miss one.
sharpe = mean/std*np.sqrt(252) appears in the momentum module, the mean-reversion module, and the report.def annualized_sharpe(r, periods=252): return r.mean()/r.std(ddof=1)*np.sqrt(periods).Orthogonality: independent components
Two components are orthogonal if changing one does not force a change in the other. Draw the dependency arrows: if your signal generator imports the broker API, then swapping brokers touches your signals - a non-orthogonal design. The fix is an interface: components depend on a small contract, not on each other’s internals.
| Design smell | Consequence | Orthogonal fix |
|---|---|---|
| Backtester reads pricer’s private arrays | Improving the pricer breaks the backtest | Depend on a price(option) interface only |
| Data loader hard-codes one vendor’s columns | New data source rewrites the strategy | A loader returns a standard schema; strategy is source-agnostic |
| Costs baked into the signal | Can’t test the signal without a cost model | Signal returns positions; a separate module applies costs |
Tracer bullets vs. prototypes
A tracer bullet is a minimal but real end-to-end system: it loads a tiny dataset, computes a trivial signal, runs a one-line backtest, and prints a metric. It is production code, just skeletal - you keep it and grow it. A prototype is throwaway: you might mock the whole data layer in a notebook just to check whether an idea has any edge, then delete it. Confusing the two - shipping the prototype - is how research code rots.
Modularity and the DRY–orthogonality link
Modularity operationalizes both principles: small modules with clear interfaces give each fact one home (DRY) and let each piece change independently (orthogonality). A research repo typically factors into: data (load/clean), signals (features → positions), execution (positions + costs → fills), backtest (fills → P&L), and report (P&L → metrics/figures). Each depends only on the interface below it.
- Copy-pasting a formula into a new module ‘just for now’ - the copies drift and one gets fixed but not the others.
- Treating DRY as ‘no duplicated lines’; two functions that happen to share a line but encode different knowledge should stay separate.
- Coupling the signal to the broker or the data vendor, so a swap ripples through unrelated code.
- Shipping a prototype as production; its mocks and shortcuts become load-bearing.
- Over-abstracting too early - building a plugin framework before you have two real cases (a different failure than duplication).
- Global config read directly everywhere, so no single component owns a parameter - a hidden form of coupling.
- Ask ‘where does this fact live?’ Every constant (252, the cost model, the universe) should have exactly one home.
- Refactor under test: pin behavior with an assertion, extract the function, confirm the assertion still passes, then delete copies.
- Design interfaces as verbs:
load(),signal(),execute(),evaluate(). Swap implementations freely. - Fire a tracer bullet on day one: get end-to-end plumbing working on toy data before optimizing any stage.
- Prefer two concrete cases before you abstract; premature abstraction couples things that shouldn’t be.
Interactive: remove the duplication
The starter has the same Sharpe knowledge encoded three times. Refactor to a single source of truth and prove the refactor changed no numbers.
Knowledge Check
Practical Exercise
You are given a 900-line research script that loads data, computes three signals, applies costs, backtests, and plots - all inline, with the universe list and cost rate hard-coded in five places each. Propose a modular, DRY, orthogonal redesign: name the modules, their interfaces, and where each fact lives. Then describe how you would refactor safely.
Modules and interfaces (each depends only on the one below):
data.load(universe, dates) → DataFramewith a standard schema; the vendor detail lives here only.signals.build(prices) → positions; each signal is a function with the same signature so they are interchangeable.execution.apply_costs(positions, cost_model) → net_returns; the cost rate lives incost_modelalone.backtest.run(net_returns) → equity, metrics; knows nothing about how signals were built.report.figure(metrics); pure presentation.
Single sources of truth: the universe list and the cost rate each live in one config object, imported everywhere; the annualization factor lives in one metrics function.
Safe refactor: first pin the script’s current outputs (equity curve, metrics) on a fixed seed/dataset with np.allclose assertions. Extract one module at a time, re-running the assertions after each extraction. Only when all pass and duplication is gone do you delete the original inline code.
Lesson Summary
Retrieval Practice
Close the lesson and answer from memory before checking. This is deliberate, effortful recall - the single highest-yield study action.
A: DRY: every piece of knowledge has a single authoritative representation. Example: the 252 trading-day annualization factor typed into many modules; it should live in one metrics function so a change is made once.
A: Orthogonality means components are independent, so changing one has no unintended effect on another. Quick test: ‘if I change X, how many files must I touch?’ A decoupled design answers ‘one’.
Completion Checklist
- I can explain the core ideas in my own words
- I worked the derivations/examples by hand
- I completed the interactive workbench(es)
- I passed the knowledge check
Source References
This lesson synthesizes and paraphrases concepts from the sources below. No copyrighted text, problem sets, or solutions are reproduced. Return to the originals for full depth.
- The Pragmatic Programmer (Thomas & Hunt, 2nd/20th Anniv. ed., 2019) current - Ch. 2, 7, 8 - The DRY principle, orthogonality/decoupling, tracer bullets vs. prototypes, and refactoring.
- The Elements of Statistical Learning (Hastie, Tibshirani & Friedman, 2nd ed.) current - Ch. 7 - Modular estimation/validation pipelines as an example domain for decoupled research code.