3. The 15 design principles
August 14, 2026 · View on GitHub
Each principle: the rule, the one-sentence reason (mapped to a Cordis concept), and the anti-pattern.
1 · Registrations are reversible effects — register returns a disposer / 注册即效果
Rule: every contribution goes through ctx.effect() / ctx.on(); register() returns a disposer.
Why: the runtime accumulates inverses and replays them LIFO on unload (revertible effects).
Anti-pattern: side effects performed outside the context, or a register() whose return value you drop.
2 · Reify shared state as a service key, never ambient globals / 共享状态物化为协作用键
Rule: any state shared across plugins lives behind a ctx.<key> service.
Why: everything a plugin touches must flow through the context to be tracked and reverted.
Anti-pattern: a module-level mutable variable that two plugins read/write — it leaks on unload and breaks isolation.
3 · Declare dependencies via inject, never look them up optimistically / 依赖走 inject
Rule: required services go in the plugin's inject array; the loader runs apply only when they exist.
Why: satisfaction σ ⊨ d decides activation; optimistic lookups reintroduce null checks and crashes.
Anti-pattern: a service locator pattern (getBean-style) with a null check at every call site.
4 · Supply an inverse per atomic effect; the inverse must actually revert / 每个原子副作用配一个逆
Rule: ctx.effect(() => { acquire; return cleanup }) — the cleanup must undo the acquire.
Why: composite inverses are derived by composition, but the runtime does not verify each inverse.
Anti-pattern: a cleanup that doesn't fully undo (a leaked timer, an unclosed connection, a listener left behind).
5 · Keep related teardown in one effect / 相关拆除放同一个 effect
Rule: if teardown order matters, do all the related registration in a single ctx.effect.
Why: inverses compose in reverse order (twisted composition), so one effect unwinds in the right sequence.
Anti-pattern: splitting a setup into several effects whose disposal order you then have to reason about by hand.
6 · Prefer commutative set-valued coeffects; reserve ordered chains / 集合型可交换协作用优先
Rule: make shared registries "sets of independently-addable entries" (tools, listeners); use ordered chains only for order-sensitive logic. Why: commutative keys make plugins' effects independent, so they can be reverted out-of-order and interleaved safely. Anti-pattern: a middleware chain where every plugin must be inserted at a precise position and can't be removed independently.
7 · Use a service broker for multi-provider seams / 多 provider 走 service broker
Rule: when several providers implement one interface, inject a broker as the entrypoint and dispatch through it. Why: the broker absorbs provider swaps (no reload), enabling load balancing, rolling updates, cross-process calls. Anti-pattern: exclusive binding where switching implementations unloads/reloads every consumer.
8 · Encapsulate capabilities as seams / 能力封装成 seam
Rule: a capability = Service Definition + Providers + Consumers; consumers depend on the interface, not the impl. Why: a seam is a coeffect key + value type + operation set — providers stay swappable, unloading one drops it from the routing set. Anti-pattern: consumers importing a concrete provider module directly.
9 · Emission crosses the boundary — withhold or compensate / 发射不可逆
Rule: acquiring a resource (open/connect/subscribe) is reversible; emitting (writing bytes, sending packets, calling an external API) is not. Withhold until committed, or compensate (saga-style). Why: the system boundary splits tracked acquisition from untracked emission. Anti-pattern: a tool that sends a message or writes to a shared file assuming the runtime can "undo" it on unload.
10 · Long-lived state lives in a dependency, not the plugin / 跨重载状态放更长命协作用
Rule: state that must survive an HMR/reload goes into a longer-lived service. Why: reload reverts the old fiber's effects and reapplies from a clean slate; plugin closure state doesn't survive. Anti-pattern: caching in a plugin-local variable and expecting it to survive a hot reload.
11 · Avoid dependency cycles: they cause permanent inactivity / 依赖环=永久失活
Rule: two plugins must not each declare a key the other provides. Why: mutual satisfaction can never hold — both stay inactive forever (predictable, but it won't self-heal). Anti-pattern: A depends on B's key and B depends on A's key; decompose into unidirectional cores + an integration plugin.
12 · Declarative config via cordis.yml + !!js, never !js / 声明式配置走 cordis.yml
Rule: describe composition declaratively; !!js marks an expression (config + disabled), overlays select plugins by environment.
Why: the loader reconciles per-field incrementally; confluence guarantees the quiescent state depends only on the final config.
Anti-pattern: !js (single-bang) — it is not the expression tag and silently mis-parses.
13 · Consume declared deps via ctx.<key>; ctx.get() is for optional lookups / 消费依赖走 ctx.<key>
Rule: access services you injected via the proxy (ctx.<key>); for optional services use ctx.get('name') (returns undefined).
Why: the proxy enforces your inject declaration at the point of use (throws UNDECLARED_ACCESS); ctx.get is a safe flat lookup.
Anti-pattern: using ctx.<key> for an un-declared (optional) service — the proxy is topology-sensitive and throws.
14 · Isolate with realms; govern with interception / realm 隔离、interception 治理
Rule: use isolation realms for multi-tenant/test/sandbox contexts; use interception metadata for policy. Why: the same key can resolve differently per realm; interception is right-biased so an orchestrator can constrain access without touching provider/consumer code. Anti-pattern: forking provider code just to give two consumers different values for the same key.
15 · Use typed events with declared dispatch modes / 类型化事件+声明派发模式
Rule: event names go through TypeScript declaration merging; each event declares its dispatch mode (@mode).
Why: the dispatch mode is part of the public contract; use events for interception/policy and service methods for direct capability calls.
Anti-pattern: a hand-rolled pub/sub with stringly-typed topics and no declared mode.
← Prev: The three plugin shapes · Contents · Next: Debugging and verification workflow →