<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Atanas Oreshkov</title>
    <description>The latest articles on DEV Community by Atanas Oreshkov (@aoreshkov).</description>
    <link>https://dev.to/aoreshkov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4038812%2F77251568-8253-46d8-854d-233eb8c88569.png</url>
      <title>DEV Community: Atanas Oreshkov</title>
      <link>https://dev.to/aoreshkov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aoreshkov"/>
    <language>en</language>
    <item>
      <title>Let your AI assistant read Oracle Forms — without Forms Builder</title>
      <dc:creator>Atanas Oreshkov</dc:creator>
      <pubDate>Sat, 22 Aug 2026 05:38:21 +0000</pubDate>
      <link>https://dev.to/aoreshkov/let-your-ai-assistant-read-oracle-forms-without-forms-builder-29kd</link>
      <guid>https://dev.to/aoreshkov/let-your-ai-assistant-read-oracle-forms-without-forms-builder-29kd</guid>
      <description>&lt;p&gt;Somewhere in your organization there is an Oracle Forms application from 1998 that still runs payroll. Its logic lives in binary &lt;code&gt;.fmb&lt;/code&gt; and &lt;code&gt;.pll&lt;/code&gt; modules that exactly one program can open — Forms Builder — and the developer who wrote it retired years ago. Your AI assistant, which will happily read any Kotlin file you point it at, is completely blind to it.&lt;/p&gt;

&lt;p&gt;That's an &lt;em&gt;input&lt;/em&gt; problem, not a model problem. &lt;a href="https://github.com/aoreshkov/oracle-forms-mcp" rel="noopener noreferrer"&gt;Oracle Forms MCP&lt;/a&gt; is an MCP server that solves it: point it at a directory of modules and it converts them to text, indexes them, and serves the pieces — blocks, items, triggers, program units, PL/SQL — as MCP tools and resources. Kotlin Multiplatform core, JVM server, Apache-2.0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You:  What does ORDERS.fmb do?
AI →  list_modules                 → ORDERS.fmb (NOT_CACHED), MAINMENU.mmb, UTILS.pll …
AI →  fetch_module ORDERS.fmb      → converted + indexed (2 blocks, 3 triggers, 3 program units)
AI →  get_module_overview ORDERS   → blocks, triggers, LOVs, record groups, windows, canvases …
You:  Show me the validation logic on the ORDERS block.
AI →  list_triggers block=ORDERS   → WHEN-VALIDATE-ITEM (on ORDER_ID), WHEN-VALIDATE-RECORD
AI →  get_trigger ORDERS WHEN-VALIDATE-ITEM  → the decoded PL/SQL body
You:  That validation is the legacy pre-2010 path — note it so we remember.
AI →  annotate_element ORDERS trigger WHEN-VALIDATE-ITEM kind=note "Legacy pre-2010 path" → saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The hard part is the front door, not the protocol
&lt;/h2&gt;

&lt;p&gt;Writing the MCP layer took days. Getting &lt;em&gt;text&lt;/em&gt; out of a &lt;code&gt;.fmb&lt;/code&gt; took considerably longer, and it shaped everything above it.&lt;/p&gt;

&lt;p&gt;Oracle ships the only converters — &lt;code&gt;frmf2xml&lt;/code&gt; for &lt;code&gt;.fmb&lt;/code&gt;/&lt;code&gt;.mmb&lt;/code&gt;/&lt;code&gt;.olb&lt;/code&gt;, &lt;code&gt;frmcmp_batch&lt;/code&gt; for &lt;code&gt;.pll&lt;/code&gt; — and they are proprietary, so nothing can bundle them. Three modes fall out of that, in strict precedence: a site-supplied &lt;code&gt;--convert-command&lt;/code&gt;, then an &lt;code&gt;ORACLE_HOME&lt;/code&gt; installation, then &lt;strong&gt;copy-mode&lt;/strong&gt;, where pre-converted &lt;code&gt;orders_fmb.xml&lt;/code&gt; / &lt;code&gt;utils.pld&lt;/code&gt; files already sit next to the modules. Copy-mode is what makes the Docker image and the bundled sample forms work on a laptop with no Oracle anything.&lt;/p&gt;

