Skip to content

Functional Decomposition

The Functional Viewpoint decomposes the system into the elements that together provide its overall capabilities. Each element occupies a specific level in a containment hierarchy, has a defined architectural role, and contributes to a single canonical naming scheme that propagates across every configuration surface — HOCON, Helm, CloudFormation, and Ktor — and into the public URLs the system exposes.

This page is the full statement of the Functional Viewpoint. The top-level catalogue of actual Domains and Modules lives in the Functional Overview. The Module-level deep dive lives in Module Concept. Divergences between this canonical specification and the live system, together with the mechanics that govern Module evolution, are documented in Legacy State.

System → Domain → Module → Service → Endpoint

Each level has a specific architectural role. The roles determine where each level surfaces in configuration, code, and URLs.

The top of the hierarchy — the complete set of capabilities the product provides. The Arda system itself. The System level is implicit in any one configuration scope; it never appears as an explicit token in surfaces because every surface is “inside the System” by construction.

The canonical name of the System is arda.

A collection of capabilities closely related from a functional or dependency point of view. The Domain layer is primarily a naming and grouping unit — it supports the decomposition of the overall System into understandable pieces but does not enforce policy, transactions, or authorization on its own. Domains group Modules; that is their role.

The Domains present in the Arda system today are procurement, reference, resources, shop-access, and system. The catalogue of Domains and the Modules they contain is in the Functional Overview.

The Module declares the resources it requires and encapsulates its state. It is the minimum unit of independent versioning and deployment. A Module’s contract version travels with every Service and Endpoint it exposes.

Key Module properties:

  • Declares its required resources — a database schema, a set of secrets, external HTTP servers, in-memory registries — using logical names. These resources are provided by the Runtime Component that packages the Module.
  • Encapsulates a portion of System state, accessible only through the Module’s Services.
  • Owns its own version. The Module’s HOCON configuration carries an explicit version field; that field is the version of every contract the Module exposes.
  • May host one or more Services; in practice many Modules host exactly one.

The full Module discussion — cohesion, encapsulation, interaction mechanisms, cross-Module references, shared libraries — is in Module Concept.

A Service is the entry point for execution and the boundary of a transaction. Operations that must share a transaction belong to the same Service. A Service may own multiple entities when those entities must be mutated atomically.

Future-facing: the Service is the unit at which fine-grained authorization (attribute-based access control, ABAC) is enforced.

A specific kind of Service is the DataAuthority — a Service providing bitemporal CRUDQ over an Entity. The DataAuthority’s standard four-layer structure (Protocol Adaptor → Service → Persistence → Proxy) is described in the Data Authority Module Pattern. Most Modules have a DataAuthority as their primary Service, but Services need not be DataAuthorities — operational services, rendering services, and event-ingest services are all valid non-DataAuthority Services.

An Endpoint is the protocol-bound surface that exposes a Service’s operations. Each Endpoint defines:

  • The wire protocol (REST over HTTP, gRPC, webhook ingest, etc.).
  • The authentication mechanism (JWT, Bearer, API key, …).
  • The address space — the URL prefix at which the Endpoint mounts.

An Endpoint groups multiple operations — a sub-tree of HTTP routes for a REST Endpoint, not a single operation. A Service may have more than one Endpoint when its callers come through distinct protocol or authentication regimes. The Email module is the current example: its EmailJobService exposes both a Job Endpoint (in-perimeter JWT-authenticated REST) and a PostmarkEvents Endpoint (Postmark webhook with Bearer-plus-tenant-hash authentication). Both Endpoints invoke the same Service.

Future-facing: the Endpoint may carry coarse-grained authorization in addition to its authentication mechanism — an Endpoint may forbid an entire authentication regime from accessing operations the Service otherwise allows.

The Runtime Component is the deployable unit that packages one or more Modules and provisions or binds the resources those Modules declare. The Component is orthogonal to the functional hierarchy — it never appears in URLs, OpenAPI titles, or any other name a Module exposes. Moving a Module between Components is a re-packaging operation; it does not change any functional name.

A Component has its own substantial configuration footprint:

  • Identity: name, version, image, container registry.
  • Compute: CPU, memory, replicas, HPA, JVM options.
  • Networking: ingress class, gateway base URL.
  • Shared infrastructure: database server URI, AWS region, ServiceAccount, IRSA role.
  • Authentication infrastructure: JWT issuers, client IDs.
  • Observability: Sentry DSN, log level.
  • The bindings that satisfy the Modules’ resource declarations: the database connection (from the Component’s URI plus each Module’s declared schema name); the ExternalSecret mounts (from the Component’s secret-store config plus each Module’s declared secret names); the HTTP clients (from the Component’s defaults plus each Module’s declared server URLs).

The split between what a Module declares and what a Component provides is an inversion-of-control pattern: the Module’s code only ever consumes logical names (cfg.dataSource.name, cfg.secrets.X, cfg.servers.Y); the Helm template at deploy time and the Kotlin wiring at start-up time combine those declarations with the Component’s provisions to produce bound K8s/AWS resources and bound in-pod objects.

