Inara

Compile-time dependency injection for Kotlin Multiplatform: your modules are just interfaces.

Inara turns a plain Kotlin interface into a dependency-injection module. You declare what a module offers as ordinary interface members, and you decide how each member is produced by supplying a Binding when you build the module. A KSP processor generates the implementation.

@DIModule
interface NetworkModule {
    val baseUrl: String
    fun httpClient(): HttpClient
}

val network = NetworkModule( (1)
    baseUrl = Binding.value("https://api.example.com"),
    httpClient = Binding.single { HttpClient(baseUrl) }, (2)
)

network.httpClient() (3)
1 A builder function generated by Inara, named after the interface.
2 A binding body sees the module as its receiver, so it can read any other member.
3 Consumers call a normal method on a normal interface.

No reflection. No service locator. No runtime graph. Just an interface, a generated implementation, and a compiler that tells you when a dependency is missing.

Why Inara

Your module is its own API

A @DIModule interface is the contract consumers depend on. There is no separate "component" type, no qualifier annotation, and no get<T>() lookup.

Opinionated, by design

There is no global context and no mutable context. Business code stays independent from the dependency-injection layer: it has no notion that Inara even exists. @DIFactory takes that further still, building classes that do not import a single Inara type.

Wiring is a value, not a configuration language

A module instance is built by calling a generated function with one Binding per member. Bindings are ordinary values that you can pass around, store, and swap.

Every dependency is checked by the compiler

Forget a binding, and the builder call does not compile. There is no runtime "unresolved dependency" failure.

suspend members are native

Mark a member suspend and it is bound with a SuspendBinding whose bodies can suspend. Asynchronous construction propagates the way it should, instead of hiding behind runBlocking.

Overriding is first-class

Every module gets a generated copy(…​) function, so building a test variant means overriding one member and keeping the rest.

Multiplatform, without surprises

Inara behaves exactly the same on every platform it supports. The only exception is JS and Wasm, where thread synchronization is a no-op, since there is no thread to synchronize.

How to read this documentation

The documentation starts simple and builds up:

  • Getting started gets a first module running in a few minutes.

  • The Modules section explains @DIModule from the ground up: declaring a module, choosing bindings, then inheritance and composition.

  • The Factories section explains @DIFactory, which builds your own classes from a module.

  • The Guides section shows Inara in context: testing, scopes, multi-module projects, and concurrency.