skip to content

In Ktor, how do you read a custom value from application.conf inside a module function?

level: middleimportance: should knowfreq 58%

answer

  1. Config hangs off the application environment
  2. One method throws, its sibling returns null
  3. Sub-config narrows a component's view
  4. Read at startup, not per request

basics

~10 s

Go 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 s

Inside `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 lines
kotlin
fun 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

for a junior

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.

for a middle

Explain the throwing versus nullable accessors, list and sub-config access, and why custom keys belong outside the ktor namespace.

for a senior

Show the startup-time mapping into typed settings and argue it on operability grounds: configuration errors must fail the deploy, not the hundredth request.

for a principal

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

context