Dependency Injection (DI)
Compile-time dependency injection — a macwire port built on Hearth's
enclosingScope. wire and friends inspect the enclosing lexical scope at compile time and generate the
constructor calls for you: no reflection, no runtime container, and — unlike macwire — the exact same code
cross-compiles to JVM, Scala.js and Scala Native on both Scala 2.13 and Scala 3.
Installation
The com.softwaremill.common::tagging library is pulled in
transitively so the @@ tag types are available out of the box.
Quick start
Each wire[A] resolves every parameter of A's primary constructor from a value of a compatible type found in the
enclosing class/object, and emits new A(...):
import hearth.kindlings.di.DI
class DatabaseAccess()
class SecurityFilter()
class UserFinder(databaseAccess: DatabaseAccess, securityFilter: SecurityFilter)
class UserModule {
lazy val databaseAccess = new DatabaseAccess()
lazy val securityFilter = new SecurityFilter()
lazy val userFinder: UserFinder = DI.wire[UserFinder] // = new UserFinder(databaseAccess, securityFilter)
}
Prefer the bare wire (the macwire spelling)? Import the package object — both spellings are equivalent:
import hearth.kindlings.di._
class UserModule {
lazy val databaseAccess = new DatabaseAccess()
lazy val securityFilter = new SecurityFilter()
lazy val userFinder: UserFinder = wire[UserFinder]
}
Resolution rules
wire builds an A by, in order:
- resolving each parameter of
A's public primary constructor from the enclosing scope; - if the primary constructor is inaccessible, falling back to a single matching companion
apply; - resolving implicit parameters through normal implicit search rather than from the scope.
Candidates are gathered from:
- the members (declared and inherited) of the enclosing classes/objects — so module composition by trait inheritance works;
- the public, parameterless members of any in-scope value whose type is annotated
@Module; and - (Scala 2 only)
vals local to the enclosing method declared before the call site.
When the same type is available in more than one scope, the nearest (innermost) scope wins; an ambiguity is an error only when a single scope offers more than one candidate.
Composition by inheritance
trait DatabaseModule {
lazy val databaseAccess: DatabaseAccess = new DatabaseAccess()
lazy val securityFilter: SecurityFilter = new SecurityFilter()
}
class AppModule extends DatabaseModule {
lazy val userFinder: UserFinder = DI.wire[UserFinder] // inherited members are candidates
}
wireRec — recursive wiring
wireRec[A] is like wire, but any dependency not found in scope is itself constructed recursively (refusing to
fabricate java.*/scala.* library types):
class Service(databaseAccess: DatabaseAccess)
class Handler(databaseAccess: DatabaseAccess)
class App(service: Service, handler: Handler)
class RecursiveModule {
lazy val databaseAccess = new DatabaseAccess()
lazy val app: App = DI.wireRec[App] // Service and Handler are built for you from databaseAccess
}
wireWith — factory wiring
wireWith resolves the parameters of a factory function (a lambda or an eta-expanded method/constructor reference)
from the scope and applies it. Useful when construction is not a plain constructor call:
def makeUserFinder(db: DatabaseAccess, sf: SecurityFilter): UserFinder = new UserFinder(db, sf)
class FactoryModule {
lazy val databaseAccess = new DatabaseAccess()
lazy val securityFilter = new SecurityFilter()
lazy val userFinder: UserFinder = DI.wireWith(makeUserFinder _)
// or: DI.wireWith((db: DatabaseAccess, sf: SecurityFilter) => new UserFinder(db, sf))
}
Factory arities 0–22 are supported.
wireSet / wireList
Collect every value of a type found in scope:
class Plugin
class PluginModule {
val a, b, c: Plugin = new Plugin
lazy val plugins: Set[Plugin] = DI.wireSet[Plugin] // Set(a, b, c)
lazy val pluginList: List[Plugin] = DI.wireList[Plugin] // List(a, b, c) — declaration order preserved
}
autowire — explicit object graphs
autowire[A](dependencies*) builds a complete object graph for A from an explicit list of dependencies —
instances and/or factory functions — constructing everything else via public constructors / companion applys. Each
distinct type is instantiated exactly once and shared through a generated local val:
val db = new DatabaseAccess()
val sf = new SecurityFilter()
val finder: UserFinder = DI.autowire[UserFinder](db, sf)
// factory functions are accepted too:
val finder2 = DI.autowire[UserFinder](db, sf, (d: DatabaseAccess, s: SecurityFilter) => new UserFinder(d, s))
autowire reports, at compile time:
- cyclic dependencies,
- attempts to wire a primitive or
String, and - any provided dependency that ended up unused.
Tagging
Disambiguate two values of the same base type with tags — the @@
type combinator. Resolution falls out of subtyping, so no extra wiring annotations are needed:
import com.softwaremill.tagging._
trait Primary
trait Replica
class Database(url: String)
class Connector(primary: Database @@ Primary, replica: Database @@ Replica)
class TaggedModule {
val primaryDb: Database @@ Primary = new Database("primary").taggedWith[Primary]
val replicaDb: Database @@ Replica = new Database("replica").taggedWith[Replica]
lazy val connector: Connector = DI.wire[Connector] // each tag is matched independently
}
Modules
Annotate a type with @Module to let wire reach into a value of that type for its public, parameterless members —
useful for composing applications out of pre-built modules passed as constructor arguments:
import hearth.kindlings.di.Module
@Module
class DataModule {
lazy val databaseAccess: DatabaseAccess = new DatabaseAccess()
lazy val securityFilter: SecurityFilter = new SecurityFilter()
}
class AppModule(dataModule: DataModule) {
// databaseAccess / securityFilter are pulled out of dataModule because DataModule is a @Module
lazy val userFinder: UserFinder = DI.wire[UserFinder]
}
DI.plan — recursive, cached, customizable wiring (Kindlings' own)
wire / wireRec / autowire follow macwire's conventions. DI.plan[A] is Kindlings' own opinionated
endpoint — it is always recursive and always caches (each distinct type is built once and shared), and it lets
you customize how the graph is assembled without touching your domain classes. Finish the builder chain with
.build:
import hearth.kindlings.di.DI
class DatabaseAccess()
class UserFinder(databaseAccess: DatabaseAccess)
class App(userFinder: UserFinder)
object Main {
// builds the whole graph from public constructors: App -> UserFinder -> DatabaseAccess
val app: App = DI.plan[App].build
}
Storage strategy
By default every constructed dependency is stored as a val. Change the whole-graph default, or override it per type:
import hearth.kindlings.di.DI
class DatabaseAccess()
class UserFinder(databaseAccess: DatabaseAccess)
class App(userFinder: UserFinder)
object Main {
val app: App =
DI.plan[App]
.asLazyVals // whole-graph default: val | asLazyVals | asDefs
.storeAsDef[DatabaseAccess] // per-type override: storeAsVal | storeAsLazyVal | storeAsDef
.build
}
asVals(default) /storeAsVal[T]— created once, eagerly.asLazyVals/storeAsLazyVal[T]— created once, on first use.asDefs/storeAsDef[T]— re-created on every use (so adef-stored dependency is not shared).
Construction overrides
Tell plan to initialise a specific type with your own factory instead of calling its constructor — "when you need a
T, use this":
import hearth.kindlings.di.DI
class DatabaseAccess()
class UserFinder(databaseAccess: DatabaseAccess)
class App(userFinder: UserFinder)
object Main {
val app: App =
DI.plan[App]
.provide[DatabaseAccess](new DatabaseAccess()) // by-name factory, evaluated per the type's storage
.build
}
Seeing how things are wired — the wiring graph
Inspired by ZIO Magic / ZIO 2.0's automatic layer wiring (ZLayer.Debug.tree /
ZLayer.Debug.mermaid), every DI endpoint can print how the entities/resources combine, as a DAG-aware ASCII tree or a
Mermaid diagram. For DI.plan request it inline with .debugTree / .debugMermaid:
import hearth.kindlings.di.DI
class DatabaseAccess()
class RecService(databaseAccess: DatabaseAccess)
class RecHandler(databaseAccess: DatabaseAccess)
class RecApp(service: RecService, handler: RecHandler)
object Main {
val app: RecApp = DI.plan[RecApp].debugTree.build
}
prints (at compile time) the shared DatabaseAccess once, referencing it under the second consumer:
RecApp
├─ RecService [val]
│ ╰─ DatabaseAccess [val]
╰─ RecHandler [val]
╰─ DatabaseAccess ↻ (shared, wired above)
For any endpoint (including wire/autowire), enable the graph project-wide with a scalac option — tree for the
ASCII view, mermaid for the Mermaid diagram + a render link:
To also see the full generation log (the resolution logic and the generated code), enable
di.logGeneration — see Debugging & Diagnostics.
Differences from macwire
DI.planis not macwire.wire/wireRec/autowiremirror macwire;DI.planis Kindlings' own opinionated endpoint (always recursive, always caching, customizable storage + construction overrides).- Cross-platform. macwire's request/session scopes rely on JVM bytecode proxies; this module targets JVM, Scala.js and Scala Native, so those JVM-only proxy scopes are intentionally not ported.
- Enclosing-method parameters / locals. Local
vals declared before the call site are discovered on Scala 2; enclosing-method parameters are not currently exposed cross-platform by the underlying macro API. - Only the immediately-enclosing class exposes a usable
this; members of outer enclosing classes are not reachable (enclosing objects always are, since they have a stable path).