# Kora Teacher

> Teach Kora 2.0 (io.koraframework) to a newcomer — guided curriculum, lesson plans and concept explanations, grounded in the framework source at tag 2.0.0.RC1 and the migrated example apps on kora-examples branch migration/2.0. Covers the beginner spine (helloworld, getting-started, dependency injection, config, JSON, HTTP, JDBC, testing) and the unlearning a Kora 1.x user needs: synchronous contracts on virtual threads instead of suspend/reactive/CompletionStage, no request Context, telemetry off by default, and why a green build is not proof of a working service. Use when someone is new to Kora, asks for a tutorial, course or walkthrough, or asks a foundational question such as "what is @KoraApp" or "how does compile-time DI work". Kora 2.0 has no documentation site — never send a learner to kora-docs for a 2.0 answer.

- Skill: `kora-projects/kora-teacher-2` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add kora-projects/kora-teacher-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kora-projects/kora-teacher-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- License: Apache-2.0
- Author: kora-projects (https://skillmd.com/u/kora-projects)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kora-projects/kora-teacher-2

---


# Kora Teacher — learning Kora 2.0 from scratch

> **Kora sub-skill — obey the [kora-v2 meta rules](../../SKILL.md) on every task:** **R0** ground the workspace on Kora 2.0 refs before starting (framework source at tag `2.0.0.RC1` + `kora-examples` at `migration/2.0`; `kora-docs` is 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`](references/learning-path-reference.md) — the curriculum, stop by stop |
| Knows Kora 1.x | [`references/kora-2-unlearning-reference.md`](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](../../SKILL.md). 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.

1. **Name the concept and the problem it solves** — one sentence, before any code.
2. **Show the smallest runnable form** — from a migrated app, not from memory.
3. **Have the learner type it.** Not paste. Typing surfaces the imports, and in Kora the import is
   usually the thing that is wrong.
4. **Compile** — `./gradlew clean classes`. In Kora the annotation processors, not the compiler,
   are what validate the code, so this is the real feedback signal.
5. **Open the generated code together** — `build/generated/sources/annotationProcessor/` (Java) or
   `build/generated/ksp/` (Kotlin). Nothing demystifies compile-time DI faster.
6. **Break it deliberately.** Delete the `@Component`, misspell a config key, drop the module from
   the `@KoraApp` extends clause. Read the error together, then fix it.
7. **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.

```java
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);
    }
}
```

```java
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:**

1. `@KoraApp` goes on an **interface**, not a class. It is a declaration of which modules the
   application is made of — `extends HoconConfigModule, JsonModule, …` is the wiring.
2. `ApplicationGraph` is **generated**, named after the interface (`Application` → `ApplicationGraph`).
   It does not exist until `./gradlew classes` runs. A learner whose IDE shows it red before the
   first compile has not made a mistake — show them why.
3. `KoraApplication.run(ApplicationGraph::graph)` takes a `Supplier<ApplicationGraphDraw>`. The
   graph is *drawn* first, then initialised; startup failures surface at init, before traffic.
4. `@Component` marks a class for the graph. Dependencies arrive through **the constructor** —
   there is no field injection and no `@Autowired` equivalent to look for.
5. 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](../../SKILL.md).

---

## 5. What a Kora 1.x learner must unlearn

Full treatment, with the source behind each item, in
[`references/kora-2-unlearning-reference.md`](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, `CompletionStage` and Kotlin
  `suspend` are not Kora contracts any more. Blocking is the correct style. Wrapping a Kora call in
  `withContext(Dispatchers.IO)` is pure overhead. Real parallelism moves to Java
  `StructuredTaskScope`.
- **The request `Context` is 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 JDK `ScopedValue`, 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 usual `jakarta.validation` annotations"
  is wrong.
- **Resilience is typed, not string-named.** `@Retry("name")` became `@Retryable(SomeSpec.class)`.
- **Coordinates changed wholesale**: group `io.koraframework`, BOM `io.koraframework:kora-bom`,
  version `2.0.0.RC1` from plain `mavenCentral()`, Java 25 floor. `ru.tinkoff.kora:kora-parent`
  does 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:

```bash
cat ~/.kora-teacher-progress.md 2>/dev/null || echo "No progress file yet — this is session 1"
```

```markdown
# 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-java/SKILL.md) · [`kora-project-setup-kotlin`](../kora-project-setup-kotlin/SKILL.md) · [`kora-project-dependencies`](../kora-project-dependencies/SKILL.md) |
| Graph wiring beyond the introduction | [`kora-di-compile`](../kora-di-compile/SKILL.md) · [`kora-di-runtime`](../kora-di-runtime/SKILL.md) |
| Typed configuration | [`kora-config-hocon`](../kora-config-hocon/SKILL.md) · [`kora-config-yaml`](../kora-config-yaml/SKILL.md) |
| HTTP endpoints, clients, auth | [`kora-http-server`](../kora-http-server/SKILL.md) · [`kora-http-client`](../kora-http-client/SKILL.md) · [`kora-http-server-auth`](../kora-http-server-auth/SKILL.md) |
| JSON DTOs | [`kora-json`](../kora-json/SKILL.md) |
| Repositories and transactions | [`kora-database-jdbc`](../kora-database-jdbc/SKILL.md) · [`kora-database-migration`](../kora-database-migration/SKILL.md) |
| Tests | [`kora-testing-junit-java`](../kora-testing-junit-java/SKILL.md) · [`kora-testing-junit-kotlin`](../kora-testing-junit-kotlin/SKILL.md) · [`kora-testing-blackbox`](../kora-testing-blackbox/SKILL.md) |
| Metrics, tracing, logs | [`kora-telemetry-metrics`](../kora-telemetry-metrics/SKILL.md) · [`kora-telemetry-tracing`](../kora-telemetry-tracing/SKILL.md) · [`kora-telemetry-logging`](../kora-telemetry-logging/SKILL.md) |
| Anything else | the routing tables in the [meta-skill](../../SKILL.md) §3 |

Record any Kora mistake made during a lesson — yours or theirs — with
[`kora-journal`](../kora-journal/SKILL.md) (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 dependency` will 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?"

