Skip to main content

Zero-Magic Dependency Injection

I don't use reflection-heavy DI containers like Spring, Guice, or Dagger. Instead, I use pure Kotlin constructors wired explicitly in a single provider file (Apis.kt).


How I Wire Dependencies (Apis.kt)​

fun Application.apis() {
val db: R2dbcDatabase by dependencies

val geocoding: Geocoding = Nominatim()
val events = Events()

// Explicit Constructor Injection
val items = Items(db, geocoding)
val users = Users(db, geocoding)
val borrows = Borrows(db)
val updateBorrowStatus = UpdateBorrowStatus(borrows, conversations, items, users, events)

// Expose Singletons to Ktor Routes
dependencies {
provide { items }
provide { users }
provide { borrows }
provide { updateBorrowStatus }
}
}
Spring vs. Pure DI

While Spring's @Autowired or @Component annotations are convenient, they hide the dependency graph until runtime. If you miss an annotation, the app crashes on startup. With pure constructor DI, if it compiles, the object graph is guaranteed to work!


Why Pure Constructor DI?​

  1. Deterministic Startup: No runtime class-scanning errors or missing bean exceptions. If it compiles, the object graph is guaranteed to work.
  2. Instant Code Navigation: Clicking a dependency in the IDE takes me directly to its constructor call site.
  3. Frictionless Testing: Unit tests instantiate services directly (Items(mockDb, mockGeocoding)) without starting heavy framework containers.