Kora DI Compile — Compile-Time Dependency Injection
Kora sub-skill — obey the kora-v2 meta rules on every task: R0 ground the workspace on Kora 2.0 refs before starting (framework source at tag
2.0.0.RC1+kora-examplesatmigration/2.0;kora-docsis 1.x only) · R1 read this sub-skill before writing code · R2 Kora 2.0 APIs only — no Spring/Micronaut/Quarkus, no Kora 1.x APIs, no invented annotations or config keys · R3 journal any incorrect Kora usage. Add comments/Javadoc only if asked.
| Framework | Kora 2.x — group io.koraframework, BOM io.koraframework:kora-bom |
| Java | 25+ (the published artifacts are class-file 69) |
| Kotlin | 2.4.x with KSP 2.3.x |
| Gradle | 9.x |
Kora wires the whole container at compile time. The annotation processor (Java) or symbol
processor (Kotlin) reads your @KoraApp interface, resolves every dependency, and emits a plain
Java/Kotlin class that constructs the graph. There is no reflection, no classpath scanning and no
runtime proxying: an unresolvable dependency is a compile error, not a startup failure.
Read this skill when: bootstrapping @KoraApp, registering components with @Component /
@Module / @FactoryModule, splitting a build with @KoraSubmodule, disambiguating with @Tag,
gating components with @Conditional, or decoding a graph build failure.
Scope boundary. This skill covers everything the processor decides: declaration, discovery,
resolution and the compile errors it emits. What the graph does once it is running —
init()/release() ordering, refresh, GraphInterceptor — belongs to kora-di-runtime.
Quick Start
Task Progress:
- [ ] 1. Add the kora-bom platform + annotation-processors (Java) / symbol-processors (KSP)
- [ ] 2. Create the @KoraApp interface and its main() entry point
- [ ] 3. extends the external modules you need (HoconConfigModule, LogbackModule, …)
- [ ] 4. Register your own types with @Component / @Module factory methods
- [ ] 5. ./gradlew classes and read the generated <App>Graph
1. Build Setup
Java
configurations {
koraBom
annotationProcessor.extendsFrom(koraBom)
implementation.extendsFrom(koraBom)
testAnnotationProcessor.extendsFrom(koraBom)
}
dependencies {
koraBom platform("io.koraframework:kora-bom:$koraVersion") // koraVersion=2.0.0.RC1
annotationProcessor "io.koraframework:annotation-processors"
testAnnotationProcessor "io.koraframework:annotation-processors"
}
java { toolchain { languageVersion = JavaLanguageVersion.of(25) } }
application { mainClass = "com.example.Application" }
Kotlin
plugins {
kotlin("jvm") version "2.4.10"
id("com.google.devtools.ksp") version "2.3.11"
}
dependencies {
implementation(platform("io.koraframework:kora-bom:$koraVersion"))
ksp("io.koraframework:symbol-processors:$koraVersion") // explicit version required
kspTest("io.koraframework:symbol-processors:$koraVersion")
}
kotlin {
jvmToolchain(25)
sourceSets.main { kotlin.srcDir("build/generated/ksp/main/kotlin") }
sourceSets.test { kotlin.srcDir("build/generated/ksp/test/kotlin") }
}
application { mainClass.set("com.example.ApplicationKt") } // not "…Application"
ksp is not covered by the BOM platform, so symbol-processors needs its own version. Every
Gradle module declaring @KoraApp, @KoraSubmodule, @Module or @Component needs the processor
on its own compile classpath — otherwise nothing is generated and the only symptom is
cannot find symbol: ApplicationGraph.
Full templates: build.gradle.template,
build.gradle.kts.template.
2. Application Bootstrap
package com.example;
import io.koraframework.application.graph.KoraApplication;
import io.koraframework.common.annotation.KoraApp;
import io.koraframework.config.hocon.HoconConfigModule;
import io.koraframework.logging.logback.LogbackModule;
@KoraApp
public interface Application extends HoconConfigModule, LogbackModule {
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph);
}
}
// same imports; a top-level main() compiles into com.example.ApplicationKt
@KoraApp
interface Application : HoconConfigModule, LogbackModule
fun main() {
KoraApplication.run(ApplicationGraph::graph)
}
ApplicationGraph is generated into your own package — never import it from
io.koraframework.*. The class name is always <KoraAppSimpleName>Graph.
Key rules: @KoraApp only on an interface · one graph per runnable application ·
external modules need extends · local @Module interfaces are discovered automatically.
→ @KoraApp Reference
3. Component Registration
@Component — constructor injection
@Component
public final class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) { // exactly one public constructor
this.repository = repository;
}
}
@Module — factory methods
@Module
public interface StorageModule {
default Storage storage(StorageConfig config) {
return new TempFileStorage(config);
}
}
@Module and @KoraApp accept interfaces only; providers are default methods (Kotlin: bodied
interface functions). A provider must return a reference type and must not return null unless the
consumer's parameter is @Nullable.
→ Component Registration Reference · Component Factories Reference
4. Module Discovery — when extends is required
The processor auto-collects every @Module interface it compiles in this Gradle module.
Anything already on the classpath as bytecode was never seen by the processor and must be pulled in
explicitly.
| Where the module lives | extends on @KoraApp? |
|---|---|
@Module interface in the same compilation (src/main/java, src/main/kotlin) |
No — auto-discovered |
Module shipped by a Kora artifact (HoconConfigModule, JdbcDatabaseModule, …) |
Yes |
@KoraSubmodule in another Gradle subproject |
Yes |
→ Module Auto-Discovery Reference
5. Multi-Module Projects
// pet-api/ — the domain subproject
@KoraSubmodule
public interface PetModule extends CommonModule, JdbcDatabaseModule { }
// app/ — the assembly subproject
@KoraApp
public interface Application extends PetModule, HoconConfigModule, LogbackModule {
static void main(String[] args) { KoraApplication.run(ApplicationGraph::graph); }
}
@KoraSubmodule makes the processor emit <Name>SubmoduleImpl next to the interface, carrying
every @Component and @Module provider compiled in that subproject. The processor must run in the
subproject too, or the @KoraApp build fails with "Kora submodule was not generated yet".
→ @KoraSubmodule Reference
6. Disambiguation with @Tag
A tag is any class used as a marker. Matching is exact, with one wildcard:
| Injection point | Provider tag | Result |
|---|---|---|
| no tag | no tag | match |
| no tag | @Tag(X.class) |
no match — tagged components are invisible to untagged claims |
@Tag(X.class) |
@Tag(X.class) |
match |
@Tag(Tag.Any.class) |
any / none | match |
public final class RedisTag { private RedisTag() {} }
@Tag(RedisTag.class) @Component
public final class RedisCache implements Cache { }
public UserService(@Tag(RedisTag.class) Cache cache) { }
→ Tags & Collections Reference
7. Collection, Lazy and Optional Dependencies
| Parameter shape | Meaning |
|---|---|
All<T> |
every matching component; All<T> extends Iterable<T>, not List<T> |
All<ValueOf<T>> / All<PromiseOf<T>> |
the same set, held indirectly |
ValueOf<T> |
indirect link — breaks cycles, decouples refresh |
PromiseOf<T> |
indirect link resolved to Optional<T> after init |
@Nullable T |
optional; the processor injects null when nothing matches |
Optional<T> |
optional; the processor builds the Optional for you |
Wrapped<T> (provider side) |
provider returns a wrapper, consumers receive the unwrapped T |
@Component
public final class NotificationService {
public NotificationService(@Tag(Tag.Any.class) All<Notifier> notifiers,
ValueOf<AuditLog> audit,
@Nullable SmsProvider sms) { }
}
Java nullability is JSpecify (org.jspecify.annotations.Nullable, type-use). Kotlin expresses it
as T? — never carry JSpecify annotations into Kotlin.
8. Graph Roots
Nothing is instantiated unless it is a @Root or a transitive dependency of one. A component whose
only job is a side effect at startup will be pruned without @Root.
import io.koraframework.common.annotation.Root;
@Root @Component
public final class CacheWarmer implements Lifecycle {
public void init() { /* runs at startup */ }
public void release() { }
}
@Root also applies to a @Module provider method. @Root lives in
io.koraframework.common.annotation — the same package as everything else in 2.0.
→ Graph Roots & Lifecycle Reference ·
runtime init/release semantics: kora-di-runtime
9. Conditional Components (new in 2.0)
@Conditional(tag = X.class) gates a component on a GraphCondition published under @Tag(X.class).
Candidates are all compiled; exactly one must match when the graph initialises.
@Component
@Conditional(tag = RedisEnabled.class)
public final class RedisCache implements Cache { }
→ Conditional Components Reference
10. Graph Build Failures
The 2.0 processor prints a diagnosis, a resolution path and a Fix: list. Match on the first
line:
| First line | Cause | Fix |
|---|---|---|
No component found for dependency: |
nothing provides that type+tag | add @Component, add a module provider, or extends the module that has one |
Multiple components match dependency: |
two providers, same type+tag | differentiate with @Tag, mark the fallback @DefaultComponent, or delete one |
Circular dependency found: |
a cycle in the graph | wrap one side in ValueOf<T> / PromiseOf<T> |
@Component class must have exactly one public constructor. |
0 or 2+ public constructors | keep one; move complex construction to a module provider |
@KoraApp can only be applied to interfaces. / @Module can only be applied to interfaces. |
annotation on a class | make it an interface |
Kora submodule was not generated yet: |
processor missing in the submodule's build | add annotationProcessor / ksp there |
@Tag.Factory can only be used inside factory modules. |
Tag.Factory outside a @FactoryModule |
use an explicit @Tag(...) |
Dependency uses a raw type: |
raw List, Map, Repository … |
supply type arguments |
Expected @KoraApp as SubModule, but Submodule implementation not found (warning) |
a test @KoraApp extends the main one without -Akora.app.submodule.enabled=true |
see @KoraSubmodule Reference |
A No component found error also lists same-type-different-tag candidates under Note: — read
that section first; a forgotten or mismatched @Tag is the usual cause.
11. Debugging
# Java — generated graph
ls build/generated/sources/annotationProcessor/java/main/
# Kotlin — generated graph
ls build/generated/ksp/main/kotlin/
Raise processor verbosity (Java annotation processor only) by adding the koraLogLevel compiler
argument; the processor also writes a full log under build/kora/log/:
compileJava { options.compilerArgs += ["-AkoraLogLevel=DEBUG"] }
After renaming a package or migrating from 1.x, stale generated sources produce phantom errors that point at classes you have already deleted. Rebuild the generated tree — never hand-edit it:
./gradlew clean classes --no-build-cache
12. Removed in Kora 2.0
Contextdoes not exist anywhere in the framework. A component or provider that takes a KoraContextparameter will not compile.- Component contracts are synchronous, executed on virtual threads.
CompletionStage, ReactorMono/Fluxand Kotlinsuspendare no longer Kora contracts. ru.tinkoff.kora.*is gone; DI annotations moved fromru.tinkoff.kora.commontoio.koraframework.common.annotation.@Rootwas already in…common.annotationunder 1.x — it only changed group.ru.tinkoff.kora:kora-parentis replaced byio.koraframework:kora-bom.
References
| Reference | When |
|---|---|
| @KoraApp | Bootstrap, generated graph, entry point |
| Component Registration | Getting a type into the graph |
| Module Auto-Discovery | extends rules |
| @KoraSubmodule | Multi-module Gradle builds |
| @DefaultComponent | Overridable defaults |
| Component Factories | Providers, generics, @FactoryModule |
| Tags & Collections | @Tag, All<T>, ValueOf<T> |
| Conditional Components | @Conditional, GraphCondition |
| Graph Roots & Lifecycle | @Root, Lifecycle, Wrapped<T> |
Assets
Templates in assets/: Application.java.template, Application.kt.template,
Component.java.template, Module.java.template, KoraSubmodule.java.template,
build.gradle.template, build.gradle.kts.template, settings.gradle.template,
gradle.properties.template, gradle-wrapper.properties.template, application.conf.template,
application.yaml.template, plus QUICK_REFERENCE.md.
python3 scripts/generate_project.py --name my-app --package com.example --dry-run
python3 scripts/generate_project.py --name my-app --package com.example --lang kotlin
python3 scripts/validate_gradle.py --file build.gradle