13. Stable IDs
Names rot. A Payments service becomes PaymentsService. An Orders.CreateOrder command becomes Orders.PlaceOrder. A whole subsystem gets renamed when the team renames itself. In any system where the source files are the canonical model, you need an identity that survives renames — otherwise every rename looks, to the diff tools, like a delete plus an add.
Archlang’s answer is stable IDs: opaque alphanumeric suffixes that anchor identity independent of names.
service #pay001 Payments { team: Payments command Authorize}#pay001 is the stable ID. The module has it; the interface Authorize doesn’t. This chapter explains both decisions.
What carries an ID
Carries #id | Doesn’t carry #id |
|---|---|
| Modules | Facets |
| Types | Interfaces |
| Processes | |
| Views | |
| Subprocesses |
The rule: anything that can be renamed independently of its container. Modules can be renamed; their identity needs anchoring. Interfaces can be renamed too, but they live inside a module — their identity is “the Authorize interface inside #pay001,” a path that stays stable as long as the parent module and the interface name both do. When an interface itself is renamed, the diff engine uses heuristics on contract shape and name similarity.
This is a trade-off. IDs on every interface would buy perfect rename detection at the cost of permanent visual clutter in source files (every interface in every module would carry an opaque prefix). The current allocation puts IDs where they earn their keep.
What IDs are
#pay001#a3f2b7e#orderschk#immediately followed by an opaque alphanumeric suffix.- Suffix length is unconstrained; conventionally 4–8 characters.
- Purely technical — no encoded name, no encoded kind, no human-readable stem.
The “no embedded meaning” is the entire point. Encoding anything into the ID — even “this looks like a payments ID” — undermines stability: rename the system from Payments to Settlements and the embedded hint goes stale immediately. IDs are opaque so they have nothing to rot.
You don’t write IDs by hand. The formatter mints them on first save:
// You write:service Payments { team: Payments}
// After saving, the formatter writes:service #pay001 Payments { team: Payments}The exact suffix is implementation-chosen; treat it as random.
Where IDs go in a declaration
The ID slot is between the kind and the name:
service #pay001 Payments { ... }process #flow42 Checkout { ... }view #v9 PaymentsLandscape { ... }type #t001 module service { ... }For type declarations the slot is between type and the parent kind.
Omit the slot and the formatter fills it on save. Including a hand-typed ID for a brand-new declaration is allowed; including one for a declaration the formatter didn’t mint is fine too. Removing an ID after the formatter wrote one is a bad idea — every diff that compares before/after will fail to match the renamed module against its prior self.
Why facets and interfaces don’t carry IDs
If facets and interfaces had IDs, the visual would look like:
service #pay001 Payments { facet #f823 PaymentsResource { command #cmd9c0 Authorize command #cmdbb2 Capture }}Every line gains an opaque prefix. For a service with twenty interfaces, that’s twenty extra tokens of visual noise per service.
In return you’d get: trivial rename detection for interfaces. The current heuristic-based rename detection isn’t perfect — if you rename an interface and change its kind and its description in the same commit, the diff might call it delete-plus-add. That trade-off — accept some heuristic imperfection in exchange for vastly cleaner source — is the deliberate choice.
If your team renames interfaces constantly, you may find this annoying. Doing both at once (drop OldName + command NewName { ... }) in a single commit can mask continuity. The mitigation: rename in small steps.
Auto-propagated submodules
Type bodies can stamp pre-filled submodules onto every instance:
type module service { component metrics { command Emit } // every service instance gets a 'metrics' component}The metrics component appears in every service; the question is what stable ID it gets. The answer: deterministically derived, not freshly minted.
The formula: hash(parent_module_id + type_id + declared_subname), truncated to the standard suffix length. This makes the ID:
- Stable across re-saves — the same instance produces the same
metricsID on every format. - Identical across instances that derive from the same template — except for the per-instance differentiation introduced by
parent_module_id. - Cross-reference-friendly —
[[#derived-id]]to a type-supplied submodule resolves the same way every time.
You won’t see this in source. The formatter mints these IDs but they sit on the resolved nodes, not in your .arch files.
Scoping
IDs are scoped per workspace. Collisions within a workspace are recommended-against — the formatter mints to avoid them and tools warn on detection — but not fatal. Duplicate IDs resolve in declaration order and tooling flags the conflict.
When libraries and external packages arrive (a future iteration), each package will own its own ID namespace; cross-package references will qualify by package.
What IDs enable
- Diffs see renames. Chapter 14 covers this: a module with the same
#idand a different name is a rename, not a delete-plus-add. - Cross-references stay stable.
[[#pay001]]in a description keeps working afterPaymentsbecomesPaymentsService. - Tooling URLs stay stable. External systems that link to a specific module (
<viewer>?focus=#pay001) survive every rename of the module.
Summary
- IDs are opaque alphanumeric suffixes anchoring identity through renames.
- Modules, types, processes, views, and subprocesses carry IDs.
- Facets and interfaces don’t — their identity is the dot-path inside a stable parent module.
- The formatter mints IDs on save; don’t write them by hand.
- Auto-propagated submodules get deterministically derived IDs.
- The trade-off: some heuristic imperfection on interface renames in exchange for clean source.
What’s next
Chapter 14: Diffs → — change as a first-class artifact, built on the ID system from this chapter.