[{"data":1,"prerenderedAt":929},["ShallowReactive",2],{"navigation":3,"\u002Fblog\u002Froad-to-1-0-0":334,"\u002Fblog\u002Froad-to-1-0-0-surround":923},[4,43,91,154,181,224,262,293],{"title":5,"path":6,"stem":7,"children":8,"icon":42},"Getting Started","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,17,22,27,32,37],{"title":10,"path":6,"stem":7,"icon":11},"Getting started","i-lucide-flag",{"title":13,"path":14,"stem":15,"icon":16},"Modules overview","\u002Fdocs\u002Fgetting-started\u002Fmodules-overview","1.docs\u002F1.getting-started\u002F2.modules-overview","i-lucide-boxes",{"title":18,"path":19,"stem":20,"icon":21},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","1.docs\u002F1.getting-started\u002F3.installation","i-lucide-download",{"title":23,"path":24,"stem":25,"icon":26},"License configuration","\u002Fdocs\u002Fgetting-started\u002Flicense-configuration","1.docs\u002F1.getting-started\u002F4.license-configuration","i-lucide-key-round",{"title":28,"path":29,"stem":30,"icon":31},"Your first app","\u002Fdocs\u002Fgetting-started\u002Ffirst-app","1.docs\u002F1.getting-started\u002F5.first-app","i-lucide-square-play",{"title":33,"path":34,"stem":35,"icon":36},"Project setup","\u002Fdocs\u002Fgetting-started\u002Fproject-setup","1.docs\u002F1.getting-started\u002F6.project-setup","i-lucide-package",{"title":38,"path":39,"stem":40,"icon":41},"AI assistants","\u002Fdocs\u002Fgetting-started\u002Fai-assistants","1.docs\u002F1.getting-started\u002F7.ai-assistants","i-lucide-bot",false,{"title":44,"path":45,"stem":46,"children":47,"icon":50},"Core Concepts","\u002Fdocs\u002Fconcepts","1.docs\u002F2.concepts\u002F1.index",[48,51,56,61,66,71,76,81,86],{"title":49,"path":45,"stem":46,"icon":50},"Core concepts","i-lucide-book-open",{"title":52,"path":53,"stem":54,"icon":55},"Architecture","\u002Fdocs\u002Fconcepts\u002Farchitecture","1.docs\u002F2.concepts\u002F2.architecture","i-lucide-layers",{"title":57,"path":58,"stem":59,"icon":60},"Exactly-once semantics","\u002Fdocs\u002Fconcepts\u002Fexactly-once","1.docs\u002F2.concepts\u002F3.exactly-once","i-lucide-shield-check",{"title":62,"path":63,"stem":64,"icon":65},"Lanes and parallelism","\u002Fdocs\u002Fconcepts\u002Flanes-and-parallelism","1.docs\u002F2.concepts\u002F4.lanes-and-parallelism","i-lucide-split",{"title":67,"path":68,"stem":69,"icon":70},"State and thread-safety","\u002Fdocs\u002Fconcepts\u002Fstate-and-thread-safety","1.docs\u002F2.concepts\u002F5.state-and-thread-safety","i-lucide-lock",{"title":72,"path":73,"stem":74,"icon":75},"Event time and watermarks","\u002Fdocs\u002Fconcepts\u002Fevent-time-and-watermarks","1.docs\u002F2.concepts\u002F6.event-time-and-watermarks","i-lucide-clock",{"title":77,"path":78,"stem":79,"icon":80},"The configuration model","\u002Fdocs\u002Fconcepts\u002Fconfiguration-model","1.docs\u002F2.concepts\u002F7.configuration-model","i-lucide-sliders-horizontal",{"title":82,"path":83,"stem":84,"icon":85},"The error-handling model","\u002Fdocs\u002Fconcepts\u002Ferror-handling-model","1.docs\u002F2.concepts\u002F8.error-handling-model","i-lucide-triangle-alert",{"title":87,"path":88,"stem":89,"icon":90},"How StoatFlow differs from Kafka Streams","\u002Fdocs\u002Fconcepts\u002Fhow-stoatflow-differs-from-ks","1.docs\u002F2.concepts\u002F9.how-stoatflow-differs-from-ks","i-lucide-arrow-left-right",{"title":92,"path":93,"stem":94,"children":95,"icon":98},"Building Topologies","\u002Fdocs\u002Fbuilding","1.docs\u002F3.building\u002F1.index",[96,99,104,109,114,119,124,129,134,139,144,149],{"title":97,"path":93,"stem":94,"icon":98},"Building topologies","i-lucide-blocks",{"title":100,"path":101,"stem":102,"icon":103},"Error handling and DLQ","\u002Fdocs\u002Fbuilding\u002Ferror-handling-dlq","1.docs\u002F3.building\u002F10.error-handling-dlq","i-lucide-circle-x",{"title":105,"path":106,"stem":107,"icon":108},"State stores","\u002Fdocs\u002Fbuilding\u002Fstate-stores","1.docs\u002F3.building\u002F11.state-stores","i-lucide-database",{"title":110,"path":111,"stem":112,"icon":113},"Testing topologies","\u002Fdocs\u002Fbuilding\u002Ftesting","1.docs\u002F3.building\u002F12.testing","i-lucide-flask-conical",{"title":115,"path":116,"stem":117,"icon":118},"Sources and sinks","\u002Fdocs\u002Fbuilding\u002Fstreams-builder","1.docs\u002F3.building\u002F2.streams-builder","i-lucide-import",{"title":120,"path":121,"stem":122,"icon":123},"KStream and KTable operations","\u002Fdocs\u002Fbuilding\u002Fkstream-ktable","1.docs\u002F3.building\u002F3.kstream-ktable","i-lucide-waypoints",{"title":125,"path":126,"stem":127,"icon":128},"Aggregations","\u002Fdocs\u002Fbuilding\u002Faggregations","1.docs\u002F3.building\u002F4.aggregations","i-lucide-sigma",{"title":130,"path":131,"stem":132,"icon":133},"Windowing","\u002Fdocs\u002Fbuilding\u002Fwindowing","1.docs\u002F3.building\u002F5.windowing","i-lucide-calendar-clock",{"title":135,"path":136,"stem":137,"icon":138},"Joins","\u002Fdocs\u002Fbuilding\u002Fjoins","1.docs\u002F3.building\u002F6.joins","i-lucide-git-merge",{"title":140,"path":141,"stem":142,"icon":143},"The Processor API","\u002Fdocs\u002Fbuilding\u002Fprocessor-api","1.docs\u002F3.building\u002F7.processor-api","i-lucide-cpu",{"title":145,"path":146,"stem":147,"icon":148},"Scheduled sources","\u002Fdocs\u002Fbuilding\u002Fscheduled-sources","1.docs\u002F3.building\u002F8.scheduled-sources","i-lucide-timer",{"title":150,"path":151,"stem":152,"icon":153},"Serdes and Avro","\u002Fdocs\u002Fbuilding\u002Fserdes","1.docs\u002F3.building\u002F9.serdes","i-lucide-binary",{"title":155,"path":156,"stem":157,"children":158,"icon":80},"Configuration","\u002Fdocs\u002Fconfiguration","1.docs\u002F4.configuration\u002F1.index",[159,161,166,171,176],{"title":160,"path":156,"stem":157,"icon":80},"How configuration works",{"title":162,"path":163,"stem":164,"icon":165},"Engine configuration (:core)","\u002Fdocs\u002Fconfiguration\u002Fcore-config","1.docs\u002F4.configuration\u002F2.core-config","i-lucide-settings-2",{"title":167,"path":168,"stem":169,"icon":170},"Runtime configuration (:runtime)","\u002Fdocs\u002Fconfiguration\u002Fruntime-config","1.docs\u002F4.configuration\u002F3.runtime-config","i-lucide-server-cog",{"title":172,"path":173,"stem":174,"icon":175},"Defaults, adaptivity, and presets","\u002Fdocs\u002Fconfiguration\u002Fdefaults-and-presets","1.docs\u002F4.configuration\u002F4.defaults-and-presets","i-lucide-gauge",{"title":177,"path":178,"stem":179,"icon":180},"Kafka client configuration","\u002Fdocs\u002Fconfiguration\u002Fkafka-client-config","1.docs\u002F4.configuration\u002F5.kafka-client-config","i-lucide-plug",{"title":182,"path":183,"stem":184,"children":185,"icon":188},"Running in Production","\u002Fdocs\u002Fruntime","1.docs\u002F5.runtime\u002F1.index",[186,189,194,199,204,209,214,219],{"title":187,"path":183,"stem":184,"icon":188},"The runtime","i-lucide-server",{"title":190,"path":191,"stem":192,"icon":193},"The REST API","\u002Fdocs\u002Fruntime\u002Frest-api","1.docs\u002F5.runtime\u002F2.rest-api","i-lucide-globe",{"title":195,"path":196,"stem":197,"icon":198},"Health checks","\u002Fdocs\u002Fruntime\u002Fhealth-checks","1.docs\u002F5.runtime\u002F3.health-checks","i-lucide-heart-pulse",{"title":200,"path":201,"stem":202,"icon":203},"Metrics","\u002Fdocs\u002Fruntime\u002Fmetrics","1.docs\u002F5.runtime\u002F4.metrics","i-lucide-activity",{"title":205,"path":206,"stem":207,"icon":208},"Pause and resume","\u002Fdocs\u002Fruntime\u002Fpause-unpause","1.docs\u002F5.runtime\u002F5.pause-unpause","i-lucide-pause",{"title":210,"path":211,"stem":212,"icon":213},"Plugins and lifecycle hooks","\u002Fdocs\u002Fruntime\u002Fplugins","1.docs\u002F5.runtime\u002F6.plugins","i-lucide-puzzle",{"title":215,"path":216,"stem":217,"icon":218},"Docker images","\u002Fdocs\u002Fruntime\u002Fdocker","1.docs\u002F5.runtime\u002F7.docker","i-lucide-container",{"title":220,"path":221,"stem":222,"icon":223},"GraalVM native image","\u002Fdocs\u002Fruntime\u002Fnative-image","1.docs\u002F5.runtime\u002F8.native-image","i-lucide-zap",{"title":225,"path":226,"stem":227,"children":228,"icon":231},"Deploying & Operating","\u002Fdocs\u002Foperating","1.docs\u002F6.operating\u002F1.index",[229,232,237,242,247,252,257],{"title":230,"path":226,"stem":227,"icon":231},"Deploying and operating","i-lucide-life-buoy",{"title":233,"path":234,"stem":235,"icon":236},"Running on Kubernetes","\u002Fdocs\u002Foperating\u002Fkubernetes","1.docs\u002F6.operating\u002F2.kubernetes","i-lucide-ship",{"title":238,"path":239,"stem":240,"icon":241},"High availability","\u002Fdocs\u002Foperating\u002Fhigh-availability","1.docs\u002F6.operating\u002F3.high-availability","i-lucide-copy",{"title":243,"path":244,"stem":245,"icon":246},"Liveness and readiness probes","\u002Fdocs\u002Foperating\u002Fprobes","1.docs\u002F6.operating\u002F4.probes","i-lucide-stethoscope",{"title":248,"path":249,"stem":250,"icon":251},"Observability","\u002Fdocs\u002Foperating\u002Fobservability","1.docs\u002F6.operating\u002F5.observability","i-lucide-telescope",{"title":253,"path":254,"stem":255,"icon":256},"Tuning under load","\u002Fdocs\u002Foperating\u002Ftuning","1.docs\u002F6.operating\u002F6.tuning","i-lucide-sliders",{"title":258,"path":259,"stem":260,"icon":261},"Production checklist","\u002Fdocs\u002Foperating\u002Fproduction-checklist","1.docs\u002F6.operating\u002F7.production-checklist","i-lucide-clipboard-check",{"title":263,"path":264,"stem":265,"children":266,"icon":90},"Migrating from Kafka Streams","\u002Fdocs\u002Fmigration","1.docs\u002F7.migration\u002F1.index",[267,268,273,278,283,288],{"title":263,"path":264,"stem":265,"icon":90},{"title":269,"path":270,"stem":271,"icon":272},"Automated port","\u002Fdocs\u002Fmigration\u002Fautomated-port","1.docs\u002F7.migration\u002F2.automated-port","i-lucide-wand-sparkles",{"title":274,"path":275,"stem":276,"icon":277},"Migration without carrying state","\u002Fdocs\u002Fmigration\u002Fwithout-data-migration","1.docs\u002F7.migration\u002F3.without-data-migration","i-lucide-sparkles",{"title":279,"path":280,"stem":281,"icon":282},"Migration carrying state","\u002Fdocs\u002Fmigration\u002Fwith-data-migration","1.docs\u002F7.migration\u002F4.with-data-migration","i-lucide-database-backup",{"title":284,"path":285,"stem":286,"icon":287},"The migration tool","\u002Fdocs\u002Fmigration\u002Fmigration-tool","1.docs\u002F7.migration\u002F5.migration-tool","i-lucide-truck",{"title":289,"path":290,"stem":291,"icon":292},"Reusing your Kafka Streams dashboards","\u002Fdocs\u002Fmigration\u002Freusing-kafka-streams-dashboards","1.docs\u002F7.migration\u002F6.reusing-kafka-streams-dashboards","i-lucide-line-chart",{"title":294,"path":295,"stem":296,"children":297,"icon":299},"Reference","\u002Fdocs\u002Freference","1.docs\u002F8.reference\u002F1.index",[298,300,305,310,315,320,325,329],{"title":294,"path":295,"stem":296,"icon":299},"i-lucide-list",{"title":301,"path":302,"stem":303,"icon":304},"Configuration reference","\u002Fdocs\u002Freference\u002Fconfiguration-reference","1.docs\u002F8.reference\u002F2.configuration-reference","i-lucide-table",{"title":306,"path":307,"stem":308,"icon":309},"REST API reference","\u002Fdocs\u002Freference\u002Frest-api-reference","1.docs\u002F8.reference\u002F3.rest-api-reference","i-lucide-network",{"title":311,"path":312,"stem":313,"icon":314},"Gradle plugin reference","\u002Fdocs\u002Freference\u002Fgradle-plugin-reference","1.docs\u002F8.reference\u002F4.gradle-plugin-reference","i-lucide-box",{"title":316,"path":317,"stem":318,"icon":319},"Maven reference","\u002Fdocs\u002Freference\u002Fmaven-reference","1.docs\u002F8.reference\u002F5.maven-reference","i-simple-icons-apachemaven",{"title":321,"path":322,"stem":323,"icon":324},"Kafka Streams compatibility matrix","\u002Fdocs\u002Freference\u002Fks-compatibility-matrix","1.docs\u002F8.reference\u002F6.ks-compatibility-matrix","i-lucide-table-2",{"title":326,"path":327,"stem":328,"icon":175},"Metrics reference","\u002Fdocs\u002Freference\u002Fmetrics-reference","1.docs\u002F8.reference\u002F7.metrics-reference",{"title":330,"path":331,"stem":332,"icon":333},"Glossary","\u002Fdocs\u002Freference\u002Fglossary","1.docs\u002F8.reference\u002F8.glossary","i-lucide-book-a",{"id":335,"title":336,"authors":337,"badge":343,"body":345,"date":912,"description":913,"draft":42,"extension":914,"image":915,"meta":917,"navigation":918,"path":919,"seo":920,"stem":921,"__hash__":922},"posts\u002F3.blog\u002F12.road-to-1-0-0.md","From first alpha to release candidate: the road to StoatFlow 1.0.0",[338],{"name":339,"to":340,"avatar":341},"Hartmut Armbruster","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fhartmut-co-uk\u002F",{"src":342},"\u002Fassets\u002Fhartmut_armbruster_monochromatic.jpg",{"label":344},"Announcement",{"type":346,"value":347,"toc":902},"minimark",[348,419,422,428,433,464,485,523,542,545,605,608,612,615,637,644,654,657,696,699,703,706,713,716,740,743,747,750,760,779,785,795,798,802,805,820,824,839,842,847,890],[349,350,351,358],"blockquote",{},[352,353,354],"p",{},[355,356,357],"strong",{},"TL;DR",[359,360,361,372,384,407,413],"ul",{},[362,363,364,367,368,371],"li",{},[355,365,366],{},"What:"," StoatFlow ",[355,369,370],{},"1.0.0-rc.1"," is cut — the twenty-seventh release on the 1.0.0 line, and the one that freezes it: from here to general availability, bug fixes only.",[362,373,374,377,378,383],{},[355,375,376],{},"The journey:"," twelve weeks and 26 releases since the ",[379,380,382],"a",{"href":381},"\u002Fblog\u002Fintroducing-stoatflow","first alpha",". Hot standby grew into multi-standby HA with in-place recovery, state stores became bounded, metrics became Kafka Streams–compatible, emission became internally consistent, a rejected output record stopped taking its epoch down with it, and porting gained a codemod, a state-migration tool, and an AI skills pack.",[362,385,386,389,390,396,397,400,401,406],{},[355,387,388],{},"Compatibility:"," ",[379,391,395],{"href":392,"rel":393},"https:\u002F\u002Faxual.github.io\u002Fksml\u002F1.3.0\u002F",[394],"nofollow","KSML"," — an entire framework built on Kafka Streams — now has an experimental StoatFlow integration. A framework exercises what no single application does: every public class, constructor, join variant, handler, and config key. Working through its findings — and our own audits behind them — grew the ",[379,398,399],{"href":322},"compatibility matrix"," from 417 tracked entries to 619, almost all of it around the DSL, not in it. Credit where it is due: ",[379,402,405],{"href":403,"rel":404},"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fdizzl",[394],"Jeroen van Disseldorp",".",[362,408,409,412],{},[355,410,411],{},"The honest part:"," before freezing, we ran three full-codebase review rounds. They surfaced 4 critical and 36 high-severity findings; every one is fixed, and the later rounds re-audited the earlier fixes rather than taking them on trust.",[362,414,415,418],{},[355,416,417],{},"GA:"," no date. The candidate proves out against real workloads first — what remains is proof, not features.",[352,420,421],{},"StoatFlow 1.0.0-rc.1 is cut, and with it the 1.0 feature set is locked. From here to general availability the rule is strict: bug fixes only — no new features, no API changes, no configuration changes. The eight beta releases carried the last of it: a correctness campaign for the most part, and in the final two a handful of behaviour changes we would rather make before a freeze than after one. What separates the candidate from 1.0.0 is proof, not features.",[352,423,424,425,427],{},"This post is the account of the twelve weeks between the ",[379,426,382],{"href":381}," and this candidate: the journey in three acts, the framework integration that sharpened what \"Kafka Streams–compatible\" means here, the bugs we found because we went looking for them — and the ledger of limitations the launch post named, settled item by item.",[429,430,432],"h2",{"id":431},"twelve-weeks-three-acts","Twelve weeks, three acts",[352,434,435,438,439,441,442,446,447,450,451,454,455,459,460,406],{},[355,436,437],{},"Act one — make it port (alpha.1–alpha.13)."," Compatibility was the premise, not the project: at alpha.1 the ",[379,440,399],{"href":322}," already tracked the DSL, the topology API and the state stores method by method, with nothing marked missing. The early alphas worked the level beneath that: the accessors, store suppliers, ",[443,444,445],"code",{},"describe()"," output, interactive-query metadata, and exact overloads that real Kafka Streams code actually calls. Two milestones anchor the act. The in-memory test driver became a drop-in — constructed the Kafka Streams way, so that, as the alpha.11 notes put it, \"a Kafka Streams test suite ports with an import swap\". And configuration followed in alpha.13: build StoatFlow's configuration from a standard Kafka-Streams-keyed ",[443,448,449],{},"Properties"," or ",[443,452,453],{},"Map"," — \"a config that works with Kafka Streams now configures StoatFlow with no changes\". The same act shipped ",[379,456,458],{"href":457},"\u002Fblog\u002Fkip-1271-record-headers-in-state-stores","record headers in state stores"," with an in-place format upgrade, first-class Maven builds alongside Gradle, Flink-style watermark alignment, and ",[379,461,463],{"href":462},"\u002Fblog\u002Fnative-image-g1-pgo-jni-vs-ffm","GraalVM native-image support",[352,465,466,469,470,474,475,479,480,484],{},[355,467,468],{},"Act two — make it operable (alpha.14–alpha.18)."," With porting credible, the middle alphas turned to running the thing. ",[379,471,473],{"href":472},"\u002Fblog\u002Fhot-standby-high-availability","Hot-standby high availability"," arrived as an opt-in active\u002Fpassive pair, then hardened into ",[379,476,478],{"href":477},"\u002Fblog\u002Fin-place-restart-multi-standby","multi-standby clusters with in-place recovery"," — an engine that rebuilds inside the live process instead of bouncing the pod. Windowed, session, sliding, cogroup, and join stores became bounded; the alpha.17 notes name the defect plainly — \"these stores previously grew without limit\" — and that is the act in one line: say what was wrong, fix it, move on. Idle exactly-once applications stopped churning empty commits (roughly 200x fewer idle transactions when quiet). RocksDB internals became observable, and an opt-in metrics mode publishes under the Kafka Streams metric names, so existing Grafana dashboards and alerts keep working. The act closed with alpha.18: ",[379,481,483],{"href":482},"\u002Fblog\u002Finternal-consistency","barrier-consistent emission",", a published OpenRewrite recipe that automates the code port, and licence validation that defers itself out of the way of CI runs.",[352,486,487,490,491,495,496,499,500,503,504,510,511,514,515,518,519,522],{},[355,488,489],{},"Act three — make it correct (beta.1–beta.8)."," The betas were a different kind of work. The beta.1 note says it plainly: \"a top-to-bottom internal review of the entire codebase drove a full pass of fixes, hardening exactly-once behaviour and Kafka Streams compatibility ahead of general availability.\" beta.2 is the correctness release — eleven named fixes plus a sweep of smaller ones, which the review section below returns to. beta.3 shipped the two halves of an assisted migration: the ",[379,492,494],{"href":493},"\u002Fblog\u002Fai-assistant-skills","AI assistant skills pack"," and a ",[379,497,498],{"href":280},"data-migration tool"," that carries an existing application's state across the cutover. beta.4 added ",[443,501,502],{},"processor.wrapper.class"," — decorate every node in a topology, DSL-built and Processor API alike, through the same interface Kafka Streams uses — and published the documentation machine-readably as ",[379,505,507],{"href":506},"\u002Fblog\u002Fllms-txt-machine-readable-docs",[443,508,509],{},"llms.txt",". beta.5 turned an undiagnosed RocksDB startup failure into a supported, explained downgrade path for header-format stores — one Kafka Streams refuses outright — and closed a metrics audit: every meter documented, every documented meter real. And beta.6 closed the range-query surface: open-ended (",[443,512,513],{},"null",") bounds now behave across every store type the way Kafka Streams treats them, and ",[443,516,517],{},"reverseRange"," takes its bounds low-first the way Kafka Streams does — passed the other way round it had been quietly returning the wrong answer rather than complaining. The same release stopped the changelog topics behind headers-enabled window, session and versioned stores growing without limit; deployments already carrying one need a manual ",[443,520,521],{},"kafka-configs --alter"," to recover the space.",[352,524,525,526,529,530,533,534,537,538,541],{},"Then two more releases, and neither was polish. beta.7 stopped splitting a topology at every key change and started splitting it where Kafka Streams would — ",[443,527,528],{},"selectKey → mapValues → to()"," is one sub-topology now, not two — and made unprefixed Kafka client properties actually apply: a ported application's ",[443,531,532],{},"security.protocol"," and ",[443,535,536],{},"sasl.*"," had been recognised, warned about, and then ignored, so it connected unauthenticated and unencrypted. It also closed a race between closing a RocksDB store and flushing it that took the whole JVM down with a ",[443,539,540],{},"SIGSEGV"," — on the standby, which meant a promotion could kill the instance meant to replace the active one. beta.8 was the largest release on the line, and its centre is the poison-epoch rewrite: a record the broker rejects poisons the transaction it sits in, and StoatFlow used to abort that transaction and advance past it, losing every innocent record that happened to share the epoch. Now the source offsets are held, the epoch is replayed with the one bad record quarantined, and its epoch-mates commit normally. Alongside it, the dead letter for an oversized record became deliverable at all: it had been assembled larger than the record the broker had just refused, so a DLQ topic sized like the output topic refused it too — nothing was ever written, in precisely the case a DLQ exists for. Two more from that release deserve naming, because neither was visible from outside: an engine restart arriving mid-commit could break exactly-once, silently; and an at-least-once topology with no sink committed nothing at all — no offsets, no changelog — so every restart replayed its input from the beginning. The one deliberate tightening in the set: a Processor API processor may now only reach the stores it declared, where before it could quietly read someone else's.",[352,543,544],{},"The numbers underneath the acts, for the record:",[546,547,548,561],"table",{},[549,550,551],"thead",{},[552,553,554,558],"tr",{},[555,556,557],"th",{},"The number",[555,559,560],{},"What it measures",[562,563,564,573,581,589,597],"tbody",{},[552,565,566,570],{},[567,568,569],"td",{},"27 releases on the 1.0.0 line (18 alphas, 8 betas, one candidate)",[567,571,572],{},"a release roughly every three days, for twelve weeks",[552,574,575,578],{},[567,576,577],{},"1,086 commits, 94 merged pull requests",[567,579,580],{},"the volume of change between alpha.1 and the candidate",[552,582,583,586],{},[567,584,585],{},"212 → 586 test files, 3,136 → 5,721 tests",[567,587,588],{},"4,353 unit, 840 end-to-end through the in-memory test driver, 528 integration against real brokers and real builds",[552,590,591,594],{},[567,592,593],{},"417 → 619 tracked entries in the compatibility matrix",[567,595,596],{},"the portability surface, pinned method by method — next section",[552,598,599,602],{},[567,600,601],{},"4 critical + 36 high review findings, all fixed",[567,603,604],{},"three full-codebase review rounds — two sections down",[352,606,607],{},"One caution about reading that table: volume is evidence of effort, not of correctness. Nothing in a commit count says the engine is right. That claim belongs to the two sections that follow — the integration that tried to break compatibility from the outside, and the reviews that tried to break the engine from the inside.",[429,609,611],{"id":610},"the-integration-that-kept-us-honest","The integration that kept us honest",[352,613,614],{},"The most useful thing that happened to StoatFlow in these twelve weeks was another engineer's test suite.",[352,616,617,618,621,622,627,628,631,632,636],{},"Early in the alpha, ",[379,619,405],{"href":403,"rel":620},[394]," — CTO of ",[379,623,626],{"href":624,"rel":625},"https:\u002F\u002Faxual.com\u002F",[394],"Axual"," and creator of ",[379,629,395],{"href":392,"rel":630},[394]," — began integrating StoatFlow as an alternative engine underneath KSML. KSML is an open-source framework that builds Kafka Streams applications from YAML and Python, without writing Java. That is not porting ",[633,634,635],"em",{},"an application",": it means swapping the engine underneath an entire framework and running the framework's own test suite against the replacement. The integration is experimental — an honest effort to support StoatFlow as a second engine, and to test how far Kafka Streams portability really goes.",[352,638,639,640,643],{},"It is also the hardest compatibility exercise we could have asked for. A framework touches essentially the whole public surface: all the operators, not one topology's worth; every constructor and overload, because a framework cannot pick the convenient one; the exception handlers, the lifecycle state machine, the configuration surface down to individual keys, the serde boundaries, and the test harness its own suite runs on. KSML's dynamic data model reaches cases hand-written Java never produces — a key that is not null but ",[633,641,642],{},"serialises"," to null bytes, serdes chosen at runtime, topic names from a custom extractor. The findings arrived in rounds through June; our own porting audits kept pulling on the same threads well after.",[352,645,646,647,649,650,653],{},"The measure of that work is the public ",[379,648,399],{"href":322},". At alpha.1 it tracked 417 methods and behaviours across 62 API surfaces, and none was marked \"not implemented\" — the operator core was compatible on day one; that was the premise of the product. Twelve weeks later it pins 619 entries across 72 surfaces, and the growth sits exactly where a framework lives and an application port rarely looks: all 74 ",[443,651,652],{},"StreamsConfig"," keys of Kafka Streams 4.3 accounted for — mapped, passed through to the Kafka clients, or documented as moot on a single instance; the test harness pinned method by method, all green; topology description, interactive-query metadata, exception handlers, lifecycle, each from present to pinned. The early waves were about API shape — does Kafka Streams–idiom code compile? The later ones were about execution fidelity — does it behave, configure, observe, and test the same?",[352,655,656],{},"Some of what came out you have already met: the drop-in test driver and the Kafka Streams config model in act one both trace back to these rounds. The rest reshaped the surfaces around the DSL:",[359,658,659,672,690],{},[362,660,661,389,664,667,668,671],{},[355,662,663],{},"Runtime behaviour.",[443,665,666],{},"StreamsUncaughtExceptionHandler.REPLACE_THREAD"," stopped being a documented downgrade and became a real recovery — the ",[379,669,670],{"href":477},"in-place engine restart",". Null keys are dropped before aggregation exactly as Kafka Streams drops them, across all nineteen aggregation operators.",[362,673,674,677,678,682,683,686,687,689],{},[355,675,676],{},"Handlers and interop."," The ",[379,679,681],{"href":680},"\u002Fblog\u002Ferror-handling-three-gates-one-transaction","error-handling surface"," follows the Kafka Streams handler shape; a custom ",[443,684,685],{},"KafkaClientSupplier"," uses the exact Kafka Streams signature; ",[443,688,445],{}," prints the Kafka Streams topology description, character for character where tooling depends on it.",[362,691,692,695],{},[355,693,694],{},"Tooling."," The port path itself got tooled: the OpenRewrite recipe rewrites the code, the migration tool carries the state, and the skills pack keeps AI assistants writing StoatFlow rather than half-remembered Kafka Streams.",[352,697,698],{},"The integration also exposed a process weakness we did not like: divergences were being discovered by a person, not by our CI. That is now guarded — build gates compile Kafka Streams–idiom Java against the artefacts we actually publish, so the next divergence has to get past a machine before it can reach anyone's port.",[429,700,702],{"id":701},"we-went-looking","We went looking",[352,704,705],{},"Compatibility is one axis of confidence. The other is whether the engine is right — and for that we did not wait for reports.",[352,707,708,709,712],{},"Before freezing the surface we ran three full-codebase review rounds, each one reading the engine end to end against a fixed set of lenses: correctness, exactly-once semantics, thread safety, crash windows. Between them they surfaced ",[355,710,711],{},"four critical and thirty-six high-severity findings. Every one is fixed"," — and because a fix you have not re-verified is just a claim, the later rounds re-audited the earlier rounds' fixes rather than taking them on trust.",[352,714,715],{},"The beta.2 release notes are what that looked like from the outside: eleven named fixes plus a sweep. Three are worth retelling for what they teach:",[359,717,718,728,734],{},[362,719,720,723,724,727],{},[355,721,722],{},"KTable-KTable join retractions."," A non-materialised join did not forward the ",[443,725,726],{},"(key, null)"," retraction when one side was deleted — a downstream store or compacted topic kept the stale pre-delete join result forever. Correct-looking output, permanently wrong at rest.",[362,729,730,733],{},[355,731,732],{},"Versioned-store reads during commits."," A versioned store read could return a stale older version for the whole commit window. The rewrite that fixed it also made the read path measurably faster — the fix and the optimisation were the same change.",[362,735,736,739],{},[355,737,738],{},"Foreign-key join consistency under exactly-once."," When the two sides of a foreign-key join process concurrently, an intermediate update could — around a crash — go permanently missing from the output stream. Final values always converged, which is precisely why nobody would have noticed.",[352,741,742],{},"None of the three arrived as a field report. All of them came out of reviews that went looking — which is the point. A release candidate is not a claim that no bugs remain; it is the evidence of having gone looking, three times over, with the findings written down and closed.",[429,744,746],{"id":745},"the-ledger-from-may","The ledger from May",[352,748,749],{},"The launch post made claims, and — deliberately — named its own limitations. A release candidate should answer for both.",[352,751,752,755,756,759],{},[355,753,754],{},"\"State migration is a reprocess, not a restore\" — closed."," In May, the recommended path onto StoatFlow was to reprocess your input topics; if retention ruled that out, we asked you to get in touch. Now the ",[379,757,758],{"href":280},"migration tool"," translates a Kafka Streams application's changelog topics into StoatFlow's format and carries the input offsets across, so a stateful cutover resumes exactly where Kafka Streams stopped — reading the old application's topics, never modifying them.",[352,761,762,765,766,769,770,773,774,778],{},[355,763,764],{},"\"Failover behaviour is a fair question\" — answered, with measurements."," The launch post admitted it had no failover story. It now has one with numbers attached: ",[379,767,768],{"href":472},"hot standby",", grown into multi-standby clusters with a lag-aware election and ",[379,771,772],{"href":477},"in-place recovery",", and ",[379,775,777],{"href":776},"\u002Fblog\u002Fha-failover-testing","measured scenario by scenario from the pod logs",". \"The docs are still being written\" no longer applies either — the documentation now runs from getting started through operations and reference.",[352,780,781,784],{},[355,782,783],{},"The single-machine ceiling — stands."," On the benchmarked 8-vCPU machine it is 200–300 MB\u002Fs of uncompressed throughput; higher-end hardware remains unbenchmarked. That number moved nowhere in twelve weeks because we spent them elsewhere. It is still the honest boundary of the design.",[352,786,787,790,791,794],{},[355,788,789],{},"No horizontal scale-out — stands, by design."," Twenty-six releases changed a great deal, and none of it touched the bet the product is built on: one instance per application, and no ",[633,792,793],{},"distribution tax"," for scale-out your workload does not need. Standbys are redundancy, not throughput. To go faster, scale up.",[352,796,797],{},"Two closed, two standing — and the two that stand are the product, not the gap.",[429,799,801],{"id":800},"what-a-release-candidate-means-here-and-whats-next","What a release candidate means here — and what's next",[352,803,804],{},"Feature-locked is a discipline, not a mood. Between rc.1 and 1.0.0 the only changes are bug fixes; the API, the configuration surface, and the wire formats hold still. There is no GA date to announce, deliberately: the candidate now proves out against real workloads — ports, soak runs, failure drills — and 1.0.0 follows the evidence, not a calendar.",[352,806,807,808,811,812,815,816,819],{},"Locking 1.0 also frees the thinking past it. Three items lead the 1.1 line: ",[355,809,810],{},"Flink-style side outputs",", so a processor can route to multiple named outputs instead of contorting a topology around one; an ",[355,813,814],{},"async I\u002FO processor",", for calling slow external services at high parallelism without giving up per-key ordering; and ",[355,817,818],{},"interactive queries over REST",", reading state stores over HTTP without writing a server. Behind those sits a bench that is designed and reviewed but not scheduled: multi-table joins, CEP pattern recognition, read-only queries served from hot standbys. Directions, not commitments — the only commitment right now is the freeze.",[429,821,823],{"id":822},"credits","Credits",[352,825,826,827,830,831,834,835,406],{},"A warm special thanks to ",[379,828,405],{"href":403,"rel":829},[394],", CTO of ",[379,832,626],{"href":624,"rel":833},[394],", who built the experimental KSML integration and filed findings against us round after round. The compatibility half of this story owes its precision to those reports — the matrix is as sharp as it is because of them. If you want to see what a framework on top of Kafka Streams looks like, ",[379,836,838],{"href":392,"rel":837},[394],"KSML is worth your time",[352,840,841],{},"Thanks as well to everyone who tested StoatFlow, put it through an evaluation, or reviewed our work and told us what was wrong. rc.1 is better for it.",[352,843,844],{},[355,845,846],{},"Read on:",[359,848,849,855,868,874,880,886],{},[362,850,851,854],{},[379,852,853],{"href":381},"StoatFlow: Kafka Streams compatible engine built to scale up — not out"," — the first alpha, and the ledger this post settles.",[362,856,857,860,861,773,864,867],{},[379,858,859],{"href":472},"Hot standby",", ",[379,862,863],{"href":776},"measuring failover",[379,865,866],{"href":477},"in-place restart + multi-standby"," — the availability arc.",[362,869,870,873],{},[379,871,872],{"href":482},"Internal consistency on Kafka"," — barrier-consistent emission, measured to zero.",[362,875,876,879],{},[379,877,878],{"href":680},"Three gates, one transaction"," — the error-handling model.",[362,881,882,885],{},[379,883,884],{"href":264},"Migration"," — the tooled port path, code and state.",[362,887,888,406],{},[379,889,10],{"href":6},[352,891,892,893,897,898,406],{},"For what happens between here and 1.0.0, ",[379,894,896],{"href":895},"\u002Fcontact","reach out"," — and for the running commentary on how StoatFlow gets built, ",[379,899,901],{"href":340,"rel":900},[394],"follow along on LinkedIn",{"title":903,"searchDepth":904,"depth":904,"links":905},"",2,[906,907,908,909,910,911],{"id":431,"depth":904,"text":432},{"id":610,"depth":904,"text":611},{"id":701,"depth":904,"text":702},{"id":745,"depth":904,"text":746},{"id":800,"depth":904,"text":801},{"id":822,"depth":904,"text":823},"2026-08-04","StoatFlow 1.0.0-rc.1 is cut and the feature set is frozen — what separates the candidate from GA is proof, not features. The twelve weeks from the first alpha: 26 releases, 1,086 commits, a compatibility matrix grown from 417 to 619 tracked entries, three full-codebase review rounds — and the framework integration that kept us honest.","md",{"src":916},"\u002Fassets\u002Fblog\u002Fog\u002Froad-to-1-0-0.png",{},true,"\u002Fblog\u002Froad-to-1-0-0",{"title":336,"description":913},"3.blog\u002F12.road-to-1-0-0","UDqbZQrHLEHb0qvzWPqwgLUT6cAGT_pFE86OG9bUH0w",[924,925],null,{"title":926,"path":680,"stem":927,"description":928,"children":-1},"Three gates, one transaction: error handling in StoatFlow","3.blog\u002F11.error-handling-three-gates-one-transaction","A record can fail on the way in, in the middle, or on the way out. StoatFlow gives every gate the same verdicts — continue, fail, or dead-letter — and settles them all inside the exactly-once transaction: DLQ records commit on the same barrier as your output, and what cannot be handled kills the epoch, never the guarantee. The full model, from one bad record to a Kubernetes restart.",1786987415680]