Skip to content

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 #k7m2qx Payments {
aspect team: "Payments"
rest_create authorize
}

#k7m2qx is the stable ID. The module has it; the interface Authorize doesn’t. This chapter explains both decisions.

Rule. An ID must be meaningless and permanent. Meaningless because anything you encode — a domain, a type, an abbreviation — eventually goes wrong and tempts you to “fix” it. Permanent because the whole point of an ID is to survive every rename, re-scope, and move untouched. Never change an ID. That #k7m2qx could just as well read #q4w8zr — the opacity is the feature.

Carries #idDoesn’t carry #id
ModulesSurfaces
TypesInterfaces
Processes
Views
Subprocesses
Policies
Queries

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 #k7m2qx,” 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.

#a3f2b7e
#k7m2qx
#q4w8zr
  • # immediately followed by an opaque alphanumeric suffix.
  • Suffix length is unconstrained; conventionally 4–8 characters.
  • Purely technical — no encoded name, no encoded type, 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. (#pay001 or #orderschk would be anti-patterns — they smuggle a domain into the identity.)

Prefer letting the tool mint IDs. Truly-random suffixes are hard to produce by hand, so the formatter mints one on first save:

// You write:
service Payments {
aspect team: "Payments"
}
// After saving, the formatter writes:
service #k7m2qx Payments {
aspect team: "Payments"
}

The exact suffix is implementation-chosen; treat it as random.

If you must hand-write an ID — autocomplete unavailable, sketching offline — randomness by hand is genuinely hard, so it’s acceptable to fall back to a filename-based seed, with the filename as the main piece:

service #payments-1 Payments { ... } // <filename>-<index>
service #payments-9f3 Settlements { ... } // <filename>-<random-postfix>

This will look stale as a name once the module moves to a different file — and that’s fine. The ID is meaningless and never changes regardless of where the module lives; stale-looking is acceptable, mutating it is not. (If you can find a better way to get hand randomness, use it — the filename seed is a fallback, not the ideal.)

The ID slot is between the type and the name:

service #k7m2qx Payments { ... }
process #h2k9p4 Checkout { ... }
view #z7q3w PaymentsLandscape { ... }
type #m4d8c module service { ... }

For type declarations the slot is between type and the parent type.

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.

Rule. Everything in a real architecture carries an ID. Modules, interfaces, types, processes, views — the whole durable model. Optional IDs are a convenience for quick drafts and sketches only. A useful test: when you propose changes, things should have IDs. If it’s part of the model people review and evolve, it needs an identity that survives renames and moves. Skip IDs only while sketching.

Why surfaces and interfaces don’t carry IDs

Section titled “Why surfaces and interfaces don’t carry IDs”

If surfaces and interfaces had IDs, the visual would look like:

service #k7m2qx Payments {
surface #f823n PaymentsResource {
rest_create #cmd9c0 Authorize
rest_create #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 type 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 + rest_create NewName { ... }) in a single commit can mask continuity. The mitigation: rename in small steps.

Type bodies can stamp pre-filled submodules onto every instance:

type module service {
component metrics { rest_create 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 metrics ID 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.

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.

  • Diffs see renames. Chapter 14 covers this: a module with the same #id and a different name is a rename, not a delete-plus-add. Stable IDs are what make change first-class — a refactor reads as renames and moves, not “everything deleted and re-added.”
  • Cross-references stay stable. [[#k7m2qx]] in a description keeps working after Payments becomes PaymentsService.
  • Tooling URLs stay stable. External systems that link to a specific module (<viewer>?focus=#k7m2qx) survive every rename of the module.
  • IDs must be meaningless and permanent — opaque alphanumeric suffixes that never change and encode no domain, type, or semantics.
  • Modules, types, processes, views, subprocesses, policies, and queries carry IDs.
  • Surfaces and interfaces don’t — their identity is the dot-path inside a stable parent module.
  • Prefer the formatter to mint random IDs on save; the hand fallback is a filename-based seed (<filename>-<index>), which is fine because the ID is meaningless regardless of where the module ends up.
  • Everything in a durable model carries an ID; skip them only while sketching. When you propose changes, things should have IDs.
  • Auto-propagated submodules get deterministically derived IDs.
  • The trade-off: some heuristic imperfection on interface renames in exchange for clean source.

Chapter 14: Diffs → — change as a first-class artifact, built on the ID system from this chapter.