Files
alkcall/docs/architecture/decisions/032-one-way-door-decision-framework.md
glm-5.2 cc470a363a docs: port architecture specs + 45 ADRs from alknet, renumbered
Port the call + channels architecture documentation from the alknet
mono-repo into docs/architecture/, renumbered as alkcall ADR-001..045.

Renumbering map (alknet -> alkcall):
  Core:        001,002,004,006,007,011,065,070,092,014,050,091 -> 001-012
  Call:        005,064,012,023,015,022,024,016,049,017,028,029,030,032,066,069,067,068 -> 013-030
  Shared:      003,009,013 -> 031-033
  Channels:    071,093,072,073,074,075,076,094,079,080,081,089 -> 034-045

3 superseded/reversed ADRs kept for historical trail:
  - ADR-013 (irpc foundation, superseded by ADR-014)
  - ADR-023 (peer-scoped filtering, superseded by ADR-024)
  - ADR-077 (TTY inside channels, reversed by ADR-035 — not ported, TTY-only)

Ported docs (11 spec files + README + open-questions):
  - call-README.md, call-protocol.md, operation-registry.md, client-and-adapters.md
  - channels-README.md, channels-overview.md, channels-wire.md, channels-connection.md, channels-adapter.md, channel-operations.md, channel-client.md
  - README.md (index with doc table, ADR table grouped by category, key principles)
  - open-questions.md (lean — 30 OQs, renumbered OQ-01..030; includes new OQ-22 for the pub/sub gap)

Cross-reference rewriting:
  - All ADR-NNN references rewritten single-pass (no chaining bug)
  - Markdown link paths fixed
  - Title lines aligned with filenames
  - Non-ported ADR refs (052, 082, 086, etc.) left as-is with README note

The open-questions.md includes OQ-22 (new): the call protocol pub/sub
gap — subscribe exists but pub does not, needed for channels
channel/resources/subscribe fan-out. This is the next ADR to write
(alkcall ADR-046).
2026-08-12 07:06:57 +00:00

5.6 KiB

ADR-032: One-Way Door Decision Framework

Status

Accepted

Context

Not all architectural decisions carry the same reversal cost. Some decisions are easy to change later — if you pick the wrong data structure, you refactor. Other decisions are nearly impossible to reverse — if you build a type hierarchy that forecloses WASM compatibility, every handler written against that hierarchy must be rewritten.

This distinction matters especially during Phase 0 (exploration) and early Phase 1 (architecture). The project is post-pivot with foundational ADRs in place but no implementation code yet (except alknet-vault). Decisions made now shape the API surface that every handler depends on.

Without an explicit framework, one-way doors can be treated as casually as two-way doors, leading to costly rework. Or conversely, two-way doors can be over-analyzed, blocking progress on decisions that are cheap to reverse.

Decision

Classification

Every architectural decision is classified by reversal cost — how expensive it is to undo if you got it wrong:

One-way door — Reversing this decision requires rewriting significant code across multiple crates or permanently closes a capability door. Getting it wrong is expensive. Examples:

  • BiStream as a concrete quinn type (closes WASM door permanently)
  • alknet-vault pulled into alknet-core as a dependency (loses standalone property permanently)
  • ProtocolHandler signature changes (every handler must be rewritten)

Two-way door — Reversing this decision is cheap or additive. Getting it wrong is recoverable. Examples:

  • Static vs dynamic handler registration (can add ArcSwap later)
  • Single transport vs multi-transport endpoint (can add transport trait later)
  • Call protocol stream model (can add multiplexing later)

Process

  • One-way doors require an ADR before implementation. If the right choice is unclear, validate with a POC before writing the ADR. If a POC can't resolve the uncertainty within a reasonable timebox, default to the option that keeps more doors open. One-way doors get the deliberation they deserve because getting them wrong is expensive.
  • Two-way doors still require a decision — pick the simplest option that works, implement it, and move on. If it turns out wrong, revert and try the alternative. The decision is made; what's cheap is the reversal. Note the decision in a commit message or a brief ADR if the context is worth capturing, but don't block on it.
  • When in doubt about which classification applies, classify up. If it's unclear whether a door is one-way or two-way, treat it as one-way until proven otherwise.

What this framework is NOT

This framework classifies decisions by reversal cost, not by urgency. It does not say "two-way doors can be deferred." A two-way door is a decision you make now and can revert later if needed — it's not a license to leave the decision unmade.

  • Deferral is a separate concept: sometimes a decision genuinely doesn't need to be made yet because the use case isn't concrete (scope management). That's valid, but it's a scoping judgment, not a door-type classification.
  • Conflating the two — using "it's a two-way door" as a reason to defer an architectural decision — leads to decisions that compound into a mess. The decision gets made by default (the implementation picks something), and downstream code builds on it, making the "cheap reversal" expensive.
  • The architect's role: architecture decisions (one-way OR two-way) are for the architect to make, not the implementation agent. The implementation agent makes implementation decisions (variable names, loop order, which library to use for a parsed task). If a decision affects the system's structure, constraints, or API surface, it's an architecture decision regardless of its door type.

WASM as a design constraint

WASM compatibility is not an immediate implementation goal, but it is a design constraint on one-way doors. Decisions that would permanently prevent WASM targets from participating as peers require explicit justification. This means:

  • Core types (BiStream, ProtocolHandler, AuthContext) must not assume tokio or quinn
  • Protocol parsers that are pure data transformations should remain transport-agnostic
  • The cost of keeping the WASM door open is low (trait vs concrete type, abstracted I/O) and the cost of closing it is high (impossible to reverse without rewriting every handler)

This is not "WASM support now." It's "don't close the WASM door accidentally."

Consequences

Positive:

  • One-way doors get the deliberation they deserve — ADRs, POCs, explicit justification — because getting them wrong is expensive
  • Two-way doors don't block progress — decide, implement, revert if needed — because getting them wrong is recoverable
  • WASM compatibility is preserved as a constraint, not treated as an active deliverable
  • The framework creates a shared vocabulary for discussing reversal cost ("is this a one-way door?")

Negative:

  • Classification requires judgment — some decisions are genuinely ambiguous (mitigated: classify up when in doubt)
  • POC timeboxing can feel constraining on genuine hard problems (mitigated: the timebox is "reasonable," not "arbitrary")
  • The framework adds a step to every architectural discussion ("is this one-way or two-way?") — but this step is fast and prevents expensive mistakes

References

  • ADR-005: BiStream type definition (one-way door: WASM compatibility)
  • ADR-008: Secret service integration point (one-way door: standalone crate independence)
  • SDD process: docs/sdd_process.md (Phase 0 exploration, POC specialist)
  • Pivot proposal: docs/research/pivot/alpn-service-architecture.md