Every functional element has a single canonical name: kebab-case, lowercase, derived from the element’s role. The canonical is the authoritative identifier — every other name a surface produces (Kotlin class name, HOCON key, Helm value, CFN logical ID, URL slug) is derived from the canonical mechanically.

Naming rules:

  • All canonicals are kebab-case lowercase. business-affiliate, not businessAffiliate or BusinessAffiliate. Compound names use a single hyphen separator.
  • Service canonical equals the Entity name for DataAuthority Services. The Service is named after the Entity it manages: OrderService (Kotlin) → canonical order; KanbanCardService → canonical kanban-card; EmailJobService → canonical job (with the Module-name prefix stripped for namespace hygiene within the Module).
  • Service canonical equals the Module canonical for non-DataAuthority Services. A Service without a managed entity — a processing service, a rendering service, an event-ingest service — inherits the Module’s canonical name.
  • DataAuthority Endpoint canonical equals the Service canonical equals the Entity canonical. The Endpoint that exposes a DataAuthority Service is named after the entity.
  • Non-DataAuthority Endpoint canonicals are role-driven. A webhook Endpoint that ingests Postmark events is canonical postmark-events, regardless of which Service it ultimately invokes.

The hierarchical relationship between canonicals is fixed: every Endpoint’s chain is (domain, module, service, endpoint) with each canonical drawn from the rules above.

The full canonical URL of a REST route, derived top-down from the functional hierarchy, is:

/v{module-version}/{domain}/{module}/{service}/{endpoint}/{routes+}

Components:

  • v{module-version} — the version of the Module that owns the Endpoint. Every URL contains exactly one Module, so the leading version segment is unambiguous. Currently every Module is at v1.
  • {domain}, {module}, {service}, {endpoint} — the canonical tokens of the levels.
  • {routes+} — the in-Endpoint operations sub-tree (the per-operation routes the Endpoint registers below its mount). The shape of {routes+} is the Endpoint’s own contract and is documented in the Endpoint’s OpenAPI specification.

The functional hierarchy is preserved explicitly in every configuration surface. The URL surface — and only the URL surface — applies a collapse rule for readability: adjacent identical canonicals in the Module / Service / Endpoint chain merge into a single segment.

This yields four chain shapes that a URL can take after collapse. Notation: m = Module, s = Service, e = Endpoint; parentheses group canonicals that share a single segment.

Chain shapeCardinality (M : S : E)Example chainExample URL
(x/x/x)1 : 1 : 1 — all three coincideitem / item / item/v1/reference/item
m/(x/x)M distinct, S = Eemail / job / job/v1/shop-access/email/job
(x/x)/eM = S, distinct E(no current example)/v1/{domain}/{module=service}/{endpoint}
m/s/eall three distinctemail / job / postmark-events/v1/shop-access/email/job/postmark-events

The collapse rule applies in URL strings wherever they appear — API Gateway route keys, Helm-rendered ingress paths, Ktor route mount paths, OpenAPI servers/paths, the API Endpoint Catalog. It does not apply in HOCON paths, Helm value-file keys, or CFN logical IDs.

Each configuration surface keys its content at the level its consumer cares about, using the canonical names. Configuration surfaces never collapse adjacent identical canonicals — each level appears explicitly because each level may carry its own config shape.

The Component’s top-level application.conf carries the Component identity in a component { ... } block and the functional hierarchy in a modules { ... } tree. The functional tree is URL-ordered: modules.<version>.<domain>.<module>. Version and Domain are path segments, not collection wrappers; the only wrappers in the functional tree are modules at the top, then services and endpoints inside each Module.

Module identity (version, domain, name) is derived from the lookup path by the platform — module files declare no identity fields. ConfigurationProvider.moduleConfiguration("modules.v1.shop-access.email") reads the path and produces (version="v1", domain="shop-access", name="email") automatically.

Each Module’s content includes dataSource, secrets, servers, optional extras (operational tuning and Module-specific bindings that the platform doesn’t model with a typed sub-tree), and a services.<service>.endpoints.<endpoint> sub-tree whose entries declare per-Endpoint authentication and policy.

A minimal sketch:

component {
name = "operations"
version = "v1"
title = "Operations"
description = "..."
baseUrl = ""
}
modules {
v1 {
shop-access {
email { # included from shop-access/email/application.conf
dataSource { name = "email_db"; flyway { ... } }
services {
job {
endpoints {
job { authentication { type = "JWT" } }
postmark-events { authentication { type = "Bearer" } ; policy { never-return-403 = true } }
}
}
}
}
}
}
}

The full file-layout reference — every Component-level field, every per-Module field, the per-Endpoint fields, the include directive convention, the path-derived identity mechanism, and the deploy-time validators — lives in application.conf Structure. That pattern doc is the authoritative reference for engineers writing or reading application.conf files.

