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.
Binding a service
Section titled “Binding a service”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 resolvec.bind(ReportBuilder::class.java) { ReportBuilder() }
// One shared instance, created on first resolvec.singleton(Clock::class.java) { SystemClock() }
// A factory that pulls in another servicec.singleton(Mailer::class.java) { container -> SmtpMailer(container.make(Clock::class.java)) }
// An interface bound to an implementation class, which the container autowiresc.singleton(UserRepository::class.java, SqlUserRepository::class.java)
// An object you already havec.instance(AppSettings::class.java, AppSettings(region = "eu"))
// A string keyc.singleton("greeter") { Greeter("Hello") }Container c = app.getContainer();
// A new instance on every resolvec.bind(ReportBuilder.class, container -> new ReportBuilder());
// One shared instance, created on first resolvec.singleton(Clock.class, container -> new SystemClock());
// A factory that pulls in another servicec.singleton(Mailer.class, container -> new SmtpMailer(container.make(Clock.class)));
// An interface bound to an implementation class, which the container autowiresc.singleton(UserRepository.class, SqlUserRepository.class);
// An object you already havec.instance(AppSettings.class, new AppSettings("eu"));
// A string keyc.singleton("greeter", container -> new 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.
Resolving a service
Section titled “Resolving a service”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) // trueClock clock = app.resolve(Clock.class);Mailer mailer = app.getContainer().make(Mailer.class);Greeter greeter = app.getContainer().make("greeter");boolean known = app.getContainer().bound(Clock.class); // trueWhat 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.
Autowiring
Section titled “Autowiring”When asked for an unbound class, the container:
- 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.
- if exactly one is annotated with
- Resolves each parameter type from the container, recursively, using the same rules.
- 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)public interface Clock { Instant now(); }public final class SystemClock implements Clock { public Instant now() { return Instant.now(); } }
public final class AuditLog { public AuditLog(Clock clock) { /* ... */ }}public final class UserService { public UserService(AuditLog audit, Clock clock) { /* ... */ }}
app.getContainer().singleton(Clock.class, c -> new SystemClock());
// Neither AuditLog nor UserService is bound: both are autowired.UserService users = app.resolve(UserService.class);Choosing a constructor with @Inject
Section titled “Choosing a constructor with @Inject”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)}public final class ReportService { @Inject public ReportService(Clock clock) { /* ... */ }
public ReportService(Clock clock, String title) { /* ... */ }}Without @Inject, the container would pick the two-parameter constructor and fail on String.
What autowiring will not do
Section titled “What autowiring will not do”- Invent values. A parameter whose type is a primitive or lives in
java.*,javax.*, orkotlin.*(such asString,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.
Aliases
Section titled “Aliases”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")app.getContainer().alias("clock", Clock.class.getName());Clock clock = app.getContainer().make("clock");Errors
Section titled “Errors”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.
Thread safety
Section titled “Thread safety”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.
When to bind and when to autowire
Section titled “When to bind and when to autowire”- 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.