&lt;p&gt;The tools themselves are 1990s-era and behave like it. &lt;code&gt;frmf2xml&lt;/code&gt; writes into the &lt;em&gt;process working directory&lt;/em&gt;, so the server runs it with the output directory as its cwd rather than passing a path. Exit codes are unreliable, so success is judged by the output file existing, being non-empty, and being &lt;strong&gt;newer than the invocation&lt;/strong&gt; — which has the amusing consequence that a wrapper script using &lt;code&gt;cp -p&lt;/code&gt; to copy a pre-generated file is rejected, because it preserved the source's mtime.&lt;/p&gt;

&lt;p&gt;The resulting XML is large and version-dependent, so parsing is a single streaming StAX pass with one firm rule: &lt;strong&gt;the parser never fails on unknown vocabulary&lt;/strong&gt;. An element nobody has seen before is skipped generically rather than throwing. A parser that dies on an unfamiliar tag is worthless against thirty years of Forms releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the tool surface
&lt;/h2&gt;

&lt;p&gt;Current MCP guidance converges on a few principles — one bounded context per server, a curated tool surface, bounded resources, reads cheap and writes deliberate. Here's how those landed in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sixteen tools is more than "curated" likes.&lt;/strong&gt; I'll defend it anyway: they aren't sixteen peers. They're two verbs — &lt;code&gt;list_*&lt;/code&gt; and &lt;code&gt;get_*&lt;/code&gt; — over one noun space, plus a five-tool annotation layer. The server's &lt;code&gt;instructions&lt;/code&gt; field states the entry sequence explicitly (&lt;code&gt;list_modules&lt;/code&gt; → &lt;code&gt;fetch_module&lt;/code&gt; → &lt;code&gt;get_module_overview&lt;/code&gt; → drill down), which is what actually keeps a model from flailing. Tool &lt;em&gt;count&lt;/em&gt; is a bad proxy; tool &lt;em&gt;shape&lt;/em&gt; is the real thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every tool declares its behaviour.&lt;/strong&gt; All eleven read tools carry &lt;code&gt;readOnlyHint = true&lt;/code&gt;, &lt;code&gt;destructiveHint = false&lt;/code&gt;, &lt;code&gt;idempotentHint = true&lt;/code&gt;, &lt;code&gt;openWorldHint = false&lt;/code&gt; — a closed, local, side-effect-free domain. Clients that gate or auto-approve on annotations can do so correctly instead of guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every tool declares an &lt;code&gt;outputSchema&lt;/code&gt; and returns both shapes.&lt;/strong&gt; One DTO is serialized once and handed back twice: as &lt;code&gt;structuredContent&lt;/code&gt; matching the schema, and as pretty JSON text for clients without structured-output support. DTO fields are all defaulted, so adding one is forward-compatible rather than a breaking change. A &lt;code&gt;ToolRegistrationTest&lt;/code&gt; fails the build if any tool ships without a title, annotations, and a schema — the convention is enforced, not documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors are content, not exceptions.&lt;/strong&gt; Expected failures — a bad argument, an un-fetched module, a conversion that died — come back as &lt;code&gt;isError&lt;/code&gt; results whose text is written &lt;em&gt;for the model&lt;/em&gt;: every message names the tool call that fixes the situation. "Module ORDERS is stale, call fetch_module" is a message an agent can act on. A stack trace is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token cost is a design constraint.&lt;/strong&gt; &lt;code&gt;search_source&lt;/code&gt; paginates via &lt;code&gt;offset&lt;/code&gt;/&lt;code&gt;nextOffset&lt;/code&gt;. The list tools take &lt;code&gt;verbosity=concise|detailed&lt;/code&gt;, defaulting to concise. And the index itself stays small by construction: PL/SQL bodies never live in &lt;code&gt;index.json&lt;/code&gt; — they're extracted to &lt;code&gt;.sql&lt;/code&gt; sidecars and referenced by line range, so &lt;code&gt;get_trigger&lt;/code&gt; reads exactly one body instead of the model paying for a megabyte of JSON to find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotations: letting the model write back
&lt;/h2&gt;

&lt;p&gt;The part I'd build again first. Reading a form is only half the job — the other half is &lt;em&gt;remembering&lt;/em&gt; what you worked out about it, because nobody fully understands these applications on the first pass.&lt;/p&gt;

