Legacy State
The Functional Decomposition page is the canonical statement of how Arda names its functional elements and structures every configuration surface. The live system today partially diverges from that statement. This page documents those divergences so engineers can navigate the existing code base without mistaking what they see for what should be, and describes the evolution mechanics — cardinality transitions and the collapse-rule shadowing risk — that govern how Modules migrate from the legacy state to the canonical one.
For the snapshot of actual URLs published today, see the API Endpoint Catalog. The Email module URLs in that catalogue are in the canonical shape (/v1/shop-access/...) because the Email module was implemented under the canonical naming rules from day one; every other operations Module is still at the pre-canonical shape, and every accounts-component Module is at a pre-canonical shape that elides additional levels.
Divergences
Section titled “Divergences”Domain elision in URLs
Section titled “Domain elision in URLs”Every URL outside the Email module is at the shape /v1/{module}/... — the Domain segment is absent. The canonical formula is /v1/{domain}/{module}/.... The elision is safe today only because Module canonical names happen to be unique across the System; if two Modules in different Domains came to share a canonical name, the elision would be ambiguous and the Domain segment would become load-bearing.
Affected: every operations Module except Email; every accounts-component Module.
HOCON depth divergence between components
Section titled “HOCON depth divergence between components”The two backend components configure the same conceptual content at different HOCON tree depths:
- The
operationscomponent wraps Module configs atsystem.{domain}.{module}— preserving the System and Domain levels. Thesystem.system.batchpath doubles the tokensystembecause theSystemDomain is nested under the System-rootsystem { ... }block. - The
accounts-componentincludes Module configs at top-level paths:tenant,userAccount,agentFor,invitation— nosystem.*wrapper and no Domain layer.
The canonical HOCON shape has every Module at modules.<version>.<domain>.<module> regardless of Component. Both current legacy shapes (operations’ system.<bucket>.<module> and accounts’ flat root paths) coexist with the canonical tree long-term — modules migrate from the legacy tree to the canonical tree individually when their owners are ready. The two trees are stable side-by-side; no Component needs to migrate atomically.
Component-level HOCON restructure (deferred)
Section titled “Component-level HOCON restructure (deferred)”Today the Component’s ktor, dataSource.pool, aws, and auth blocks live at the file root, alongside (not inside) the component { ... } identity block. A future revision moves them under component { ... } as component.ktor, component.infrastructure.dataSource, component.infrastructure.aws, and component.authentication. The restructure is deferred because it would touch every Module’s deployment shape; canonical module adoption can land per-Module without it.
Cross-Module Endpoint surfacing
Section titled “Cross-Module Endpoint surfacing”Some operations are mounted under a consumer Module’s URL prefix rather than under the owning Module’s prefix:
/v1/item/upload-job/{job-id}and/v1/item/upload-job/upload-urlmount Batch entity operations under the Item Module’s URL./v1/item/lookup-suppliersmounts a BusinessAffiliate query under the Item Module’s URL.
The canonical hierarchy requires each operation to live under its owning Module’s Endpoint. Migration moves these operations to their canonical homes (/v1/system/batch/job/... and /v1/reference/business-affiliate/lookup-suppliers) with a deprecation window during which both URL shapes serve identically.
Naming-style irregularities
Section titled “Naming-style irregularities”Several surface-level naming styles predate the canonical rules and remain inconsistent in the live system:
- Pluralisation has no consistent rule today.
procurement/orders(Kotlin, plural) but/v1/order/...(URL, singular).reference/businessaffiliates(Kotlin, plural) butsystem.reference.businessAffiliate(HOCON, singular). The canonical rule is one singular kebab-case name per functional element; pluralisation drift disappears at migration. - HOCON keys use camelCase for compound names (
pdfRender,businessAffiliate,shopAccess) while URLs use kebab-case (pdf-render,business-affiliate,shop-access). The canonical rule is kebab-case lowercase everywhere. databases:entries carry three different case conventions in a single entry (cfn_key_suffix: "OrderDb"PascalCase,name: "order_db"snake_case,secretKey: "order"lowercase). Migration consolidates these to derive from a single canonical with predictable case-conversion rules per surface.OrderHomeEndpointis the only*Endpoint.ktclass with aHomequalifier. Canonical naming strips the qualifier.PdfRenderhas configuration as if it were a public Module — a Helmapis:entry, an ingress path — butregistry.registerInternal(...)in itsModule.ktand noEndpoint.kt. It is functionally in-pod only; the Helmapis:entry’s published URL does not resolve to a live REST handler. Migration either promotes it to a real Endpoint or removes the misleadingapis:entry.
OAM cross-cutting URL prefix
Section titled “OAM cross-cutting URL prefix”/operations/oam/{proxy+} and /accounts/oam/{proxy+} use the Component name as a URL prefix. Components are orthogonal to the functional hierarchy and do not appear in any functional URL. These routes are Component-level operational concerns, not functional Endpoints, and are deliberately kept outside the canonical formula.
Cardinality and the collapse rule under evolution
Section titled “Cardinality and the collapse rule under evolution”The canonical URL formula applies the collapse rule (adjacent identical canonicals in the M / S / E chain merge) to keep URLs short in the common case where a Module is a pure DataAuthority. The collapse has consequences for how a Module evolves — specifically, when a Module gains new Services or Endpoints over time, the collapse must be safe against shadowing the existing Endpoint’s internal routes.
Cardinality
Section titled “Cardinality”A Module’s per-Endpoint chain is M / S / E (Module / Service / Endpoint canonicals). The chain can take one of four cardinality shapes:
| Shape | Meaning | Example chain | Collapsed URL |
|---|---|---|---|
(x/x/x) | 1 : 1 : 1 — M = S = E canonical | item / item / item | /v1/reference/item |
(x/x)/e | 1 : 1 : n — M = S, E distinct | (no current example) | /v1/{domain}/{module=service}/{endpoint} |
m/(x/x) | 1 : n : 1 — M distinct, S = E | email / job / job | /v1/shop-access/email/job |
m/s/e | 1 : n : n — all three distinct | email / job / postmark-events | /v1/shop-access/email/job/postmark-events |
Cardinality transitions
Section titled “Cardinality transitions”Adding a new Service or Endpoint to an existing Module changes the cardinality. The collapse rule operates per chain, so the existing URLs are preserved across these transitions — the new entity’s URL slots in at a deeper path while existing Endpoints keep their mounts.
| Transition | What is added | New URL added | Existing URLs |
|---|---|---|---|
| 1 : 1 : 1 → 1 : 1 : n | New Endpoint added to the existing Service | /v1/{domain}/{module}/{new-endpoint} | unchanged |
| 1 : 1 : 1 → 1 : n : 1 | New Service (with one Endpoint) added to the Module | /v1/{domain}/{module}/{new-service} | unchanged |
| 1 : 1 : n → 1 : n : n | New Service added to a Module that already has multiple Endpoints | /v1/{domain}/{module}/{new-service} (or deeper if the new Service has non-collapsing Endpoints) | unchanged |
| 1 : n : n → 1 : n : n | New Endpoint or Service added to an already-distinct Module | path determined by the new chain’s collapse | unchanged |
In every transition, each existing Endpoint’s chain is evaluated against the collapse rule independently of the new chain; the existing URL keeps its shape.
Collapse-rule shadowing risk
Section titled “Collapse-rule shadowing risk”The collapse rule’s URL-shortening comes at a price: it narrows the namespace available below the collapsed segment. A collapsed Endpoint at /v1/{domain}/{module} mounts its internal operations (the {routes+} sub-tree) directly under that prefix. If a new sibling Service or Endpoint is later added with a canonical that collides with one of those internal-route first segments, the sibling’s mount shadows the internal route.
Worked example. The Item Endpoint mounts at /v1/reference/item ((x/x/x)); internally it serves operations at /{eid}, /query, /query/{page}, /rid/{eid}, /print-label, /lookup-suppliers, …
If a new Service with canonical query is later added to the Item Module, its mount would be /v1/reference/item/query — colliding with the existing /query internal route. The Module’s main URL /v1/reference/item survives, but the existing query operation becomes unreachable from outside.
API Gateway, K8s Ingress, and Ktor all resolve the path conflict by longest-match precedence — the new mount wins; the legacy operation is shadowed. None of the three detects the conflict at configuration time without an additional check.
Mitigation — the reserved-token registry
Section titled “Mitigation — the reserved-token registry”Every Endpoint that benefits from collapse declares the list of internal-route first segments it uses. The validator in common-module reads services.{service}.endpoints.{endpoint}.reserved-internal-tokens at start-up and rejects any sibling Service/Endpoint canonical that matches a token in any peer Endpoint’s list. The conflict is detected at deploy time rather than at runtime.
Example, for the Item Module’s canonical Endpoint:
endpoints { item { authentication { type = "JWT" } reserved-internal-tokens = [ "query", "rid", "print-label", "print-breadcrumb", "lookup-suppliers", "lookup-units", "upload-job" ] }}The list is maintained by the Endpoint’s owner alongside the Endpoint’s contract; its drift is detectable at PR review (a new route added in Kotlin code without the corresponding token in the registry will cause the next build’s validator to fail).
The registry is the architectural answer that makes the collapse rule safe under Module evolution. Every collapsed Endpoint in the system must carry one.
Per-Module migration
Section titled “Per-Module migration”The seven legacy modules in operations and four in accounts-component migrate to canonical individually. Each module’s migration is a small per-module change with no cross-module coupling. The Email module is the pilot; subsequent modules migrate when their owners are ready.
Per-module migration steps:
- Module HOCON file — remove any
name = "...",domain = "...", orversion = "..."declarations (the path-derivation now provides all three). Promotesecrets/servicespeer blocks if needed. Move declarative authentication / transaction config into aservices.<svc>.endpoints.<ep>sub-tree. - Top-level component HOCON — relocate the module’s include from
system.<bucket>.<module>(legacy) tomodules.<version>.<domain>.<module>under the canonical tree. Thesystem.*parent path stays for any modules that have not yet migrated. If a<version> { ... }block already exists undermodulesfrom another canonical module, add this module’s sub-tree under it; otherwise create the version + domain wrappers. - Component
Main.kt— update the module’s wiring to callcfgProvider.moduleConfiguration("modules.<version>.<domain>.<module>"). Use the canonicalEndpointLocator.Rest(...)secondary constructor for each endpoint with explicitserviceandresourcesegments. values.yamlapis-entry — keep the apis-key (system.<bucket>.<module>) unchanged. Change thenamevalue from the single-segment form ("kanban") to the compound form ("resources/kanban").- CFN routes — rename logical IDs to
{Domain}{Module}{Service}{Endpoint}Route; updateRouteKeystrings to canonical paths. - Per-env Helm overlays — rewrite the module’s override keys from
system.<bucket>.<module>.*tomodules.<version>.<domain>.<module>.*. - Tests — update test fixtures that hardcode the legacy HOCON path.
No module has to migrate in lockstep with any other. Mixed legacy + canonical state is stable; the legacy modules continue to deploy and route correctly through the transition.
Compatibility for client-facing URL changes
Section titled “Compatibility for client-facing URL changes”When a Module’s URL prefix changes during migration (e.g. /v1/item/... → /v1/reference/item/...), the migration is additive before subtractive:
- Add the canonical URL alongside the legacy URL. Both serve identically. CFN registers both routes targeting the same in-pod handler; Ingress mounts both paths; Ktor registers both
route(...)blocks pointing at the same Endpoint. - Mark the legacy URL deprecated in the OpenAPI specification; emit
DeprecationandSunsetresponse headers communicating the timeline. - After observability confirms the legacy URL has zero (or pre-agreed minimal) traffic, remove it.
When a sibling Service or Endpoint addition would otherwise be blocked by the reserved-token registry (the new canonical collides with an existing reserved token), the dual-mount pattern is an alternative: publish the existing Endpoint at both the collapsed URL and the equivalent uncollapsed URL (/v1/<domain>/<module>/<module> in addition to /v1/<domain>/<module>). Clients of the existing Endpoint then have an unshadowed recovery path. The dual mount doubles the URL footprint per Endpoint and is reserved for cases where the reserved-token registry would otherwise block a high-value addition.
Related Pages
Section titled “Related Pages”- Functional Decomposition — Canonical statement of the naming rules.
- API Endpoint Catalog — Snapshot of URLs published today.
- Module Concept — Module-level deep dive.
Copyright: © Arda Systems 2025-2026, All rights reserved