Phase 1: Global Understanding

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

⚠️ Advanced Content (Optional)

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
Figure 1: The Five Design Philosophies of NautilusTrader

1.1 Domain-Driven Design (DDD)

📖 Code in the Language of Trading

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 ConceptCode Syntax ClassDescription
PricePriceNot 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/VolumeQuantityFixed-point structure ensuring exact execution sizing and integrity.
OrderOrderA complete order object tracking every state transition from submission to execution.
PositionPositionAggregated from multiple execution events, reflecting real-time mark-to-market PnL and inventory.
InstrumentInstrumentFinancial instruments like equities, futures, and options, each with dedicated subclasses.
AccountAccountCash or margin accounts, each carrying specific settlement logic.
💡 Why is this critical?

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)

⚡ Everything is an Event

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
Figure 2: Event-driven algorithmic execution lifecycle

1.3 Hexagonal Architecture (Ports and Adapters)

🔌 Strategies are decoupled from external connectivity

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
Figure 3: Hexagonal architecture — Core logic operates neutrally regardless of the exchange
✅ Your Direct Advantage

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

💥 Crashing is an acceptable way to stop

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.

⚠️ Pragmatic Trading Relevance

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

🚫 Bad Data? Instant Rejection!

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
Figure 4: Fail-Fast structurally isolating systemic contamination vectors

Specific categorical violations triggering framework `Panics` (Insta-halts):

Violation ProtocolObserved ExamplesCore Necessity Justification
Arithmetic OverflowE.g., Adding timestamps exceeds `u64` limitsResults in silent data corruption.
Invalid DataE.g., Price is NaN or InfinityCascades into erroneous downstream calculations.
Type Conversion FailureE.g., Negative quantity or timestampRepresents physically impossible states.
Malformed FormatE.g., Unparseable price stringIndicates 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)"]
Figure 5: Strict Sequential Prioritization Parameters
⚠️ Real-World Trade-offs

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 --> [*]
Figure 6: Global Operational Lifecycle Topologies

Stable vs. Transitional States

StateTypeDescription
PRE_INITIALIZEDStableNewly instantiated, configuration pending.
READYStableConfigured and ready to start.
RUNNINGStableOperating normally.
STOPPEDStableExecution halted.
DEGRADEDStableOperating with degraded functionality (e.g., disconnected).
FAULTEDStableSystem encountered a fault.
DISPOSEDStableAll resources released.
STARTING, STOPPING, etc…TransitionalMoving between states; components do not rest here.
💡 Trading Scenario

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
Figure 7: Implementing architectural logic splits across variable types.
💡 Why the Separation?

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:

🧵 Core Principle: Single-Threaded Logic Core

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
Figure 8: Structuring Singular Deterministic Paths supported via detached IO threaded pools.
✅

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.

💡 Inspired by LMAX Exchange

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

🚨 Important Restriction

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

🧠 Exercise 1: Design Decision Reasoning

How would NautilusTrader handle the following scenarios? Choose the correct action and explain why:

  1. 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
  2. WebSocket connection drops for 3 seconds → The adapter state transitions to ____
    A) FAULTED
    B) DEGRADED
  3. Migrating a strategy from Binance to OKX → Requires modifying ____
    A) Only the configuration files
    B) Rewriting the strategy code
1. B) Panic and crash immediately → Fail-Fast protocol. A negative price is physically impossible and indicates corrupted data. Continuing execution would lead to catastrophic trading decisions.2. B) DEGRADED → Temporary disconnections trigger a degraded state. The system continues operating using cached order book data while actively attempting reconnection. It only escalates to FAULTED if recovery is deemed impossible.3. A) Only the configuration files → The power of Hexagonal Architecture. The strategy interacts only with normalized ports, agnostic to Binance or OKX. Switching venues means instantiating an `OKXAdapter` instead of a `BinanceAdapter`, with the strategy still talking to the normalized port.
🧠 Exercise 2: Identify the Design Pattern

Which design pattern is reflected in each of the following NautilusTrader features?

  1. The order object is named Order instead of DataPacket → ________
  2. The strategy’s on_bar() callback is automatically triggered when a new bar arrives → ________
  3. The system recovers its state from Redis after a crash → ________
  4. The same strategy logic connects to different exchanges without modification → ________
1. Domain-Driven Design (DDD) — Utilizing the ubiquitous language of trading 2. Event-Driven Architecture (EDA) — Triggered via push events rather than polling 3. Crash-Only Design — Unified recovery path and externalized state 4. Hexagonal Architecture (Ports and Adapters) — Decoupling core business logic from external systems

8. Module Summary

📋 Key Takeaways
  1. Five Design Patterns: Domain-Driven Design (DDD), Event-Driven Architecture (EDA), Hexagonal Architecture, Crash-Only, and Fail-Fast.
  2. Quality Priorities: Reliability > Performance > Modularity > Testability > Maintainability > Deployability.
  3. Component State Machine: Defined by 7 stable states and 5 transitional states, universally governing all components.
  4. Single-Threaded Core: Preserves deterministic event sequencing; network I/O is offloaded to background threads.
  5. Fail-Fast: Corrupted data is instantly rejected, preventing it from polluting the matching engine or execution logic.
  6. Hexagonal Architecture: The core mechanism enabling 100% code parity between backtesting and live trading.