&lt;p&gt;So the server has a small write surface: &lt;code&gt;annotate_element&lt;/code&gt; (notes, tags, summaries, classifications) and &lt;code&gt;relate_elements&lt;/code&gt; (directed cross-references — this trigger &lt;em&gt;calls&lt;/em&gt; that program unit). Three rules make it durable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Asserted, never derived.&lt;/strong&gt; Annotations live in their own store, not in the fingerprinted cache. Deleting the cache or re-fetching a module doesn't touch them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyed by identity, not position.&lt;/strong&gt; The key is module + kind + name + owner path — never a line range. Re-indexing moves line numbers; it doesn't move a trigger's name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift is flagged, never deleted.&lt;/strong&gt; If the source changed after a note was written, the note comes back marked &lt;code&gt;staleAgainstSource&lt;/code&gt;. Silently dropping a colleague's reasoning because a file was edited is the wrong default in every direction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The read tools surface an element's annotations inline, so knowledge accumulated in March shows up in August's session without anyone asking for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the spec is going, and where Kotlin is
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28" rel="noopener noreferrer"&gt;2026-07-28 specification&lt;/a&gt; is a bigger jump than the version string suggests. The protocol core went &lt;strong&gt;stateless&lt;/strong&gt;: the &lt;code&gt;initialize&lt;/code&gt;/&lt;code&gt;initialized&lt;/code&gt; handshake and &lt;code&gt;Mcp-Session-Id&lt;/code&gt; are gone, each request carries its protocol version and client identity in &lt;code&gt;_meta&lt;/code&gt;, and any request can hit any instance behind a load balancer. Server-initiated round trips are replaced by &lt;strong&gt;MRTR&lt;/strong&gt; — a tool returns &lt;code&gt;input_required&lt;/code&gt;, the client retries with &lt;code&gt;inputResponses&lt;/code&gt;. &lt;code&gt;tools/list&lt;/code&gt; and friends gained &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt;. Tasks moved out of the experimental core into a formal &lt;code&gt;io.modelcontextprotocol/tasks&lt;/code&gt; extension. And there's finally a deprecation policy, which immediately put Roots, Sampling, Logging and the legacy HTTP+SSE transport on a twelve-month clock.&lt;/p&gt;

