Skip to content

application.conf Structure

Every Arda backend Component’s runtime configuration lives in HOCON-format application.conf files. The file layout encodes the functional hierarchy (System → Domain → Module → Service → Endpoint) into the path under which each module’s config is loaded — the lookup path is the module’s canonical identity. This page is the engineering reference: what blocks exist, where each setting goes, how the parent component file and per-Module include files compose, and which fields are derived versus declared.

For the architectural why — what each level of the hierarchy is and what role each plays — see Functional Decomposition. For the Component-side declare/provide pattern, see Component Concept. For the divergences between this canonical shape and legacy modules’ shapes, see Legacy State.

A Component’s configuration is split across two layers of files. Default file-path conventions:

FileDefault locationNotes
Component file<repo>/src/main/resources/application.confStandard Kotlin/Ktor resource path; loaded by ConfigFactory.load() at start-up.
Module file (canonical)<repo>/src/main/resources/<domain>/<module>/application.confMirrors the canonical hierarchy on disk. Domain and module are kebab-case to match the HOCON keys. Modules deviating from this layout must declare an explicit relative include path in the component file.
src/main/resources/
├── application.conf # Component root
├── <domain>/<module>/application.conf # one per canonical Module
└── <domain>/<module>/database/ # Flyway migrations referenced from the Module conf

Following the convention means the include directive reads <domain>/<module>/application.conf — the same string a future build-time lint will assert.

The Component root carries the Component’s identity, a small set of file-root Component-level blocks (Ktor, dataSource pool, AWS, authentication), and two trees of module includes — one for legacy modules under system.* and one for canonical modules under modules.<version>.<domain>.<module>. Both trees coexist long-term; modules migrate from legacy to canonical individually when their owners are ready.

# Component identity — orthogonal to the functional hierarchy.
component {
name = "operations"
version = "v1"
title = "Operations"
description = "Multi-module monolith"
baseUrl = "" # overridden at deployment time
}
# Ktor server config (file-root Component-level block)
ktor {
application { modules: ["cards.arda.operations.runtime.MainKt.module"] }
deployment { port = 8080 }
}
# DB pool config shared across the Component's modules (file-root)
dataSource { pool { … } }
# AWS SDK and authentication infrastructure (file-root)
aws { region = ${?AWS_REGION}; … }
auth { bearer { … }; jwtWeb { … }; jwtM2M { … } }
# Legacy module tree — preserved for modules not yet migrated to canonical.
system {
system { batch { include "system/batch/application.conf" } }
shopAccess { pdfRender { include "shop-access/pdf-render/application.conf" } }
reference { item { include "reference/item/application.conf" }
businessAffiliate { include "reference/business-affiliate/application.conf" } }
resources { facility { include "resources/facility/application.conf" }
station { include "resources/station/application.conf" }
kanban { include "resources/kanban/application.conf" } }
procurement { orders { include "procurement/orders/application.conf" } }
}
# Canonical module tree — URL-order nesting: version → domain → module.
# Identity is derived from each include's lookup path by the platform.
modules {
v1 {
shop-access {
email { include "shop-access/email/application.conf" }
# Future canonical-adopted shop-access modules added here as siblings.
}
# Future domains' canonical modules added here as siblings.
}
# Future versions of any module added as a sibling sub-tree under `modules`.
}

Future direction. The ktor, dataSource, aws, and auth blocks live at the file root today. A future revision will move them under the component { ... } block as component.ktor, component.infrastructure.dataSource, component.infrastructure.aws, and component.authentication. That restructure is deferred because it touches every Module’s deployment shape; the canonical adoption can land per-Module without it.

The functional hierarchy uses collection-named wrappers at three levels rather than placing children directly under their parent:

WrapperSits underChildren
modulesthe file rootone entry per <version>
serviceseach Moduleone entry per <service>
endpointseach Serviceone entry per <endpoint>

Version and domain are path segments, not wrappers: modules.<version>.<domain>.<module> — the version segment groups all modules of the same version, the domain segment groups modules of the same Domain. Multi-version coexistence is supported natively (add another sibling under modules).

