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.1 always 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