Architecture
Gordon v7 is a five-service Rust split with six library crates published to an internal kellnr registry, one schema migrator, one Next.js console, and one Python research lab. Services communicate over a single NATS JetStream stream and a shared PostgreSQL instance. No two services share a process, a binary, or a set of credentials.
Runtime stack
Services and crates
Runtime services
| Service | Port | Responsibility |
|---|---|---|
| gordon-data | 8081 | Sole writer of market_data.*. WebSocket ingest from Binance/alt.me/Deribit/FRED. REST /klines, /warmup, /healthz. |
| gordon-risk | 8082 | Five circuit breakers at 30 Hz. Halt-latch. Emergency flatten. Pause commands. |
| gordon-manager | 8083 | Control plane + console BFF. Bot lifecycle. Backtests. WS fan-out to browser. |
| gordon-bot | 8084 | Pure strategy engine. No exchange keys. One container per strategy. |
| gordon-executor | 8085 | Sole holder of trading-capable Binance keys. Consumes intents, reconciles, emergency-flattens. |
| gordon-migrate | — | One-shot schema migrator. Owns migrations/. Applies then exits 0 or 1. |
Library crates (kellnr)
| Crate | Version | Role |
|---|---|---|
| gordon-kernel | 2.x | Error taxonomy (GordonError, ErrorKind, ErrorCode) + TraceId. Narrow leaf, frozen-ish. |
| gordon-domain | 1.x | Cross-service business types + ExecutionModel trait. Churns with business changes. |
| gordon-protocol | 7.x | NATS bus subjects + payload structs (KlineEvent, OrderIntentEvent, RiskCommandEvent, BreakerEvent). Additive-only. |
| gordon-platform | 0.4.x | Runtime libs: HTTP client + breaker, Axum trace middleware, tracing-subscriber, pg LISTEN/NOTIFY. |
| gordon-strategy | 4.x | Strategy trait, MarketContext, Signal, 14 entry strategies, overlays, BacktestExecution. Pure math, no I/O. |
| gordon-bus | 2.x | Publisher/Consumer traits over NATS JetStream + Postgres outbox. |
| gordon-exchange | 5.x | Binance REST + WS + rate limiter + FeeModel. Live and testnet. |
| gordon-test-db | 1.x | Per-test fresh Postgres clone (template-based). Optional NATS test helper. |
Other repos
| Repo | Role |
|---|---|
| gordon-console | Next.js 16 operator UI. App Router. Auth.js v5 + Kanidm OIDC. |
| gordon-lab | Python research lab. Walk-forward ablation, ML experiments. Read-only DB role. |
| gordon-docs | This VitePress site. LAN-only on srv-apps. |
Split rationale
Gordon v6 ran as two monolithic repos (gordon-trading + gordon-ui). All concerns were co-located in a single process. Three concrete problems drove the v7 split:
1. Blast radius. A panicking strategy engine killed data ingest and order reconciliation in the same process. There was no way to restart only the broken component.
2. Key co-mingling. Trading-capable Binance API keys were loaded by the same process that fetched market data. Even read-only ingest carried write-capable credentials.
3. Deployment coupling. A backtest configuration change required a full restart of the live trading loop.
The five-service split is the coarsest decomposition that achieves independent deployment, key isolation, and blast-radius containment. A two-service split (data + trading) would still combine strategy, risk, and execution in one process. A microservices approach (10+ services) adds coordination overhead without solving the stated problems.
Consequences
- gordon-executor holds the only trading-capable keys. No other service can submit orders even if compromised.
- gordon-bot crash leaves gordon-executor running; open positions are protected.
- Backtest runs (gordon-manager) do not compete with live strategy evaluation (gordon-bot) for CPU.
- gordon-data can be restarted independently without pausing risk monitoring.
- Cross-service IPC introduces network hops. For the NATS-based hot paths this is acceptable. For browser fan-out, gordon-manager is the aggregation point.
Database
Single PostgreSQL instance (GORDON_DATABASE_URL). search_path = trading, market_data, public.
Least-privilege roles per service (enforced by gordon-migrate migrations):
| Role | Grants |
|---|---|
gordon_data_writer | INSERT on market_data.* |
gordon_executor | INSERT/UPDATE on trading.orders, trading.trades, trading.order_events, trading.fill_events |
gordon_risk | INSERT on risk_events, risk_audit_log; column-level UPDATE on bot_configs.desired_state only |
gordon_manager | INSERT on bot_configs, runs, equity_points; reads via named views |
gordon_bot | INSERT on order_intents; advisory-lock-based lease via bot_leases |
gordon_lab_reader | SELECT only — INSERT/UPDATE/DELETE revoked |
Direct SELECT on underlying market_data.* tables is revoked for runtime services. Readers go through named views (v_klines_reader, v_metrics_reader, v_macro_reader, v_funding_rates_reader, v_open_interest_reader).
Infrastructure
| Host | Role |
|---|---|
| srv-apps | Pi5, 2 TB SSD, ARM64. Runtime host for the full v7 stack + Postgres + nginx + Grafana + Loki + Prometheus + kellnr + gordon-docs. |
| srv-core | Pi5. Pi-hole DNS, Bitcoin node. CI runner (ARM64) + restic backup destination for trading.*. |
| srv-compute | X64 CI runners only. |
Operator-accessible surface on srv-apps: gordon-console on port 3000 only. All other services are Docker-internal.
Invariants
- gordon-executor is the sole holder of trading-capable exchange keys.
- gordon-data is the sole writer of
market_data.*. - gordon-migrate is the sole owner of schema migrations.
- gordon-lab never writes to the database (
gordon_lab_readerrole revokes all writes DB-side). - All runtime services gate on
gordon-migratecompleting (service_completed_successfully) before starting. - No service holds a direct SELECT grant on underlying
market_data.*tables — named views only.
Related
- Event Flow — NATS stream topology and outbox pattern.
- BFF Boundary — which traffic goes through manager and which goes direct.
- Modules — per-service and per-crate reference pages.