&lt;p&gt;Honest status: &lt;strong&gt;this server speaks &lt;code&gt;2025-11-25&lt;/code&gt;, and that is the newest the Kotlin SDK offers.&lt;/strong&gt; The Tier 1 SDKs (TypeScript, Python, Go, C#) shipped &lt;code&gt;2026-07-28&lt;/code&gt;; Rust is in beta; the Kotlin SDK's &lt;code&gt;LATEST_PROTOCOL_VERSION&lt;/code&gt; is still &lt;code&gt;2025-11-25&lt;/code&gt; as of today. If you build MCP servers in Kotlin, that gap is the thing to plan around — not the thing to be surprised by.&lt;/p&gt;

&lt;p&gt;The good news is that most of the migration is architecture I'd want regardless. The server holds &lt;strong&gt;no per-session state&lt;/strong&gt;: everything durable is on disk, fingerprinted, and addressed by a stable key, which is precisely the shape a stateless core wants. Cacheable list results are a near-free win for a tool list that never changes. The one real casualty is the logging capability — server→client log forwarding is deprecated, so that plumbing is now on a countdown rather than a foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  One protocol detail that will bite you
&lt;/h2&gt;

&lt;p&gt;On stdio, &lt;strong&gt;stdout is the protocol channel.&lt;/strong&gt; Any library that prints a banner, any stray &lt;code&gt;println&lt;/code&gt;, any logger defaulting to console output corrupts the JSON-RPC stream, and the failure looks like a mysterious client-side parse error rather than "something printed." Here every log goes Kermit → SLF4J → Logback → &lt;strong&gt;stderr&lt;/strong&gt;, and the routing is installed before the SDK constructs its first logger. Budget an afternoon for this on any stdio server; it is never the bug you expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Claude Code, two lines and no clone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add aoreshkov/oracle-forms-mcp
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;oracle-forms@oracle-forms-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Desktop gets a one-click &lt;code&gt;.mcpb&lt;/code&gt; bundle from the &lt;a href="https://github.com/aoreshkov/oracle-forms-mcp/releases/latest" rel="noopener noreferrer"&gt;latest release&lt;/a&gt;; everything else can run the GHCR image or the release zip, and it's listed on the &lt;a href="https://registry.modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP Registry&lt;/a&gt; as &lt;code&gt;io.github.aoreshkov/oracle-forms-mcp&lt;/code&gt;. The repo ships sample forms, so you can see the whole pipeline work before pointing it at anything real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server --forms-dir sample-forms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Source (Apache-2.0):&lt;/strong&gt; &lt;a href="https://github.com/aoreshkov/oracle-forms-mcp" rel="noopener noreferrer"&gt;https://github.com/aoreshkov/oracle-forms-mcp&lt;/a&gt; — issues and Forms-XML edge cases especially welcome. If you have a &lt;code&gt;.fmb&lt;/code&gt; this parser chokes on, I want it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>kotlin</category>
      <category>mcp</category>
      <category>oracle</category>
    </item>
    <item>
      <title>Give your AI agent the real sources of any Kotlin/Java library</title>
      <dc:creator>Atanas Oreshkov</dc:creator>
      <pubDate>Thu, 23 Jul 2026 20:02:14 +0000</pubDate>
      <link>https://dev.to/aoreshkov/give-your-ai-agent-the-real-sources-of-any-kotlinjava-library-4kol</link>
      <guid>https://dev.to/aoreshkov/give-your-ai-agent-the-real-sources-of-any-kotlinjava-library-4kol</guid>
      <description>&lt;p&gt;Your AI coding assistant will confidently hand you a Kotlin method signature that does not exist. For a strongly-typed ecosystem, "plausible but wrong" is the worst failure mode — it compiles in your head and breaks in the build.&lt;/p&gt;

&lt;p&gt;Here's it reading the &lt;strong&gt;real&lt;/strong&gt; sources instead:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5bz4208lhuqkmy7su5w.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5bz4208lhuqkmy7su5w.gif" alt=" " width="512" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One prompt. &lt;code&gt;kotlin-lib-mcp&lt;/code&gt; downloaded Ktor's &lt;strong&gt;sources jar&lt;/strong&gt;, parsed it with the Kotlin &lt;strong&gt;Analysis API&lt;/strong&gt; (the same K2/FIR front-end that powers the IDE), and answered from the actual code.&lt;/p&gt;

&lt;h2&gt;
  
  
  One coordinate, ten tools
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch_library("io.ktor:ktor-client-core:3.5.1")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That's the whole setup. The server downloads the sources jar, extracts it, analyzes it once, and caches the parsed index on disk. Your agent then has ten tools over it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;list_packages&lt;/code&gt; / &lt;code&gt;list_declarations&lt;/code&gt; — the public API surface&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_api_signature&lt;/code&gt; — a &lt;strong&gt;type-resolved&lt;/strong&gt; signature (generics, nullability, receiver)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_kdoc&lt;/code&gt; — KDoc as structured data, not an HTML page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_source&lt;/code&gt; / &lt;code&gt;search_source&lt;/code&gt; — the real implementation + bounded search&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_dependencies&lt;/code&gt; — the dependency tree from &lt;code&gt;.pom&lt;/code&gt; / &lt;code&gt;.module&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_versions&lt;/code&gt; / &lt;code&gt;get_latest_version&lt;/code&gt; — resolve "latest stable" from Maven metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pinned to the &lt;strong&gt;exact version you depend on&lt;/strong&gt;, offline after the first fetch. No drift between "the docs" and the version in your build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "resolved" is the whole point
&lt;/h2&gt;

&lt;p&gt;Regex over source gives you a signature-shaped &lt;em&gt;string&lt;/em&gt;. The Analysis API actually ran the compiler front-end — so you get the resolved type, not a guess. When a transitive dependency is missing it degrades to a best-effort PSI signature and flags it &lt;code&gt;bestEffort: true&lt;/code&gt;, instead of failing. Graceful degradation over an empty answer.&lt;/p&gt;

&lt;p&gt;Two Kotlin-specific things made this more than "unzip a jar":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;KMP sources jars are per-target.&lt;/strong&gt; A Multiplatform library publishes &lt;code&gt;-jvm-sources.jar&lt;/code&gt;, a common one, and so on — resolved via &lt;code&gt;.module&lt;/code&gt; Gradle metadata, with every symbol tagged by target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Analysis API is version-fragile.&lt;/strong&gt; Standalone mode must be version-locked to the exact Kotlin release, so it's quarantined behind one &lt;code&gt;SourceAnalyzer&lt;/code&gt; interface.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it in ~60 seconds
&lt;/h2&gt;

&lt;p&gt;Docker, no build required:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude mcp add kotlin-lib -- docker run -i --rm -v kotlin-lib-mcp-cache:/home/mcp/.cache ghcr.io/aoreshkov/kotlin-lib-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Or grab the release zip (Java 21+), or install from the &lt;strong&gt;official MCP registry&lt;/strong&gt; as &lt;code&gt;io.github.aoreshkov/kotlin-lib-mcp&lt;/code&gt;. Then ask your agent to fetch a library and explain an API — it answers from the code.&lt;/p&gt;

&lt;p&gt;It's also a tidy reference build: all three MCP primitives (tools, &lt;strong&gt;resources&lt;/strong&gt; per cached library + a resource template, and a &lt;strong&gt;prompt&lt;/strong&gt;), plus per-tool behavior annotations, typed &lt;code&gt;outputSchema&lt;/code&gt;, progress notifications, and server→client log forwarding. Built on the current stable MCP spec (&lt;code&gt;2025-11-25&lt;/code&gt;); the 2026 roadmap's stateless core and Tasks/Apps extensions are the natural next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/aoreshkov/kotlin-lib-mcp" rel="noopener noreferrer"&gt;https://github.com/aoreshkov/kotlin-lib-mcp&lt;/a&gt; — Apache-2.0. Analysis-API edge cases especially welcome.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Enjoyed this? I also wrote about &lt;a href="https://dev.to/aoreshkov/generate-a-production-grade-kotlin-multiplatform-app-compose-room-3-navigation-3-koin-in-one-n6m"&gt;generating a full Kotlin Multiplatform app in one dialog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>kotlin</category>
      <category>mcp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Generate a production-grade Kotlin Multiplatform app (Compose, Room 3, Navigation 3, Koin) in one dialog</title>
      <dc:creator>Atanas Oreshkov</dc:creator>
      <pubDate>Mon, 20 Jul 2026 20:54:23 +0000</pubDate>
      <link>https://dev.to/aoreshkov/generate-a-production-grade-kotlin-multiplatform-app-compose-room-3-navigation-3-koin-in-one-n6m</link>
      <guid>https://dev.to/aoreshkov/generate-a-production-grade-kotlin-multiplatform-app-compose-room-3-navigation-3-koin-in-one-n6m</guid>
      <description>&lt;h1&gt;
  
  
  Generate a production-grade Kotlin Multiplatform app (Compose, Room 3, Navigation 3, Koin) in one dialog
&lt;/h1&gt;

&lt;p&gt;Starting a Kotlin Multiplatform project has a strange cost curve. The first &lt;code&gt;Hello, world&lt;/code&gt; is cheap — the IDE's built-in wizard gives you a shared module and platform targets in a minute. The next two weeks are where the real setup lives: choosing an architecture, wiring a KMP-ready database, getting type-safe navigation working, setting up DI without service-locator soup, structuring Gradle so the build doesn't rot, and writing enough tests to trust any of it.&lt;/p&gt;

&lt;p&gt;This article walks through one opinionated answer to that setup — the architecture decisions themselves, and why they're baked the way they are. The vehicle is &lt;a href="https://plugins.jetbrains.com/plugin/31786-kmp-project-wizard" rel="noopener noreferrer"&gt;KMP Project Wizard&lt;/a&gt;, an open-source IntelliJ IDEA / Android Studio plugin I built that generates the whole thing from &lt;code&gt;File → New Project&lt;/code&gt;, but every decision below stands on its own — you can apply them by hand to any KMP codebase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft4vvxnhch8hovz8y3nxv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft4vvxnhch8hovz8y3nxv.png" alt=" " width="800" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The reference-app principle: templates that can't go stale by construction
&lt;/h2&gt;

&lt;p&gt;The most important decision isn't in the generated code at all. The templates aren't hand-authored files that drift out of date — they're generated from &lt;a href="https://github.com/aoreshkov/kmp-ledger" rel="noopener noreferrer"&gt;kmp-ledger&lt;/a&gt;, a real, working, MIT-licensed double-entry ledger app that builds, tests, and runs on all three platforms. At plugin build time, concrete names (&lt;code&gt;app.oreshkov.ledger&lt;/code&gt;, &lt;code&gt;Posting&lt;/code&gt;, &lt;code&gt;Ledger&lt;/code&gt;) are replaced with placeholders; at generation time, your names are substituted back in — package, app name, feature name, entity fields, everywhere including directory names.&lt;/p&gt;

&lt;p&gt;The consequence: if the reference app compiles and its tests pass, so does your generated project. "Template rot" — the classic starter-kit disease — becomes a build failure in &lt;em&gt;my&lt;/em&gt; pipeline instead of a runtime surprise in &lt;em&gt;your&lt;/em&gt; project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: Clean layers with a feature API/impl split
&lt;/h2&gt;

&lt;p&gt;The generated project is modular from the first commit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain / Data / UI layers&lt;/strong&gt; per feature, with the dependency arrow always pointing inward — UI and Data both depend on Domain; Domain depends on nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A feature API/impl split&lt;/strong&gt;: each feature exposes a small &lt;code&gt;api&lt;/code&gt; module (public contracts) and keeps its &lt;code&gt;impl&lt;/code&gt; private. Other features depend on &lt;code&gt;api&lt;/code&gt; only, so feature boundaries survive growth and builds stay parallelizable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary-compatibility validation&lt;/strong&gt;: the Kotlin BCV plugin's &lt;code&gt;apiDump&lt;/code&gt; snapshots every module's public ABI, and &lt;code&gt;apiCheck&lt;/code&gt; fails the build when a public contract changes accidentally. (A detail I got wrong the first time: ABI dumps are compiler-output snapshots — symbol names, declaration order, and Compose lambda keys derive from &lt;em&gt;your&lt;/em&gt; names, so shipping the reference app's dumps with names substituted can never match. The wizard instead runs &lt;code&gt;apiDump&lt;/code&gt; after your project's first Gradle sync, so the baseline is generated from your code.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack, and why each piece
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compose Multiplatform&lt;/strong&gt; for shared UI across Android, iOS, and Desktop — one rendering model, platform entry points kept thin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigation 3&lt;/strong&gt; for type-safe routing: destinations are serializable types, not string routes, so a refactor that breaks navigation breaks the &lt;em&gt;compile&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room 3&lt;/strong&gt; for persistence — the KMP-ready generation of Room, meaning the same DAO and entity definitions run on all targets, with SQLite under the hood.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Koin + KSP annotations&lt;/strong&gt; for DI: annotation-driven definitions with compile-time processing, no reflective magic on the hot path, and no hand-maintained module lists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coroutines &amp;amp; Flow&lt;/strong&gt; end to end — repositories expose &lt;code&gt;Flow&lt;/code&gt;, use cases suspend, UI collects with lifecycle awareness.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gradle that scales: convention plugins + version catalog
&lt;/h2&gt;

&lt;p&gt;All build logic lives in convention plugins (&lt;code&gt;build-logic/&lt;/code&gt;), applied by module type — a feature &lt;code&gt;impl&lt;/code&gt; module's build file is a few lines naming its type and dependencies. Versions are centralized in &lt;code&gt;gradle/libs.versions.toml&lt;/code&gt;. Adding a fifteenth module costs the same as adding the second, and a dependency bump is a one-line change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests as part of the template
&lt;/h2&gt;

&lt;p&gt;Every generated project ships a passing suite: unit tests for domain and data (including an in-memory Room setup) and Compose UI tests. Not because sample tests are valuable in themselves, but because a testing &lt;em&gt;pattern&lt;/em&gt; you can copy beats a blank &lt;code&gt;src/test&lt;/code&gt; directory every time you add a feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F32j6szsw1ir6tap3hof9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F32j6szsw1ir6tap3hof9.png" alt=" " width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the one dialog actually asks
&lt;/h2&gt;

&lt;p&gt;Package name, app name, one feature name, one entity with a test value, and target platforms (Android / iOS / Desktop). From that it derives every identifier — PascalCase types, camelCase members, snake_case resources — and renders the whole tree with your names, then triggers Gradle sync and generates the ABI baselines.&lt;/p&gt;

&lt;p&gt;The plugin's core is free (all three platforms, the full architecture and tests). A paid Pro tier adds two things to the generated project: Claude Code agent config (&lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;.claude/&lt;/code&gt; skills) and GitHub Actions CI (&lt;code&gt;.github/&lt;/code&gt;) — flat files, no runtime services, and the same content you can inspect in kmp-ledger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Plugin: &lt;a href="https://plugins.jetbrains.com/plugin/31786-kmp-project-wizard" rel="noopener noreferrer"&gt;https://plugins.jetbrains.com/plugin/31786-kmp-project-wizard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Plugin source (MIT): &lt;a href="https://github.com/aoreshkov/kmp-wizard" rel="noopener noreferrer"&gt;https://github.com/aoreshkov/kmp-wizard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Reference app the templates come from: &lt;a href="https://github.com/aoreshkov/kmp-ledger" rel="noopener noreferrer"&gt;https://github.com/aoreshkov/kmp-ledger&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it, I'd genuinely value feedback — the issue tracker is open, and the architecture opinions above are exactly the kind of thing worth arguing about.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>kmp</category>
      <category>android</category>
      <category>ios</category>
    </item>
  </channel>
</rss>