The Component-level ktor, dataSource.pool, aws, and auth blocks live at the file root today (not inside component { ... }). A future revision moves them under the component block as component.ktor, component.infrastructure.*, and component.authentication; that restructure is deferred because it touches every Module’s deployment shape.

The Helm values file keeps two flat maps that the existing chart template iterates: apis: and databases:. Both use the legacy key convention system.<bucket>.<module> for every module — legacy and canonical alike. Canonical adoption changes the value of apis.<key>.name, not the key.

  • Legacy modules: name is a single segment ("item", "kanban", "order", …); the helper module.path = /{version}/{name} renders /v1/<module>.
  • Canonical modules: name is the compound <domain>/<module> (e.g. "shop-access/email"); the helper renders /v1/shop-access/email — the canonical URL prefix.
apis:
system.reference.item: { name: "item", version: "v1", enabled: true } # legacy
system.resources.kanban: { name: "kanban", version: "v1", enabled: true } # legacy
system.shopAccess.email: { name: "shop-access/email", version: "v1", enabled: true } # canonical
databases:
system.shopAccess.email: { cfn_key_suffix: "EmailDb", name: "email_db", secretKey: "email" }

A canonical Module that exposes multiple Endpoints (Email with configuration, job, postmark-events) carries one apis: entry — Helm renders a single ingress at the Module’s prefix; per-Endpoint route splitting happens at the API Gateway level via separate AWS::ApiGatewayV2::Route resources.

A future Helm-template rewrite that walks a canonical modules.<version>.<domain>.<module> tree directly is tracked but deferred; under the current Helm pipeline the compound name is the bridge from canonical Module identity to rendered ingress path. The compound value is a bounded duplication of the path-derived <domain>/<module> — never read across surfaces; a startup validator asserts agreement with ModuleConfig.rootPath.

CFN provisions cloud resources. One stack per Component. The stack contains two kinds of resources:

  • Component-level resources — provisioned once for the whole Component, used by every Module it packages: the IAM role for the Component’s pod, the API Gateway integration, the ExternalSecrets SecretStore, the per-Component RDS server (hosting one schema per Module that declares a database), and the per-Component Secrets Manager entries.
  • Per-Module / per-Endpoint resources — one CFN AWS::ApiGatewayV2::Route per Endpoint that declares ingress; one DB schema per Module that declares a database; one ExternalSecret per Module-declared secret; per-Module IAM policy statements added to the Component’s role.

CFN logical IDs follow the canonical hierarchy:

  • Per-Endpoint route: {Domain}{Module}{Service}{Endpoint}Route (PascalCase concatenation; hyphens dropped).
  • Per-Module database: {Domain}{Module}Db.
  • Component-level resources: Component{Concern} (no hierarchy prefix).

Route key paths use the canonical URL with {proxy+} appended:

EmailJobPostmarkEventsRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
RouteKey: 'ANY /v1/shop-access/email/job/postmark-events/{proxy+}'
AuthorizationType: NONE # in-pod Bearer + tenant-hash verification
...

AuthorizationType and AuthorizerId are derived from the Endpoint’s HOCON authentication.type. JWT-authenticated Endpoints attach the partition’s JWT authorizer at the gateway; in-pod authentication regimes (Bearer, API key) use AuthorizationType: NONE at the gateway and verify in-pod.

Each Endpoint mounts at its full canonical URL via the in-Module Application.{module}(...) entry function. A typical mount uses the EndpointLocator helper from common-module to derive the URL string from the canonical tuple:

fun Application.email(cfg: ModuleConfig, locator: EndpointLocator, ...) {
routing {
route(locator.urlFor(Domain.SHOP_ACCESS, Module.EMAIL, Service.CONFIGURATION, Endpoint.CONFIGURATION)) {
// → /v1/shop-access/email/configuration
configurationEndpoint.install(this)
}
route(locator.urlFor(Domain.SHOP_ACCESS, Module.EMAIL, Service.JOB, Endpoint.JOB)) {
// → /v1/shop-access/email/job
jobEndpoint.install(this)
}
route(locator.urlFor(Domain.SHOP_ACCESS, Module.EMAIL, Service.JOB, Endpoint.POSTMARK_EVENTS)) {
// → /v1/shop-access/email/job/postmark-events
postmarkEventsEndpoint.install(this)
}
}
}

The EndpointLocator is the single place the canonical URL formula is implemented in Kotlin. The URL collapse rule is applied inside it; route blocks pass the four canonicals and receive the mount URL.

  • Module Concept — Module-level deep dive: cohesion, state encapsulation, interaction mechanisms.
  • Data Authority Module Pattern — Four-layer pattern for DataAuthority Services.
  • Functional Overview — Catalogue of actual Domains and Modules in the live system.
  • API Endpoint Catalog — Snapshot of URLs published today, including the canonical Email URLs and the pre-canonical URLs of other operations Modules.
  • Legacy State — Divergences between this canonical specification and the live system; cardinality-transition and collapse-rule mechanics that govern Module evolution.
  • Viewpoint Mapping — Cross-reference from functional concepts to source code, artifacts, and runtime resources.