The wrappers exist for three reasons:

  1. Disambiguation from sibling configuration at the same level. A Module has Module-level keys (dataSource, secrets, servers, extras) sitting alongside its child Services. Without a services { ... } wrapper, a Service named dataSource would silently shadow (or be shadowed by) the Module-level resource key. The wrapper makes “what is a Service” syntactically unambiguous regardless of canonical name choice. The same logic applies at the Endpoint level.
  2. Future-proofing. Adding a new sibling concern at any level does not risk a name collision with any existing child.
  3. Validator-friendly. Schema validation can declare “every key under services is a Service definition” without a type registry.

A canonical module’s version, domain, and name are derived from the lookup path by the platform — module files declare none of these as fields. Mapping:

Path segmentIdentity fieldExample
modules(constant prefix)
segment 2versionv1
segment 3domainshop-access
segment 4 (last)nameemail

ConfigurationProvider.moduleConfiguration("modules.v1.shop-access.email") derives (version="v1", domain="shop-access", name="email") from the lookup path and passes them into the ModuleConfig constructor. The Module’s own HOCON file declares no identity fields — only its resources, services, and operational tuning.

Six validators run at module load to catch misconfigurations — see § Validation.

The include directive in the parent’s modules.<version>.<domain>.<module> slot pulls in the Module’s file content directly (no outer wrapper). A complete Module file looks like:

# Identity (version, domain, name) is declared by the component at the include
# site in the top-level application.conf. The platform derives the three fields
# from the lookup path's segments and validates them at module load.
# Resource declarations (peers of `extras`)
dataSource {
name = "email_db"
initFile = "shop-access/email/database/init.sql"
flyway {
locations = ["shop-access/email/database/migrations"]
adminTable = "shop_access_email_flyway_history"
}
}
secrets {
postmark-account-token {
source = "ExternalSecret:email-postmark-account-token"
mountPath = "/etc/secrets/email-postmark-account-token/token"
}
encryption-key {
source = "ExternalSecret:email-encryption-key"
mountPath = "/etc/secrets/email-encryption-key/values.json"
}
}
servers {
postmark { protocol = "https"; host = "api.postmarkapp.com"; port = 443; retries = 3 }
}
# Module-specific tuning and bindings that have no standardised platform sub-tree.
extras {
verification { pollingTimeoutHours = 24; pollingIntervalSeconds = 30 }
send { retry { maxAttempts = 3; backoffBaseMs = 500; maxBackoffMs = 30000 }; perAttemptTimeoutSeconds = 30 }
idempotency { replayWindowHours = 24 }
drift { checkIntervalMinutes = 15; postmarkInventory.enabled = false }
logging { recipientAddressLevel = "OFF" }
# External cloud bindings (no standardised sub-tree); resolved from CFN exports at deploy.
aws {
route53 {
hostedZoneId = ${?SYSTEM_SHOPACCESS_EMAIL_AWS_ROUTE53_HOSTED_ZONE_ID}
provisioningRoleArn = ${?SYSTEM_SHOPACCESS_EMAIL_AWS_ROUTE53_PROVISIONING_ROLE_ARN}
}
}
}
# Functional sub-hierarchy: Services → Endpoints.
services {
configuration {
transaction { isolation = "REPEATABLE_READ" }
endpoints {
configuration { authentication { type = "JWT" } }
}
}
job {
transaction { isolation = "REPEATABLE_READ" }
endpoints {
job { authentication { type = "JWT" } }
postmark-events {
authentication { type = "Bearer"; binding { tenant-hash-header = "X-Tenant-Id" } }
policy { never-return-403 = true }
}
}
}
}

After the include is resolved, the Kotlin module-loader reads its slice:

val emailConfig = cfgProvider.moduleConfiguration("modules.v1.shop-access.email")
val schemaName = emailConfig.dataSource.name // "email_db"
val jobAuth = emailConfig.services.job.endpoints.job.authentication.type // "JWT"
val webhookAuth = emailConfig.services.job.endpoints["postmark-events"].authentication.type // "Bearer"

The HOCON path is uniform: every Module is at modules.<version>.<domain>.<module>; every Endpoint is at …services.<service>.endpoints.<endpoint>. The same Kotlin access pattern works for every canonical Module in every Component.

