Module 03: Design Philosophy & Quality AttributesWhy NautilusTrader is designed the way it is
In the previous module, we looked at how the system is structured. This module focuses on why it is built this way. NautilusTrader’s design choices come from practical trading and infrastructure constraints, not theory for its own sake.
1. Five Architectural Design Patterns
This section involves advanced software engineering and system architecture concepts. If you are new to quantitative trading, you can skim or skip this section. It will not affect your ability to write trading strategies later.
NautilusTrader applies five battle-tested engineering design patterns. We will explain each of them using practical trading scenarios.
flowchart LR
root(("🐚 NautilusTrader<br />Design Philosophy"))
subgraph ddd["💎 Domain-Driven Design (DDD)"]
direction TB
ddd1["Code in Trading Domain Language"]
ddd2["Price, Order, Position"]
end
subgraph eda["⚡ Event-Driven Architecture (EDA)"]
direction TB
eda1["Everything is an Event"]
eda2["Immediate Reactivity"]
end
subgraph hexa["🔌 Hexagonal Architecture"]
direction TB
hexa1["Core Isolated from External"]
hexa2["Swappable Adapters"]
end
subgraph crash["💥 Crash-Only Design"]
direction TB
crash1["Crash and Recover"]
crash2["Fast Restart"]
end
subgraph fail["🚫 Fail-Fast Integrity"]
direction TB
fail1["Instant Rejection of Bad Data"]
fail2["Prevent Contagion"]
end
root ==> ddd
root ==> eda
root ==> hexa
root ==> crash
root ==> fail
style root fill:#0f1419,stroke:#4fc3f7,stroke-width:2px
style ddd fill:#0f1419,stroke:#ab47bc,stroke-width:1px
style eda fill:#0f1419,stroke:#ab47bc,stroke-width:1px
style hexa fill:#0f1419,stroke:#ab47bc,stroke-width:1px
style crash fill:#0f1419,stroke:#ab47bc,stroke-width:1px
style fail fill:#0f1419,stroke:#ab47bc,stroke-width:1px
1.1 Domain-Driven Design (DDD)
Core Concept: The codebase should use the ubiquitous language understood by quantitative traders, rather than abstract programming concepts.
In NautilusTrader, you will encounter the following definitive namespaces:
| Trading Concept | Code Syntax Class | Description |
|---|---|---|
| Price | Price | Not a standard float, but a fixed-point arithmetic structure ensuring precision to avoid floating-point errors (e.g., 0.1 + 0.2 ≠ 0.3). |
| Quantity/Volume | Quantity | Fixed-point structure ensuring exact execution sizing and integrity. |
| Order | Order | A complete order object tracking every state transition from submission to execution. |
| Position | Position | Aggregated from multiple execution events, reflecting real-time mark-to-market PnL and inventory. |
| Instrument | Instrument | Financial instruments like equities, futures, and options, each with dedicated subclasses. |
| Account | Account | Cash or margin accounts, each carrying specific settlement logic. |
Because it makes the codebase immediately readable for quants. You don’t need to guess the precision of a `float price`. Price("1.23450", precision=5) is unambiguous: it represents a price with a 5-decimal tick size precision.
1.2 Event-Driven Architecture (EDA)
Core Concept: The system does not poll sequentially; instead, it reacts only when an event occurs.
Consider the difference between two architectural approaches:
Traditional Method (Polling)
Polling every second: “Has the price changed?”, “Is there a new bar?”, “Is my order filled?”
→ Wastes significant computational resources when no state changes occur.
Event-Driven Method
The system pushes notifications: “New bar generated!”, “Your order is filled!”
→ Runs only when data arrives, ensuring high throughput and low latency.
Inside NautilusTrader, your strategies physically “subscribe” to targeted event streams, awaiting system callbacks:
sequenceDiagram
participant S as 🎯 Your Strategy
participant MB as 📡 Message Bus
Note over S: When `on_start()` initializes
S->>MB: Subscribe to BTC/USDT 1-Minute Bars
S->>MB: Subscribe to BTC/USDT L1 Quotes
Note over S: Strategy sleeps awaiting events...
MB->>S: `on_bar(bar)` — New bar received!
S->>S: Calculates indicators, evaluates signals
MB->>S: `on_quote_tick(tick)` — New tick received!
S->>S: Updates real-time data
MB->>S: `on_order_filled(event)` — Order filled!
S->>S: Processes execution logic
1.3 Hexagonal Architecture (Ports and Adapters)
Core Concept: Core business logic is isolated from external systems. Communication is handled exclusively through “Ports” and “Adapters”.
flowchart LR
subgraph outer_left["External Feeds"]
binance_data["Binance WS"]
ib_data["IB API"]
file_data["Historical Files"]
end
subgraph port_left["📥 Intake Ports"]
dc["DataClient<br />(Interface)"]
end
subgraph core_brain["🧠 Core Business Logic"]
de["DataEngine"]
strategy["Your Strategy"]
ee["ExecutionEngine"]
end
subgraph port_right["📤 Exhaust Ports"]
ec["ExecutionClient<br />(Interface)"]
end
subgraph outer_right["External Venues"]
binance_exec["Binance API"]
ib_exec["IB API"]
sim_exec["Simulator"]
end
binance_data --> dc
ib_data --> dc
file_data --> dc
dc --> de --> strategy --> ee --> ec
ec --> binance_exec
ec --> ib_exec
ec --> sim_exec
This is why your strategy code remains exactly the same in both backtesting and live trading. The strategy only interacts with ports, oblivious to whether it’s backed by a simulator or Binance. Switching exchanges is primarily an adapter/configuration change.
1.4 Crash-Only Design
Core Concept: Instead of writing complex “graceful shutdown” logic, ensure the system can recover rapidly from a crash.
Unified Recovery Path
Startup and crash recovery share the same code path. Since every startup is effectively a “recovery,” this path is thoroughly tested.
Externalized State
Critical states (orders, positions) are persisted to external storage (e.g., Redis). System crashes will not result in state loss.
Rapid Restart
Designed to restart in milliseconds after a crash, minimizing system downtime.
Idempotent Operations
Operations are designed to be safely retryable. Re-executing them after a restart will not produce unintended side effects.
Imagine your trading node crashes at 3:00 AM. Crash-Only design means the system will automatically restart, recover all order and position states from Redis, and resume trading. You might not even realize a crash occurred until you check the logs.
1.5 Fail-Fast Integrity Constraints
Core Concept: In trading systems, corrupted data is far more dangerous than no data. When invalid data is detected, the system panics immediately to prevent contagion.
flowchart LR
subgraph bad_scenario["😱 Without Fail-Fast"]
bad_price["Receives NaN Price"] --> calc["Calculates Indicators"] --> signal["Generates Erroneous Signal"] --> order["Submits Bad Order"] --> loss["💸 Financial Loss"]
end
flowchart LR
subgraph good_scenario["🛡️ Fail-Fast Protection"]
bad_price2["Receives NaN Price"] --> reject["🚨 Instantly Rejects and Panics"] -->
log["Logs Error"] --> safe["System Remains Safe"]
end
Specific categorical violations triggering framework `Panics` (Insta-halts):
| Violation Protocol | Observed Examples | Core Necessity Justification |
|---|---|---|
| Arithmetic Overflow | E.g., Adding timestamps exceeds `u64` limits | Results in silent data corruption. |
| Invalid Data | E.g., Price is NaN or Infinity | Cascades into erroneous downstream calculations. |
| Type Conversion Failure | E.g., Negative quantity or timestamp | Represents physically impossible states. |
| Malformed Format | E.g., Unparseable price string | Indicates a fundamental issue with the data feed. |
2. Quality Attribute Prioritization
When designing a trading system, non-functional requirements often conflict. NautilusTrader resolves these trade-offs using the following priority hierarchy:
flowchart LR
r["🥇 Reliability"] ==> p["🥈 Performance"]
p ==> m["🥉 Modularity"]
m ==> t["4️⃣ Other Attributes (Testability, Maintainability, Deployability)"]
This hierarchy explains many design choices. For example, when it encounters suspicious data, the system chooses to crash (Reliability prioritized) rather than attempt to fix it and continue (Availability prioritized). In automated trading, a wrong decision is far more dangerous than a brief period of downtime.
3. Component State Machine
Every component in NautilusTrader (Engines, Strategies, Adapters) adheres to a unified State Machine pattern, defining explicit lifecycle phases:
stateDiagram-v2
[*] --> PRE_INITIALIZED : Newly Created
PRE_INITIALIZED --> READY : register()
READY --> STARTING : start()
STARTING --> RUNNING : Running
RUNNING --> STOPPING : stop()
STOPPING --> STOPPED : Stopped
STOPPED --> STARTING : start()
STOPPED --> RESETTING : reset()
RESETTING --> READY : Reset Complete
RUNNING --> DEGRADING : degrade()
DEGRADING --> DEGRADED : Degraded State
RUNNING --> FAULTING : fault()
FAULTING --> FAULTED : Faulted State
DEGRADED --> STOPPING : stop()
DEGRADED --> FAULTING : fault()
STOPPED --> DISPOSING : dispose()
FAULTED --> DISPOSING : dispose()
DISPOSING --> DISPOSED : Disposed
DISPOSED --> [*]
Stable vs. Transitional States
| State | Type | Description |
|---|---|---|
| PRE_INITIALIZED | Stable | Newly instantiated, configuration pending. |
| READY | Stable | Configured and ready to start. |
| RUNNING | Stable | Operating normally. |
| STOPPED | Stable | Execution halted. |
| DEGRADED | Stable | Operating with degraded functionality (e.g., disconnected). |
| FAULTED | Stable | System encountered a fault. |
| DISPOSED | Stable | All resources released. |
| STARTING, STOPPING, etc… | Transitional | Moving between states; components do not rest here. |
When an exchange WebSocket disconnects, the corresponding Adapter transitions from RUNNING to DEGRADED. The system continues operating using cached order book data while attempting to reconnect. If successful, it returns to RUNNING. If the connection is unrecoverable, it may transition to FAULTED.
4. Actor vs. Component Traits
In the underlying Rust implementation, the system differentiates between two architectural roles:
Actor
Primary Function: Receives and processes messages.
Core Interface: `handle(message)`
Design Focus: Lightweight and focused on message handling.
Example Role: Throttler
Component
Primary Function: Manages lifecycle states.
Core Interface: `start()`, `stop()`, `reset()`
Attribute Logic: Implements state machines and lifecycle hooks.
Example Role: DataEngine, ExecutionEngine
Some objects demand dual traits seamlessly:
flowchart LR
subgraph actors["🎭 Actor Trait (Message Processing)"]
direction TB
throttler["Throttler"]
end
subgraph hybrid["🧩 Dual Traits"]
direction TB
strategy["Strategy"]
end
subgraph components["🔧 Component Trait (Lifecycle Management)"]
direction TB
de["DataEngine"]
ee["ExecutionEngine"]
end
actors --- hybrid --- components
Separation provides composable flexibility. Your strategy needs to receive messages (Actor) and manage lifecycle starts/stops (Component), so it inherits both. However, a rate-limit throttler is just a message forwarder; it doesn’t require complex lifecycle management, so being purely an Actor is sufficient.
5. Threading Model
This is a critical technical decision that directly impacts system throughput and determinism:
The core processing logic in NautilusTrader operates on a single thread. This includes all message bus dispatching, strategy logic, risk checks, and order management.
flowchart LR
subgraph single["🧵 Single-Threaded Core (Deterministic Execution)"]
direction TB
logic["Message Dispatch / Strategy Logic<br />Risk Checks / Position Updates"]
end
subgraph multi["🔀 Multi-Threaded I/O (High Concurrency)"]
direction TB
io["WebSocket / API / DB<br />Data Persistence"]
end
multi ==>|"Event Push"| single
single ==>|"Command Dispatch"| multi
style single fill:#0f1419,stroke:#4fc3f7,stroke-width:2px
style multi fill:#0f1419,stroke:#ab47bc,stroke-width:1px
Benefits of a Single-Threaded Core
• Preserves deterministic event sequencing.
– Avoids lock contention in the core event loop.
– Keeps backtesting and live execution behavior aligned.
– Makes debugging materially easier.
Benefits of Multi-Threaded I/O
• Network operations never block the core logic thread.
• WebSocket connections are maintained in isolated background threads.
• Database persistence does not degrade strategy execution latency.
• Uses multi-core CPU architectures effectively.
This architecture is heavily inspired by the LMAX Disruptor pattern. LMAX is a real-world FX exchange that processes over 6 million transactions per second on a single thread, proving the exceptional performance of single-threaded matching engines in high-frequency trading.
6. Process Limitation: One Node Per Process
You cannot run multiple TradingNode or BacktestNode instances within the same process. This is because the system relies on globally shared singleton resources (e.g., logging subsystems, global runtimes).
Correct Approaches:
- Running multiple strategies within a single
TradingNode✅ - Running multiple nodes across distinct OS processes ✅
- Running multiple backtests sequentially (waiting for completion before starting the next) ✅
7. Interactive Exercises
How would NautilusTrader handle the following scenarios? Choose the correct action and explain why:
- Receives a quote with a price of `-5.00` → The system will ____
A) Ignore the data point and continue running
B) Panic and crash immediately - WebSocket connection drops for 3 seconds → The adapter state transitions to ____
A) FAULTED
B) DEGRADED - Migrating a strategy from Binance to OKX → Requires modifying ____
A) Only the configuration files
B) Rewriting the strategy code
Which design pattern is reflected in each of the following NautilusTrader features?
- The order object is named
Orderinstead ofDataPacket→ ________ - The strategy’s
on_bar()callback is automatically triggered when a new bar arrives → ________ - The system recovers its state from Redis after a crash → ________
- The same strategy logic connects to different exchanges without modification → ________
8. Module Summary
- Five Design Patterns: Domain-Driven Design (DDD), Event-Driven Architecture (EDA), Hexagonal Architecture, Crash-Only, and Fail-Fast.
- Quality Priorities: Reliability > Performance > Modularity > Testability > Maintainability > Deployability.
- Component State Machine: Defined by 7 stable states and 5 transitional states, universally governing all components.
- Single-Threaded Core: Preserves deterministic event sequencing; network I/O is offloaded to background threads.
- Fail-Fast: Corrupted data is instantly rejected, preventing it from polluting the matching engine or execution logic.
- Hexagonal Architecture: The core mechanism enabling 100% code parity between backtesting and live trading.