Design Philosophy¶
TL;DR
Aurum’s design reflects five principles: push errors to compile time, keep releases immutable, separate signals instead of synthesizing state, expose the narrowest interface that works, and prefer explicit contracts over implicit conventions. This page explains the reasoning behind each principle and the alternatives that were rejected.
Why a design philosophy page?¶
Documentation usually tells you what a system does and how to use it. This page tells you why it was built this way. Understanding the principles helps you predict how unfamiliar parts of the system will behave, and helps you contribute design decisions that are consistent with the existing architecture.
If you are writing a new strategy, you don’t need this page. If you are proposing a new feature, extending the SDK, or trying to understand why a design decision was made the way it was, start here.
Principle 1: Compile-time over runtime¶
Push errors to the earliest possible detection point.
Topic keys like bar://okx/BTCUSDT@1m are string literals validated at
compile time. A typo in the interval (@1n instead of @1m) produces a
build error, not a silent runtime failure six hours into a trading session.
Schema mismatches are compile-time errors. on(TOPIC<"bar://okx/BTCUSDT@1m">,
handler) deduces the payload type from the topic key. If your handler accepts
Tick<schema::BookRow> for a bar topic, the code does not compile. You find
out in your editor, not in production.
Note
This is the same design philosophy as Rust’s type system: the compiler should reject incorrect programs, not hope they work at runtime. The cost of a build error is paid once by one developer. The cost of a runtime data error is paid by every operator who touches the system.
Alternative considered: runtime-registered string keys. Some systems use runtime string keys like Kafka topic names, where the producer and consumer agree on a string contract. This is more flexible — you can add topics without recompiling. But it pushes data contract errors to runtime: a consumer that expects a different schema than the producer receives garbled data with no compile-time warning. For a trading system where data correctness is the primary requirement, we chose compile-time safety over runtime flexibility.
Principle 2: Immutable releases¶
A published ``name@version`` is permanent. You cannot overwrite it. Increment the version and push a new release.
This means:
Every deployment is auditable. You can trace exactly which version of which project was running at any point in time.
Every version is rollback-capable.
aurum stop signal-watch --version 0.0.2 && aurum run signal-watch --version 0.0.1always works because 0.0.1 still exists in the registry.There is no “deploy the same version with different code.” Version numbers are cheap; the cost of an ambiguous deployment is high.
Alternative considered: mutable tags like Docker ``latest``. A mutable tag
is convenient for development — push to latest and your deployment picks it
up. But it makes rollback impossible (what was latest five minutes ago?)
and audit impossible (which code was running during the incident?). For a
trading system where every execution is recorded and potentially regulated,
immutability is a requirement, not a preference.
Principle 3: Separate signals, don’t synthesize state¶
Each data plane reports its own truth. The API does not guess.
Forge reports process state, runtime state, and health classification as separate fields. It does not combine them into a single “healthy” boolean. Why? Because a process can be running while its runtime endpoint is degraded. A project can be stopped while its historical data is still queryable. A capability can be absent because the project does not use that feature — not because it is broken.
If Forge reported a single “healthy” flag, every client would need to reverse-engineer what “unhealthy” meant. Was the process down? The runtime unreachable? The history database failing? By reporting signals separately, Forge lets each client apply its own operational model.
Tip
This principle applies to your strategy code too. Do not infer position from order fills — query the ledger. Do not infer runtime health from process state — check the runtime endpoint. Do not infer data freshness from subscription liveness — check the timestamp on the last frame.
Alternative considered: synthesized health status. Many systems report a single aggregated status (green/yellow/red). This is easier to consume for a quick glance but insufficient for diagnosis. A yellow status could mean anything from “one of ten replicas is slow” to “the primary database is failing over.” Aurum chose signal separation over synthesis because the operator who needs to diagnose an incident at 3 AM deserves the actual signals, not a summary that hides them.
Principle 4: The narrowest interface that works¶
Forge exposes one public API. Terminal uses it. External integrations use it. No one connects to internal components directly.
This means:
The integration surface is small enough to version, secure, and test.
Internal components (
aurumd, runtime ports, ClickHouse) can change their interfaces without breaking external clients.Authentication and authorization happen at one boundary, not at every internal component.
Alternative considered: direct connections to internal components. Some systems expose internal services directly — a metrics endpoint on the runtime process, a database connection string for history queries, a daemon socket for lifecycle commands. This gives power users flexibility but creates an unbounded integration surface. Every internal interface becomes a de facto public API that cannot change without breaking someone’s script.
Principle 5: Explicit contracts over implicit conventions¶
Every data boundary has a typed contract. Topics have compile-time schemas. Resources have catalog entries with explicit model, delivery, and history semantics. Effects have named outcome branches.
Where a convention-based system says “bars are delivered as JSON objects with
these field names, and you need to know that close_timestamp == 0 means
unconfirmed,” Aurum says: Tick<schema::BarRow> is the type,
close_timestamp is a field of that type, and the contract is enforced by
the compiler.
Note
Conventions are cheaper to implement than contracts but more expensive to maintain. Every consumer of a convention must independently implement the same parsing and validation logic. A contract is implemented once and enforced everywhere.
Alternative considered: schema-on-read with JSON. Schema-on-read (where consumers parse whatever the producer sends and handle missing fields ad hoc) is the dominant pattern in web APIs. It works well when consumers are independent and the data model evolves frequently. For a trading system where the data model is well-defined and correctness is critical, schema-on-write with typed contracts is the safer choice.
What these principles say no to¶
Design principles are as much about what you reject as what you accept. Here is what these principles explicitly rule out:
No runtime topic registration. Topics are compile-time literals. You cannot add a topic to a running strategy without recompiling.
No mutable deployments. You cannot overwrite a published version. Every release is a new version number.
No direct database access. External clients cannot connect directly to ClickHouse. All history access goes through the Forge API.
No synthesized health. The API does not combine signals into a single status. Clients receive individual signals and apply their own model.
No convention-based data contracts. Every data boundary has an explicit, typed, compiler-enforced contract.
Warning
These “no” decisions are load-bearing. If you are extending Aurum and find yourself working around one of these constraints, check whether you are inadvertently breaking a principle that exists for a reason documented here. The constraint may need revisiting, but more often the workaround is the problem.
Summary¶
Principle |
What it enables |
What it rejects |
|---|---|---|
Compile-time over runtime |
Bugs found in editor, not production |
Runtime topic registration |
Immutable releases |
Audit trail, always-possible rollback |
Mutable tags, overwrite deploys |
Separate signals |
Precise diagnosis, no hidden state |
Synthesized health booleans |
Narrowest interface |
One integration surface to version and secure |
Direct connections to internal components |
Explicit contracts |
Compiler-enforced correctness |
Convention-based, parse-at-edge patterns |
Next steps¶
To see how these principles manifest in the architecture, read Platform Architecture.
To see the contracts in practice, read Resources, Live Data, and History.
To understand the execution model, read Reactive Application Model.