FieldPurposeNotes
dataSourceModule-declared database schemaname is the logical schema name; the Component provides the JDBC URL by composing the file-root dataSource.pool + per-Module schema name. flyway lists the migration locations and the per-Module _flyway_history table. Omitted if the Module has no database state.
secretsLogical secret referencesEach child key is a logical secret name. source references an ExternalSecret resource; mountPath is the in-pod file path the K8s Secret will be mounted at. Omitted if the Module has no secrets.
serversExternal HTTP servers the Module’s L1 proxies callEach child key is a logical server name. Fields (protocol, host, port, baseUrl, retry/timeout overrides) are passed to the Module’s HTTP-client builder. Omitted if the Module makes no external HTTP calls.
extrasModule-specific configuration that doesn’t fit a standardised sub-treeFree-form. Carries operational tuning (retry budgets, polling intervals), per-environment overrides, and Module-specific resource bindings the platform doesn’t have a typed sub-tree for (e.g. AWS Route 53 bindings).
services.<service>One entry per Service the Module ownstransaction { isolation = "..." } configures the Service’s transactional behaviour; future ABAC rules will live here.
services.<service>.endpoints.<endpoint>One entry per Endpoint the Service exposesSee per-Endpoint fields below.
FieldPurposeNotes
authentication.typeThe authentication regime for the EndpointOne of JWT, Bearer, API_KEY. Forward-compat declaration in this revision; Kotlin’s route block still hardcodes the auth plugin. Future EndpointConfigurator abstraction will drive plugin selection from this field.
authentication.bindingAuth-specific binding detailsWebhook-style Endpoints carry the tenant-hash header name here (tenant-hash-header = "X-Tenant-Id").
policyOperational policy invariantsDocumentation declarations of structural endpoint properties (e.g. never-return-403 = true for the Postmark webhook per DQ-013). The invariant is enforced in code; the HOCON declaration enables cross-checking by validators.

Service and endpoint structural identifiers — the segment values like "configuration", "job", "postmark-events" — live in Kotlin code as constructor arguments to EndpointLocator.Rest. Renaming them requires editing routing code anyway, so duplicating them into HOCON adds drift surface without removing it.

Service and endpoint declarative configuration — transaction isolation, authentication type, policy invariants, binding parameters — lives in HOCON when the value is tunable per environment, a forward-compatibility declaration for not-yet-implemented driving logic, or a policy invariant that benefits from cross-checking at startup.

FieldLocationRationale
service segment name (e.g. "configuration")Hard-coded in EndpointLocator.Rest(service = "...")Structural; renaming requires code change.
endpoint segment nameHard-coded in EndpointLocator.Rest(resource = "...")Same.
services.<svc>.transaction.isolationHOCONTunable per env; validated against an enum.
services.<svc>.endpoints.<ep>.authentication.typeHOCONDeclarative-only in v1; forward-compat for the planned EndpointConfigurator abstraction.
services.<svc>.endpoints.<ep>.authentication.binding.*HOCONPer-integration parameter (e.g. header name).
services.<svc>.endpoints.<ep>.policy.* invariantsHard-coded in endpoint implementation; optional HOCON declaration for documentation/validationStructural invariant; HOCON declaration enables a startup validator to cross-check agreement.
extras.* operational tuningHOCONTunable per environment.

Six validators run at module load. Each catches a specific category of misconfiguration:

#ValidatorWhere it runsWhat it catches
1CanonicalSegment regex (^[a-z][a-z0-9]*(-[a-z0-9]+)*$) on ModuleConfig.{name, domain} and EndpointLocator.{name, domain, service, resource/endpoint}init {} of ModuleConfigFromValues, ModuleConfigFromConfig, EndpointLocator.Rest, EndpointLocator.HttpNon-kebab-case values (camelCase, PascalCase, snake_case, MIXED_Case, leading-digit, trailing-hyphen); compound values containing /; blank/empty values
2Version no-slash / no-whitespace guard (^[^/\s]+$)SameAccidental compound version values ("v1/2"); whitespace
3derivedName vs file-declared name cross-checkModuleConfigFromConfig.initA file that declares name = "x" mounted at a map-key y — silent drift between path and file declaration
4derivedDomain vs file-declared domain cross-checkSameSame drift for domain
5derivedVersion vs file-declared version cross-checkSameDrift between the version path segment and any explicit version = "..." in the module file
6HOCON services.<svc>.endpoints.<ep> declarations vs Kotlin-hardcoded EndpointLocator constructionsModule’s Module.kt startup, after all locators are builtAn endpoint declared in HOCON but not constructed in code, or constructed in code but not declared in HOCON — declarative/structural drift; hard-fails module init

