In Ktor, how do you read a custom value from application.conf inside a module function?
answer
- Config hangs off the application environment
- One method throws, its sibling returns null
- Sub-config narrows a component's view
- Read at startup, not per request
basics
~10 sGo through the application environment's config object: from an Application receiver, call environment.config.property("path.to.key").getString(), or propertyOrNull for an optional value. The config is the parsed application.conf tree, so custom keys live alongside Ktor's own.
solid answer
~40 sInside `fun Application.module()` the whole parsed configuration is reachable as `environment.config`, an `ApplicationConfig`. You address values by dotted path: `environment.config.property("app.db.url").getString()` throws if the key is absent, while `propertyOrNull("app.db.url")?.getString()` gives you an optional. Lists come back through `getList()`, and `config("app.db")` returns a nested `ApplicationConfig` you can pass to a component so it only sees its own subtree. Custom keys are ordinary HOCON — put them outside the reserved `ktor { }` block, e.g. under an `app { }` root, so they never collide with Ktor's own `ktor.deployment` and `ktor.application` settings. The same object is present whether the server was started by EngineMain or by embeddedServer, so configuration-reading code does not care which entry point booted it.
code
kotlin · 8 linesfun Application.module() {
val db = environment.config.config("app.db")
val url = db.property("url").getString()
val poolSize = db.propertyOrNull("maxPoolSize")?.getString()?.toInt() ?: 10
val origins = environment.config.property("app.allowedOrigins").getList()
log.info("connecting to {} with pool {} for {} origins", url, poolSize, origins.size)
}go deeper
Recall that the parsed application.conf is reachable from an Application receiver via the environment's config, and that values are addressed by dotted path.
Explain the throwing versus nullable accessors, list and sub-config access, and why custom keys belong outside the ktor namespace.
Show the startup-time mapping into typed settings and argue it on operability grounds: configuration errors must fail the deploy, not the hundredth request.
Set the house rule for how configuration is layered — file shape committed, values injected from the environment, secrets never literal — so every service is configured the same way.
## Where configuration lives Ktor parses `application.conf` (HOCON) — or `application.yaml` when the YAML config artifact is on the classpath — into a tree and exposes it through the application environment. Every module function has `Application` as its receiver, and from there `environment.config` is an `ApplicationConfig`: a small, engine-independent read interface over that tree. HOCON is the Typesafe Config format. It is JSON-superset syntax with unquoted keys, comments, includes, and substitutions, so a config looks like this: ``` ktor { deployment { port = 8080 } application { modules = [ com.example.ApplicationKt.module ] } } app { db { url = "jdbc:postgresql://localhost:5432/app" maxPoolSize = 10 } allowedOrigins = [ "https://example.com", "https://admin.example.com" ] } ``` The `ktor` root is reserved: Ktor itself reads `ktor.deployment.port`, `ktor.deployment.host`, `ktor.application.modules` and a handful of others from it. Everything else in the file is yours, which is why the convention is to open a separate root such as `app`. ## The reading API `ApplicationConfig` has a deliberately tiny surface: - `property(path)` returns an `ApplicationConfigValue` and **throws** if the path is missing. Use it for values the service cannot run without — failing loudly at startup is the point. - `propertyOrNull(path)` returns null instead, for genuinely optional settings with a code-side default. - On the value, `getString()` yields the raw string and `getList()` yields a list of strings. HOCON is untyped at this interface, so numbers and booleans are converted by you (`.getString().toInt()`). - `config(path)` returns a **sub-config** rooted at that path, and `configList(path)` returns a list of sub-configs for an array of objects. That sub-config method is the one people underuse. Instead of threading the whole configuration into a database factory, hand it `environment.config.config("app.db")` so the component addresses `url` and `maxPoolSize` relative to its own subtree. It keeps the key layout a local decision and makes the component trivially testable with a hand-built config. ## Fail fast, and read once The healthy pattern is to read configuration **at module time**, map it into a Kotlin data class, and pass that around: ``` data class DbSettings(val url: String, val maxPoolSize: Int) fun Application.dbSettings(): DbSettings { val c = environment.config.config("app.db") return DbSettings( url = c.property("url").getString(), maxPoolSize = c.property("maxPoolSize").getString().toInt(), ) } ``` Two benefits. First, a missing or unparseable key blows up during startup rather than on the first request that happens to need it — a service that starts and then 500s an hour later is much worse to operate than one that refuses to start. Second, request handlers stop touching string paths, so a rename is a compiler problem instead of a runtime surprise. ## Anti-patterns worth naming **Reading config inside a route handler.** It works, but it moves a startup failure into request time and repeats the lookup on every call. **Reaching for `System.getenv` directly all over the code.** HOCON already has substitution — `url = ${?DATABASE_URL}` overrides the value from the environment when the variable is set — so the environment can be a *source* for the config tree rather than a parallel channel that bypasses it. **Putting custom keys under `ktor`.** Nothing stops you, but you are squatting in a namespace whose meaning is defined by the framework and can grow. **Assuming the config is typed.** `ApplicationConfig` deals in strings and lists of strings; there is no automatic binding of a whole block into a data class the way some other frameworks do it. Writing that mapping yourself is normal Ktor practice. ## Test angle Because the interface is small, tests do not need a file at all: they can start the module with a configuration built in memory, or override individual keys, and assert that a missing required key makes startup fail. That is the cheapest possible regression test for a config contract.
- Why prefer property() over propertyOrNull() for a database URL?Because a missing database URL is not a case the service can meaningfully continue past. `property()` throws during module initialization, so the process fails at startup where a deploy pipeline notices it. `propertyOrNull()` invites a silent default that turns a configuration mistake into a runtime outage later.
- How do you keep a secret out of application.conf while still reading it through the config?Leave the key in the file bound to a substitution such as `apiKey = ${?API_KEY}`, so the file carries the shape and the environment carries the value. The code still reads one path through `environment.config`, and nothing sensitive is committed.
- What does config("app.db") give you that property("app.db.url") does not?A narrowed `ApplicationConfig` rooted at `app.db`, so a component reads `url` without knowing its absolute path. That decouples the component from the file's key layout and lets a test construct a small config containing only that block.
saying these in an interview costs you the question
- Says Ktor binds a config block into a data class automatically
- Reads configuration inside every request handler
- Uses propertyOrNull everywhere and defaults required values silently
- Puts application-specific keys under the reserved ktor block
- Thinks getString is the only accessor and lists are unsupported