Skip to content

Latest commit

 

History

History
121 lines (91 loc) · 3.74 KB

File metadata and controls

121 lines (91 loc) · 3.74 KB

Architecture Decision Records

This document explains the key design decisions in the Solana Execution Engine.

1. Trait-Based Provider Pattern

Decision

Use traits (Strategy, DexClient, WalletSelector, Notifier) instead of concrete implementations throughout the engine.

Rationale

  • Testability: Mock implementations can be injected for unit testing without RPC calls
  • Extensibility: New DEXes (Orca, Raydium) or strategies (TWAP, Grid) can be added without modifying the engine
  • Compile-time safety: Generic bounds ensure all required capabilities are implemented

Trade-offs

  • Slightly more boilerplate than direct implementation
  • Requires understanding of Rust's trait system

2. Type-Safe State Machine

Decision

Use an enum for ExecutionState with explicit transition validation rather than boolean flags.

Rationale

  • Impossible states are unrepresentable: Can't be both "paused" and "stopped"
  • Self-documenting: The enum variants clearly show all possible states
  • Exhaustive matching: Compiler ensures all states are handled
pub enum ExecutionState {
    Idle,
    Initializing,
    Running,
    Paused,
    Stopping,
    Stopped,
    Error,
}

Trade-offs

  • More explicit than checking if paused or if running
  • Requires calling transition() method instead of direct assignment

3. Async Architecture with tokio::select!

Decision

Use tokio::select! with broadcast channels for shutdown instead of Arc<AtomicBool>.

Rationale

  • Cancellation safety: Pending operations are properly dropped on shutdown
  • No polling: Shutdown signal is event-driven, not checked in a loop
  • Multiple subscribers: All components can listen to the same shutdown broadcast
tokio::select! {
    _ = shutdown_rx.recv() => break,
    result = execute_next() => handle(result),
}

Trade-offs

  • Requires understanding of async cancellation semantics
  • Slightly more complex than simple boolean flags

4. Circuit Breaker Middleware

Decision

Wrap DexClient in a CircuitBreaker middleware rather than handling failures inline.

Rationale

  • Separation of concerns: Error handling logic is decoupled from business logic
  • Configurable policies: Different clients can have different thresholds
  • Observability: Metrics track failure rates, rejections, recovery

Trade-offs

  • Additional indirection layer
  • Requires understanding of the Circuit Breaker pattern

5. thiserror Error Hierarchy

Decision

Use thiserror to define a custom error enum rather than anyhow or string errors.

Rationale

  • Programmatic handling: Callers can match on specific error variants
  • Context preservation: Each variant carries relevant context (signature, pubkey, etc.)
  • Zero overhead: No heap allocation for simple errors
pub enum Error {
    Config { message: String, source: Option<Box<dyn Error>> },
    Dex { message: String, source: Option<Box<dyn Error>> },
    RateLimit { message: String, retry_after_ms: Option<u64> },
    // ...
}

Trade-offs

  • More boilerplate than anyhow
  • Requires defining variants upfront

6. Workspace Structure

Decision

Use a Cargo workspace with separate engine (library) and cli (binary) crates.

Rationale

  • Separation of concerns: Library code is independent of CLI
  • Faster compilation: CLI only recompiles when it changes
  • Reusable: Engine can be embedded in other projects (TUI, web service)

Trade-offs

  • More complex directory structure
  • Two Cargo.toml files to maintain

Future Considerations

  • Backtesting: Add BacktestDexClient that reads historical data
  • Metrics: Integrate with Prometheus/OpenMetrics
  • Persistence: Add SQLite backend for order history