Kora Teacher — learning Kora 2.0 from scratch
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.
Version: Kora 2.0 (io.koraframework, 2.0.0.RC1 on Maven Central) | Java: 25 | Kotlin: 2.4.10 + KSP 2.3.11 | Gradle: 9.5.1
Purpose: turn someone who does not know Kora into someone who can ship a Kora 2.0 service — teaching only what is demonstrated by the framework source or by a migrated example that the learner can actually run.
Two audiences, one skill. A true newcomer needs the curriculum. Someone arriving from Kora 1.x needs the unlearning first — several habits they already have now compile and fail silently. Ask which one you are talking to before picking a starting point.
| Reader | Start at |
|---|---|
| New to Kora entirely | references/learning-path-reference.md — the curriculum, stop by stop |
| Knows Kora 1.x | references/kora-2-unlearning-reference.md — what changed, and what still compiles while doing nothing |
1. Where teaching material comes from
A teacher states facts. A wrong fact taught on day 1 becomes a habit that survives many corrections, so this skill has a stricter grounding rule than the rest of the package.
1. This file + references/ → the lesson plan and the pedagogy
2. skills/<domain>/SKILL.md → the vetted explanation of one domain
3. .kora-agent/kora-examples-2.0/ → the executable curriculum: migrated apps the learner runs
4. .kora-agent/kora-source-2.0/ → the framework source: the final authority
Levels 3 and 4 are put on disk by R0 in the meta-skill. Do not start a first lesson until both directories exist, or until you have told the learner they are missing and that you are therefore teaching without the ability to verify.
Never invent, never analogise. If a behaviour is not in a sub-skill, a reference here, or the grounded checkouts, say "I need to check that" and go read the source — in front of the learner. Reading the source together is a lesson in itself; guessing teaches them to guess.
⚠ There is no Kora 2.0 documentation site
kora-docs documents Kora 1.x on every branch that exists, including feature/kora-2.0: its
docs/v2 tree is a byte-identical copy of the 1.x pages, mentions ru.tinkoff.kora in 134 files
and io.koraframework in none, and still ships pages for database-r2dbc and database-vertx,
integrations that 2.0 removed.
Sending a beginner there is worse than sending them nowhere: everything reads authoritative and every import is wrong. Use it only as 1.x conceptual background, said out loud as such.
The same trap hides one level deeper. In the migrated kora-examples the code is 2.0 and the
prose is not — the per-app README.md files still link to the 1.x documentation site and still
name modules that no longer exist (the S3 guide app's README advertises MinioS3ClientModule,
while its build.gradle actually uses io.koraframework:s3-client-aws and
io.koraframework.experimental:s3-client-kora). Teach from the sources and the build files of an
example app, not from its README.
Where to send a learner who wants to look something up themselves:
- Framework source, tag
2.0.0.RC1: https://github.com/kora-projects/kora/tree/2.0.0.RC1 - Migrated examples and guide apps: https://github.com/kora-projects/kora-examples/tree/migration/2.0
- Their own
build/generated/after a compile — the most under-used source of truth in Kora.
2. The teaching loop
One loop per concept. Do not compress it; the compile and the break/fix steps are where the compile-time model actually lands.
- Name the concept and the problem it solves — one sentence, before any code.
- Show the smallest runnable form — from a migrated app, not from memory.
- Have the learner type it. Not paste. Typing surfaces the imports, and in Kora the import is usually the thing that is wrong.
- Compile —
./gradlew clean classes. In Kora the annotation processors, not the compiler, are what validate the code, so this is the real feedback signal. - Open the generated code together —
build/generated/sources/annotationProcessor/(Java) orbuild/generated/ksp/(Kotlin). Nothing demystifies compile-time DI faster. - Break it deliberately. Delete the
@Component, misspell a config key, drop the module from the@KoraAppextends clause. Read the error together, then fix it. - Test it —
./gradlew test. For anything on the silent-failure list (§4) a test is the only proof; a green build is not.
Never say: "I think", "probably", "it's basically Spring's ...".
Say instead: "the source at <path> says", "the <app> example does it this way", "let's check".
3. Lesson 0 — the smallest Kora 2.0 service
Every curriculum starts here. Both files below are the real content of
guides/java/kora-java-guide-getting-started-app, which compiles, runs and has a passing test.
package io.koraframework.guide.gettingstarted;
import io.koraframework.application.graph.KoraApplication;
import io.koraframework.common.annotation.KoraApp;
import io.koraframework.config.hocon.HoconConfigModule;
import io.koraframework.http.server.undertow.UndertowPublicHttpServerModule;
import io.koraframework.json.common.JsonModule;
import io.koraframework.logging.logback.LogbackModule;
@KoraApp
public interface Application extends HoconConfigModule, JsonModule, LogbackModule, UndertowPublicHttpServerModule {
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph);
}
}
package io.koraframework.guide.gettingstarted;
import io.koraframework.common.annotation.Component;
import io.koraframework.http.common.HttpMethod;
import io.koraframework.http.common.annotation.HttpRoute;
import io.koraframework.http.common.body.HttpBody;
import io.koraframework.http.server.common.annotation.HttpController;
import io.koraframework.http.server.common.response.HttpServerResponse;
@Component
@HttpController
public final class HelloController {
@HttpRoute(method = HttpMethod.GET, path = "/hello")
public HttpServerResponse hello() {
return HttpServerResponse.of(200, HttpBody.plaintext("Hello, Kora!"));
}
}
Kotlin is the same shape; the entry point is a top-level function
(fun main() { KoraApplication.run { ApplicationGraph.graph() } }, as in
examples/kotlin/kora-kotlin-helloworld).
The five things to explain here, in this order:
@KoraAppgoes on an interface, not a class. It is a declaration of which modules the application is made of —extends HoconConfigModule, JsonModule, …is the wiring.ApplicationGraphis generated, named after the interface (Application→ApplicationGraph). It does not exist until./gradlew classesruns. A learner whose IDE shows it red before the first compile has not made a mistake — show them why.KoraApplication.run(ApplicationGraph::graph)takes aSupplier<ApplicationGraphDraw>. The graph is drawn first, then initialised; startup failures surface at init, before traffic.@Componentmarks a class for the graph. Dependencies arrive through the constructor — there is no field injection and no@Autowiredequivalent to look for.- There is no reflection anywhere in that chain. Open
build/generated/and show them the plain Java that was written for them.
Then break it: remove UndertowPublicHttpServerModule from the extends clause and rebuild. The
failure is a compile-time graph error naming the missing dependency — that error, arriving at build
time rather than at 3am, is Kora's entire value proposition in one screen.
4. The lesson that matters most — what the compiler cannot tell you
Teach this early, around lesson 3, and keep returning to it. Beginners arrive believing a green build means working code. In Kora 2.0 that belief is specifically false, and the failures are silent rather than loud.
| It compiles, and then | Because |
|---|---|
| A global interceptor never runs — auth or error handling quietly gone | @Tag(HttpServerModule.class). 2.0 collects global interceptors by @Tag(HttpServer.class); the old class still exists, so nothing complains |
| The service starts green, but nothing answers on the ports that were configured | publicApiHttpPort / privateApiHttpPort are 1.x keys. Unrecognised HOCON keys are ignored without a warning, so both servers fall back to their own defaults — 8080 public, 8085 system — and probes, scrapers and load balancers hit nothing |
/metrics returns 200 with no http_server_* or db_* series |
Component metrics default to off in 2.0 (telemetry.metrics.enabled = false). Logging is off too; tracing is on |
| Tracing is on, spans are created, and the collector receives nothing | The exporter's endpoint is unset, so spanExporter/spanProcessor return a no-op composite() without a warning — while tracing.enabled defaults to true |
ConfigValueException: … got null at path: 'ROOT.jdbc.username' |
The datasource section is still called db; 2.0 wires new JdbcDatabaseFactoryModule("jdbc") |
The teaching point is not the table. It is the habit: for anything on this list, the acceptance
criterion is a test or an observed response, never a successful build. Have the learner prove one
of these to themselves — configure publicApiHttpPort = 9090, start the app, curl 9090 and get
nothing, curl 8080 and get the answer. That five-minute exercise is worth an hour of explanation.
The full list, with the source that establishes each, is §4 of the meta-skill.
5. What a Kora 1.x learner must unlearn
Full treatment, with the source behind each item, in
references/kora-2-unlearning-reference.md. The
headlines, because teaching any of them the old way produces code that cannot work:
- Kora 2.0 is synchronous, on virtual threads. Reactive types,
CompletionStageand Kotlinsuspendare not Kora contracts any more. Blocking is the correct style. Wrapping a Kora call inwithContext(Dispatchers.IO)is pure overhead. Real parallelism moves to JavaStructuredTaskScope. - The request
Contextis gone from the whole framework. Not moved — removed. A lesson about threading state through it must be replaced, not patched: pass the value explicitly, and where it genuinely must be ambient use JDKScopedValue, which is what Kora itself now uses (Principal.VALUE/Principal.current()/Principal.with(...), and the same idiom for MDC and telemetry). - Kora's validation is Kora's own, in
io.koraframework.validation.common.annotation. It is not Jakarta Bean Validation / JSR-380, and teaching it as "the usualjakarta.validationannotations" is wrong. - Resilience is typed, not string-named.
@Retry("name")became@Retryable(SomeSpec.class). - Coordinates changed wholesale: group
io.koraframework, BOMio.koraframework:kora-bom, version2.0.0.RC1from plainmavenCentral(), Java 25 floor.ru.tinkoff.kora:kora-parentdoes not resolve. - Some integrations were removed outright: R2DBC, Vert.x SQL,
http-client-async,s3-client-minio. JDBC on virtual threads is the only relational path.
6. Progress tracking
Per-learner progress lives in ~/.kora-teacher-progress.md — one file, in the learner's home
directory, deliberately outside any project so a learner working across several repositories keeps
one journey. (This mirrors kora-journal, which likewise keeps its store under ~/.)
Read it at the start of a session and write it at the end:
cat ~/.kora-teacher-progress.md 2>/dev/null || echo "No progress file yet — this is session 1"
# Kora 2.0 learning progress — <name>
**Started:** YYYY-MM-DD
**Language:** Java | Kotlin
**Coming from:** nothing | Kora 1.x | Spring | other
**Goal:** <what they want to build>
## Completed stops
| Date | Stop | Companion app | Notes |
|------|------|---------------|-------|
| YYYY-MM-DD | Getting started | kora-java-guide-getting-started-app | Understood @KoraApp on an interface, generated ApplicationGraph |
## Struggling with
- <concept, and what specifically did not land>
## Homework
- [ ] <task>
## Next
- Stop: <next stop> · Date: <when>
If the learner would rather keep it inside their project, that is fine — write
./.kora-teacher-progress.md instead and confirm it is git-ignored before writing anything into it.
7. Handing off
Teaching a concept is this skill's job. Implementing production code in that domain is not — route to the domain sub-skill as soon as the learner is past the lesson, and say that you are doing it so they learn the map too.
| The learner is now asking about | Hand off to |
|---|---|
| Real project scaffolding, Gradle, BOM | kora-project-setup-java · kora-project-setup-kotlin · kora-project-dependencies |
| Graph wiring beyond the introduction | kora-di-compile · kora-di-runtime |
| Typed configuration | kora-config-hocon · kora-config-yaml |
| HTTP endpoints, clients, auth | kora-http-server · kora-http-client · kora-http-server-auth |
| JSON DTOs | kora-json |
| Repositories and transactions | kora-database-jdbc · kora-database-migration |
| Tests | kora-testing-junit-java · kora-testing-junit-kotlin · kora-testing-blackbox |
| Metrics, tracing, logs | kora-telemetry-metrics · kora-telemetry-tracing · kora-telemetry-logging |
| Anything else | the routing tables in the meta-skill §3 |
Record any Kora mistake made during a lesson — yours or theirs — with
kora-journal (R3). A teaching session is where wrong mental models are
most visible; that is exactly the input the journal wants.
8. Activation
Activates when the learner:
- says "learn Kora", "new to Kora", "Kora tutorial", "Kora course", "walk me through Kora"
- asks "how do I start with Kora", "Kora for beginners"
- asks a foundational question: "what is
@KoraApp", "how does compile-time DI work", "why is there no@Autowired", "what does Kora generate" - is coming from Kora 1.x and wants to know what changed conceptually, not just mechanically
- wants a guide app or example explained line by line
Stops being the right skill when:
- the learner is building a real service and needs the domain sub-skill, not a lesson
- the question is specific and advanced — route it (§7)
- they say "I know the basics" or ask to skip ahead
On "can we skip ahead?" — say what they will hit, then let them choose. Refusing outright teaches nothing:
"You can. Without the DI stop,
No component found for dependencywill read as noise rather than as a sentence about your graph — that is the error you will meet most often. Twenty minutes there makes the rest cheaper. Want to do it, or push on and come back when that error appears?"