The diagnostic message for validator #1 includes a specific hint when a value contains /: “this looks like an apis-loop-injected value; canonical modules load from modules.<version>.<domain>.<module>, not their legacy system.* path.” Catches the most likely Helm-misconfiguration mistake at the right place.

A future build-time lint asserting that include paths in the component file follow the convention <domain>/<module>/application.conf is deferred.

Helm apis: and databases: companion entries

Section titled “Helm apis: and databases: companion entries”

The Helm apis: and databases: flat maps in values.yaml are preserved under the legacy system.<bucket>.<module> key convention — both for legacy modules (no change) and for canonical-adopted modules (key stays legacy, value’s name field changes).

The apis-key (the map key under apis:) stays at system.<bucket>.<module> for every module, including canonical-adopted ones. The apis-key drives:

  • Nginx ingress path rendering via the module.path helper (reads only the entry’s version and name values, not the key).
  • ConfigMap HOCON-property injection at <key>.name and <key>.version paths.
  • NOTES.txt display label.

For a canonical module, the apis-key MUST NOT collide with the module’s canonical HOCON load path under modules.<version>.<domain>.<module>. Because the legacy convention sits at the system.* root and the canonical tree sits at the modules.* root, non-collision is automatic.

name value differs between legacy and canonical

Section titled “name value differs between legacy and canonical”
apis:
# Legacy modules — name is a single segment
system.shopAccess.pdfRender: { name: "pdf-render", version: "v1", enabled: true }
system.reference.item: { name: "item", version: "v1", enabled: true }
system.reference.businessAffiliate: { name: "business-affiliate", version: "v1", enabled: true }
system.resources.kanban: { name: "kanban", version: "v1", enabled: true }
system.procurement.orders: { name: "order", version: "v1", enabled: true }
# Canonical modules — name carries the compound <domain>/<module>
system.shopAccess.email: { name: "shop-access/email", version: "v1", enabled: true }

The module.path helper renders "/${version}/${name}". For a canonical module the rendered ingress path is /v1/shop-access/email, matching the Module’s canonical URL prefix.

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

The compound <domain>/<module> in apis.<key>.name duplicates information the module’s HOCON identity (path-derived domain + name) already carries. The duplication is bounded — one field per module, never read across surfaces. A startup validator asserts that the rendered URL prefix /${apis.<key>.version}/${apis.<key>.name} equals ModuleConfig.rootPath.

databases:
system.shopAccess.email: { cfn_key_suffix: "EmailDb", name: "email_db", secretKey: "email" }
# … other modules unchanged …

Map key keeps the system.<bucket>.<module> convention for both legacy and canonical modules. The name value ("email_db") duplicates the module HOCON’s dataSource.name; same bounded-duplication policy as apis:.

  • All keys are canonical — kebab-case lowercase. No camelCase, no PascalCase, no snake_case in HOCON keys for canonical modules. (Legacy modules’ keys keep camelCase for backward compatibility.) Compound names use a single hyphen.
  • Module identity is never declared in the module fileversion, domain, and name are all derived from the lookup path by the platform.
  • include directives carry only the path string — no other settings on the include site.
  • Per-Module include files have no outer wrapper. Their content is the inside of the modules.<version>.<domain>.<module> block in the parent.
  • Per-environment overrides live in helm/config/<env>/application.conf and use the canonical path (modules.<version>.<domain>.<module>.<sub-key>) for canonical modules. Legacy modules keep their system.* overlay paths.
  • The system { ... } and modules { ... } trees coexist — they are not migration phases of each other. Both are stable long-term shapes; modules migrate from system.* to modules.* individually when their owners are ready.
  • Functional Decomposition — The hierarchy this file structure materializes, the canonical naming rules, the URL formula, and the URL collapse rule.
  • Module Concept — Module-side view of the declare/provide pattern; encapsulation; interaction mechanisms.
  • Component Concept — Component identity, the configuration footprint, the orthogonality rule.
  • Legacy State — Divergences between the canonical layout and the live components today; per-Module migration steps.
  • Ktor Module Wiring (Implementation Patterns) — How Application.{module}(...) consumes the resolved HOCON sub-tree to wire its Services.