Skip to content

Resources and Dependency Injection

Prefer Kotlin’s Closeable.use { ... } (and the AutoCloseable variant) over an explicit try { ... } finally { resource.close() } for any object whose lifetime ends at the bottom of the current scope.

use is strictly safer than the equivalent try/finally:

  • If both the body and close() throw, use propagates the body’s exception and attaches the close-time exception as Throwable.addSuppressed(...). The naïve try/finally form throws whichever exception fires last, silently dropping the body’s exception — usually the one the caller actually needs.
  • It is shorter, removes the need for a separate variable, and makes the resource’s lifetime obvious at a glance.
// WRONG — body exception is lost if close() also throws
val migrationDs = HikariDataSource(hc)
try {
DbMigration(fwCfg, migrationDs).migrate().onFailure { throw it }
} finally {
migrationDs.close()
}
// CORRECT
HikariDataSource(hc).use { migrationDs ->
DbMigration(fwCfg, migrationDs).migrate().onFailure { throw it }
}

use is for resources whose lifetime ends with the current block. It does not fit when the resource’s lifetime is conditional — for example, a pool that must outlive the function on the happy path (because it backs a value you return to the caller) but must be closed on the failure path. In that case keep an explicit try { ... } catch { resource.close(); throw }:

val runtimePool = HikariDataSource(hc).also { allCreatedPools.add(it) }
val db = Database.connect(runtimePool)
try {
HikariDataSource(hc).use { migrationDs ->
DbMigration(fwCfg, migrationDs).migrate().onFailure { throw it }
}
} catch (t: Throwable) {
// runtimePool's lifetime is conditional — close it only on the failure
// path, since `db` (and therefore the pool) is returned to the caller on
// success.
allCreatedPools.remove(runtimePool)
runtimePool.close()
throw t
}
return db

Anti-pattern: surrogate close on the wrong type

Section titled “Anti-pattern: surrogate close on the wrong type”

Calling dataSource.connection.close() on a javax.sql.DataSource returns one connection to the pool — it does not close the pool. If the intent is to release the pool, hold a typed handle to the pool implementation (e.g., HikariDataSource) and wrap it in use { ... }.

  1. Inject dependencies, do not construct them internally. Classes should receive their dependencies (services, clients, configuration) as constructor parameters. The wiring point (Module.kt) creates all dependencies and passes them in. Classes should not create their own service instances, SDK clients, or other infrastructure objects.
  2. When a class needs access to an internal component of a dependency (e.g., an S3AsyncClient owned by an S3AssetService), the dependency should expose it as a public property rather than having the consumer construct a separate instance.