Architecting a Modular Trading Engine: 11 Maven Modules, Zero Circular Dependencies
A retrospective on splitting a Java trading system into an 11-module Maven monorepo: why the dependency graph matters more than the module count, how a zero-circular-dependency rule paid off during maintenance, and what I would do differently next time.
Architecting a Modular Trading Engine: 11 Maven Modules, Zero Circular Dependencies
The trading-bridge project started as a single Maven module. One pom.xml, one src/ tree, everything in one package. It worked for about two weeks. Then the strategy count hit 30, the backtest engine grew a control plane, and the parser needed its own test suite. The monolith was breathing hard.
Six months later, the project is an 11-module Maven monorepo
The 11-module Maven dependency graph: trading-core at the foundation, four engine modules in row 2, strategy modules in row 3, application modules at the top, and side utilities. All arrows flow upward from core -- zero circular dependencies.
with a strict acyclic dependency graph. No module depends on another module that depends back on it. No cycles, ever. This is not a story about Maven. It is a story about enforced modularity.
Why monolithic dies at module 3
A single-module Java project with trading strategies, a backtest engine, broker connectors, and a parser works until you need to change two things at once. You modify the Order model to support stop-loss orders. The backtest engine breaks. The parser breaks. The broker connector breaks. You fix all three, but you cannot test them independently because the test suite runs everything.
The cost of a single change rises non-linearly with the number of components sharing a module. At 5+ components, every change is a full-regression gamble. The fix is not more tests. The fix is less surface area per module.
The dependency graph as a design document
Here is the actual module dependency graph:
trading-core domain, indicators, golden baseline trading-backtest depends on trading-core trading-data depends on trading-core trading-parser depends on trading-core trading-broker depends on trading-core trading-strategies depends on trading-core, trading-data trading-genetics depends on trading-core, trading-backtest trading-examples depends on trading-core, trading-backtest, trading-strategies, trading-data trading-runtime depends on trading-backtest, trading-strategies, trading-data, trading-broker trading-tui depends on HTTP client only (Jackson + JLine3)
Every arrow flows outward from trading-core. Nothing flows back. This is not an accident: it is enforced by Maven's reactor build. A circular dependency causes a build failure. You cannot ship a cycle.
Three rules that kept the graph clean
Rule 1: Core is sacred. trading-core has zero internal trading dependencies. It contains domain models (Bar, Order, Strategy), indicator math, and the golden backtest baseline. No HTTP clients. No XML parsing. No broker logic. Every change to core forces a full rebuild, so core changes happen rarely and deliberately.
Rule 2: One concern per module. The parser only parses StrategyQuant XML. The broker module only sends orders to OANDA or IBKR. The genetics module only runs genetic optimization. When a concern crosses module boundaries (e.g., a strategy needs both data from an API and backtest execution), the consuming module depends on both providers. The providers never depend on each other.
Rule 3: No knowledge of consumers. A module never knows who depends on it. trading-core does not know about strategies, backtests, or brokers. trading-data does not know who requested the data. This keeps the dependency direction one-way and the refactoring cost local.
What 11 modules bought us
Faster CI. A change to trading-parser builds and tests only the parser and its transitive consumers (examples and runtime). The backtest engine and broker modules are untouched. The average CI run dropped from 8 minutes to 90 seconds.
Parallel development. The parser and the broker connector were built by different agents (human and AI) without merge conflicts. They touched different modules. The parent POM defined shared dependency versions. Each team worked in isolation.
Clear provenance. When a test fails in trading-backtest, the root cause is either in trading-backtest itself or in trading-core. There are only two places to look. This eliminated the blame-game debugging that happens when six components share a module.
Swappable implementations. The broker module has a Broker interface. OANDA and IBKR are separate implementations loaded at runtime. Adding a new broker means a new class in trading-broker, not a rewrite of the backtest engine or the control plane.
The cost: ceremony that compounds
11 modules means 11 POMs. Each POM needs the parent version, the artifact ID, the dependencies, the build plugin configuration. Maven's archetype generator helps for the first three modules. By module eight, you have written scm and distributionManagement in seven files and you start wondering if Gradle is really that bad.
Inter-module refactoring is also more expensive. Moving a class from trading-core to trading-data means updating imports in every consumer, changing the POM of every consumer, and running a full build. The monolith would let you drag-and-drop and recompile. The monorepo costs you 15 minutes of POM surgery.
Worth it? Yes. But only because the cost was front-loaded. Every hour spent on module boundaries in month one saved 10 hours of debugging in month four.
What I would do differently
Generate the module POMs. A template script that reads a module registry and generates the POMs would eliminate the ceremony. I did not build this until module six. Do it at module two.
Enforce the graph in CI. Maven catches cycles at build time, but it does not catch forbidden dependencies (e.g., trading-core accidentally importing from trading-data). A custom ArchUnit test or a simple dependency:analyze check in CI would catch these before they leak into main.
Use a BOM for shared dependencies. The parent POM handles version management, but a Bill of Materials POM would let consuming modules import only the artifacts they need without inheriting build plugin configuration they do not use. Next iteration.
The takeaway
A modular monorepo with a zero-circular-dependency rule is not about Maven or Java. It is about forcing yourself to think about dependency direction before you write code. The build system is the enforcement mechanism. The design is what gets enforced.
When the backtest engine, the parser, and the broker connector all evolved independently for months without stepping on each other, the 11-module structure paid for itself. The cost was ceremony. The value was autonomy.