blog
Notes on Kotlin, Kotlin/Native, and whatever I'm currently building.
A profile can be taken and applied on stock tools, with no fork. Turning it into a binary means going around the compiler's own LTO, which costs nothing measurable on a benchmark — and the profile returns 10–13 % there. What that leaves unanswered for a service, and the recipe.
Clone a template, rename it, and you have a service that compiles to a native Linux binary and a JVM distribution from one source: two probes-and-shutdown halves that behave the same, a 14 MB image, and 3 min 48 s from git clone to both answering. Every number here was measured on a machine that had never seen the repository.
Resident memory on Kotlin/Native is the thread count times half a megabyte. A forked Ktor CIO on a fixed thread pool holds 200 connections on eight threads and 16 MB — fewer threads and a lower p99 than Go on the same machine.
A Ktor service that was OOM-killed in every run at 128 MiB now serves 2 000 rps there in 61 MB at a p99 of 5.5 ms. Two changes, neither of them a GC setting.
A statically linked Kotlin/Native binary that starts in an empty image and still resolves hostnames, the exact flags that produce it on a stock 2.4.10, what it costs in size, RSS and cold start — and the four files a Ktor service has to put back, because glibc dlopens its character-set converters.
One symbol table answers both questions. Every expectation I brought to it was wrong: the runtime is a 40 KB constant, a fifth of the file is symbol names, most of a release binary is not Kotlin, and a sampling profiler written in Kotlin hangs the process it profiles.
129 probes run on both runtimes: what agrees exactly, the seventeen rows that do not, why none of those seventeen has ever caused an outage, and why the ones that did are harder to test for.
The same service on CIO, Netty and Jetty, one -D apart: 166, 82 and 146 microseconds of CPU per request. A third of CIO's CPU is the queue inside Dispatchers.IO; one system property removes most of it and costs 7.7x on a handler that blocks. The stand, the numbers and the limits.
A CRaC checkpoint of a real Ktor service restores in 131 ms against 2 317 and its JIT is warm. Getting there means every open socket, and HikariCP names its own. What breaks, in the order it breaks, with the logs.
How to give a Ktor, http4k or any application-plugin service a Project Leyden AOT cache on JDK 25: what the cache is and is not, what it did to a real service's cold start and memory, the training, verification and packaging that Spring Boot and Quarkus hide, the same by hand in five steps, and through a Gradle plugin.
I set out to build a Kotlin/JVM optimiser plugin and measured first. User code owns 1–4 % of CPU and 3–10 % of allocations on a Ktor service, R8 cannot even run as the baseline on this stack, and the plugin was not built. The numbers and the stand.
On JDK 25 before 25.0.4 and JDK 26 before 26.0.2 the JVM does not compare an AOT cache with the jars it was trained against: replace a jar, keep the cache, and the old classes run. The experiment, the logs, and what to check yourself.
A ktlint rule set with no configuration, where every rule encodes one class of defect this stack paid to find — and the two that did not survive being measured.
kompot-studio opens a screen body beside the renderers that will draw it: a tree, a JSON editor, a schema-driven inspector, a preview in the consumer's own brand frame, and findings from schema, rules, vocabulary and the render itself.
How kvadrant-ui got built: a design language I missed, the Microsoft files that turned out to still be readable, and the one effect I built from a name instead of a document.
kompot: the server describes a screen as a tree of components, the client renders it, and a new screen ships without a client release. Open hierarchies, KSP registration, and two conformance kits.
Durability that 779 tests could not see, a fuzzer that needed two files to get through the door, and a harness that reported targets it never ran.
Since 0.3.0 viddik reads androidx.compose.ui.tooling.preview.Preview, so one annotation serves the IDE preview pane, Android's screenshot tooling and the golden-file suite at once.
Kobweb has no server-side rendering and its issue for one has been open since 2022. This is an investigation into how much of it can be built from outside the framework, what it costs, and which part turns out to be impossible without a change in Compose HTML.
What a platform actually needs from an IdP, what that costs when it ships as one native binary, and where the measurement stops being a comparison.
A lock that only ever answered yes, an out-of-scope label with eight defects behind it, and a benchmark the optimiser deleted.
Documentation formats for coding agents are written as prose rules that nothing verifies. docs-bootstrap is the same idea with checks that can fail, run by CI against the repository's own example.
Writing an S3 client for Kotlin/Native. Three things that only turned up once a real request went out, and one that a green test suite was hiding.
28 ms to first response and 20 MiB idle. Then the load arrived — and three of my own measurements turned out to be wrong.
294 tests, 71 specification scenarios, and four bugs that only a real mongod would show — because none of them threw anything.
Handing a coding agent a dashboard link is handing it nothing. tracy makes MCP the primary read path — and that single decision changes what the storage has to store.
booblik reproduces what makes an append-only log fast and leaves out everything that makes Kafka a cluster — including three features that are incompatible with zero-copy by construction.
metrik costs the monitored service 106 nanoseconds per request and a UDP packet a minute — and the reason it merges percentiles instead of averaging them is that averaging them is simply wrong.
A saga engine built around one question. Steps as interceptors, compensation in reverse, and an outbox that makes "the work happened but the notification never went out" structurally impossible.
A Kotlin/Native service has two ways to send mail — shell out to sendmail, or wrap libcurl. Both hide the protocol exactly where it has to be visible.
mani is a Kotlin Multiplatform demo where the same server compiles to the JVM and to a native binary — and where the picture advertising the interface cannot drift from it.
telek models a conversation as pure transitions with effects on the side — which is what makes a wizard resumable, testable and portable between two transports.
katcher — Kotlin/Native, Ktor and HTMX, in one binary small enough that nobody has to justify running it.
viddik renders Compose Multiplatform through a real Compose Desktop window instead of LayoutLib, so goldens stay the same on macOS, Linux and Windows.
A custom title bar for Compose Desktop has to know which side the buttons go on. On Linux there is no answer — so appframe asks GNOME directly.