Skip to content
Kona

Container

The container (org.konaframework.core.Container) is a registry of how to create the objects your application needs. Instead of constructing services by hand and threading them through every constructor in your app, you tell the container once how a type is made (a binding), and then ask the container for that type wherever you need it (resolving). The container also builds ordinary classes on its own by reading their constructor, which is called autowiring.

Every Application owns one container, available as app.container in Kotlin and app.getContainer() in Java. Bindings are normally added in a provider’s register method and resolved in boot or at request time.

Internally, every binding is stored under a string key. The class-based methods use the type’s fully qualified class name as the key, so bind(Greeter::class.java, ...) and bind("com.example.Greeter", ...) refer to the same entry. Most code should use classes; string keys are useful for named services such as "app" and "config", which the application registers for you.

A factory is a function that receives the container and returns an instance. Because it receives the container, a factory can resolve the service’s own dependencies.

val c = app.container
// A new instance on every resolve
c.bind(ReportBuilder::class.java) { ReportBuilder() }
// One shared instance, created on first resolve
c.singleton(Clock::class.java) { SystemClock() }
// A factory that pulls in another service
c.singleton(Mailer::class.java) { container -> SmtpMailer(container.make(Clock::class.java)) }
// An interface bound to an implementation class, which the container autowires
c.singleton(UserRepository::class.java, SqlUserRepository::class.java)
// An object you already have
c.instance(AppSettings::class.java, AppSettings(region = "eu"))
// A string key
c.singleton("greeter") { Greeter("Hello") }
Method Lifetime Notes
bind(type, factory) new instance per resolve bind(type, factory, true) makes it shared
bind(type, implClass) new instance per resolve implClass is autowired; optional third argument shared
singleton(type, factory) one shared instance Same as bind(..., shared = true)
singleton(type, implClass) one shared instance implClass is autowired once
instance(type, obj) the given object Registered immediately; no factory
bind / singleton / instance with a String key as above For named services

Factories are lazy: nothing is built at bind time, only on first resolve. Binding the same key again replaces the earlier binding and discards any shared instance already built for it. If a singleton’s factory throws, nothing is cached and the next resolve tries again.

make and resolve are two names for the same operation, on both Container and Application.

val clock = app.resolve(Clock::class.java)
val mailer = app.container.make(Mailer::class.java)
val greeter: Greeter = app.container.make("greeter")
val known = app.container.bound(Clock::class.java) // true

What happens depends on how you ask:

  • By class: if the class (or an alias of it) is bound, the binding is used. If not, the container autowires a fresh instance (described below). Autowired instances are not cached; each resolve builds a new one. Bind the class as a singleton if you want sharing.
  • By string key: the binding must exist. There is no autowiring for string keys; an unknown key throws BindingException: No binding for [key].

bound(key) / bound(type) tells you whether an explicit binding exists, without building anything.

When asked for an unbound class, the container:

  1. Picks a constructor among the class’s public constructors (compiler-generated synthetic constructors are ignored):
    • if exactly one is annotated with @Inject, that one;
    • otherwise, the one with the most parameters.
  2. Resolves each parameter type from the container, recursively, using the same rules.
  3. Calls the constructor with those values.
interface Clock { fun now(): Instant }
class SystemClock : Clock { override fun now(): Instant = Instant.now() }
class AuditLog(private val clock: Clock)
class UserService(private val audit: AuditLog, private val clock: Clock)
app.container.singleton(Clock::class.java) { SystemClock() }
// Neither AuditLog nor UserService is bound: both are autowired.
val users = app.resolve(UserService::class.java)

Mark the constructor the container should use with org.konaframework.core.Inject. The container recognises any annotation whose simple name is Inject, so jakarta.inject.Inject and javax.inject.Inject also work if they are already on your classpath.

class ReportService @Inject constructor(private val clock: Clock) {
constructor(clock: Clock, title: String) : this(clock)
}

Without @Inject, the container would pick the two-parameter constructor and fail on String.

  • Invent values. A parameter whose type is a primitive or lives in java.*, javax.*, or kotlin.* (such as String, Int, Duration, List) must be bound explicitly, or construction fails. Kotlin default parameter values are not used either. Read such values from config inside a factory instead.
  • Pick an implementation. Interfaces and abstract classes cannot be constructed; bind them to a concrete class or factory.

alias(alias, abstract) makes one string key refer to another key. Lookups follow alias chains, and aliasing a key back to itself throws BindingException("[x] is aliased to itself.").

app.container.alias("clock", Clock::class.java.name)
val clock: Clock = app.container.make("clock")

All resolution failures throw org.konaframework.core.BindingException (an unchecked exception), with a message naming the type:

Message Cause Fix
No binding for [key] String key never bound Bind it, or resolve by class
Target [X] is not instantiable. Interface, abstract class, or no public constructor Bind X to an implementation
Unresolvable dependency [java.lang.String] in class [X] A constructor parameter is a platform type Bind X with a factory that supplies the value
Target [X] has multiple @Inject constructors. Ambiguous choice Keep one @Inject
Failed to build [X]: … The constructor itself threw See the attached cause
Circular dependency detected: A -> B -> A A needs B, which needs A Restructure, or resolve one side lazily inside a method

CircularDependencyException is a subclass of BindingException, so you can catch it specifically. Cycles are detected through both autowiring and factories.

The container can be used from many threads at once, which matters because each HTTP request runs on its own virtual thread. Shared instances are built under a lock, so a singleton factory runs at most once even under concurrent first access. Because that lock is shared by all singletons, keep singleton factories quick: do slow setup (network connections, large file reads) in boot rather than inside a factory that request threads may be waiting on.

flush() removes every binding, instance, and alias, then re-registers the container under its own class. It also removes the Application and Config entries the application registered. It exists for tests that reuse a container; production code should not need it.

  • Bind interfaces, anything that needs configuration values, anything that should be shared (connection pools, clients, caches), and Kona infrastructure such as Router.
  • Autowire plain classes whose constructor parameters are themselves bound or autowirable, such as controllers and small domain services.

See also Application & providers for where bindings are registered during boot.