[{"data":1,"prerenderedAt":1783},["ShallowReactive",2],{"navigation":3,"\u002Fblog\u002Finternal-consistency":334,"\u002Fblog\u002Finternal-consistency-surround":1772},[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":1762,"description":1763,"draft":42,"extension":1764,"image":1765,"meta":1767,"navigation":560,"path":1768,"seo":1769,"stem":1770,"__hash__":1771},"posts\u002F3.blog\u002F8.internal-consistency.md","Internal consistency on Kafka: emitting a correct answer at every commit",[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},"Deep Dive",{"type":346,"value":347,"toc":1745},"minimark",[348,467,480,485,492,774,796,805,809,826,833,850,853,862,875,900,911,915,918,950,961,976,983,988,991,1009,1013,1037,1044,1051,1055,1058,1085,1091,1120,1137,1141,1173,1178,1272,1298,1315,1330,1424,1438,1453,1462,1465,1468,1479,1482,1485,1488,1491,1495,1499,1502,1505,1509,1512,1519,1533,1537,1539,1545,1548,1552,1564,1575,1593,1596,1610,1614,1617,1679,1686,1689,1699,1726,1729,1741],[349,350,351,358],"blockquote",{},[352,353,354],"p",{},[355,356,357],"strong",{},"TL;DR",[359,360,361,388,427,446,457],"ul",{},[362,363,364,367,368,372,373,376,377,380,381,387],"li",{},[355,365,366],{},"The test:"," a stream of ",[369,370,371],"code",{},"$1"," transfers between 10 accounts. ",[369,374,375],{},"credits − debits = balance"," per account; ",[369,378,379],{},"total = Σ balance",". Money is only moved, so ",[355,382,383,386],{},[369,384,385],{},"total"," must be 0 at every consistent cut",".",[362,389,390,393,394,397,398,400,401,404,405,408,409,412,413,415,416,419,420,423,424,387],{},[355,391,392],{},"What everyone else does:"," the ",[355,395,396],{},"Flink Table API"," emits 37,978,385 ",[369,399,385],{}," values of which ",[355,402,403],{},"13,325 are correct — 0.035%","; ",[355,406,407],{},"ksqlDB"," emits 340, of which only the last is right; our own ",[355,410,411],{},"Kafka Streams"," twin sends ",[369,414,385],{}," to ",[355,417,418],{},"−1,619 … +1,792"," and emits ",[355,421,422],{},"160 impossible balances"," on a dataset where the only valid values are ",[369,425,426],{},"{−1, 0, +1}",[362,428,429,432,433,436,437,432,440,445],{},[355,430,431],{},"What StoatFlow does:"," ",[355,434,435],{},"zero impossible balances in every mode",", and under ",[369,438,439],{},"emit.mode = on-barrier",[355,441,442,444],{},[369,443,385],{}," is exactly 0 at every committed cut"," — measured on a 5-node cluster at 2,000 tx\u002Fs, verified across churn and unique-key workloads.",[362,447,448,451,452,456],{},[355,449,450],{},"Why:"," the commit barrier is a genuine Chandy–Lamport cut aligned to Kafka offsets, and the cascade-join withholds it from a downstream operator until ",[453,454,455],"em",{},"every"," contributing chain has pushed its epoch into it. That cross-stream synchronization is exactly what the other engines lack.",[362,458,459,462,463,466],{},[355,460,461],{},"The honest part:"," getting to ",[453,464,465],{},"perfect"," took one real bug fix (a barrier-cut race across sub-topology boundaries). We found it, root-caused it, fixed it, and re-ran the benchmark to zero.",[352,468,469,470,479],{},"This post is about a property Jamie Brandon named ",[471,472,476],"a",{"href":473,"rel":474},"https:\u002F\u002Fwww.scattered-thoughts.net\u002Fwriting\u002Finternal-consistency-in-streaming-systems\u002F",[475],"nofollow",[355,477,478],{},"internal consistency",", the fact that no engine with a Kafka Streams–shaped API has it, and the measured demonstration that StoatFlow does.",[481,482,484],"h2",{"id":483},"the-money-test","The money test",[352,486,487,488,491],{},"Take the simplest invariant in accounting: ",[355,489,490],{},"money is only moved, never created or destroyed."," Model it as a stream.",[493,494,499],"pre",{"className":495,"code":496,"language":497,"meta":498,"style":498},"language-kotlin shiki shiki-themes vitesse-light","\u002F\u002F transactions: (from, to) — each moves $1\nval tx = builder.stream\u003CString, Transfer>(\"transactions\")\n\nval credits = tx.map { _, t -> KeyValue(t.to,   1L) }.groupByKey().reduce { a, b -> a + b }   \u002F\u002F received\nval debits  = tx.map { _, t -> KeyValue(t.from, 1L) }.groupByKey().reduce { a, b -> a + b }   \u002F\u002F sent\n\nval balance = credits.outerJoin(debits) { c, d -> (c ?: 0) - (d ?: 0) }   \u002F\u002F per-account net\nval total   = balance.groupBy { _, b -> KeyValue(\"ALL\", b) }.reduce({ a, b -> a + b }, { a, b -> a - b })\n","kotlin","",[369,500,501,510,555,562,624,670,675,719],{"__ignoreMap":498},[502,503,506],"span",{"class":504,"line":505},"line",1,[502,507,509],{"class":508},"s8zF2","\u002F\u002F transactions: (from, to) — each moves $1\n",[502,511,513,517,521,525,528,532,535,539,542,545,548,552],{"class":504,"line":512},2,[502,514,516],{"class":515},"sbBg2","val",[502,518,520],{"class":519},"suHK_"," tx ",[502,522,524],{"class":523},"sYZai","=",[502,526,527],{"class":519}," builder.",[502,529,531],{"class":530},"sySUi","stream",[502,533,534],{"class":519},"\u003C",[502,536,538],{"class":537},"sUxyF","String",[502,540,541],{"class":519},", ",[502,543,544],{"class":537},"Transfer",[502,546,547],{"class":519},">(",[502,549,551],{"class":550},"spphp","\"transactions\"",[502,553,554],{"class":519},")\n",[502,556,558],{"class":504,"line":557},3,[502,559,561],{"emptyLinePlaceholder":560},true,"\n",[502,563,565,567,570,572,575,578,581,584,587,590,594,597,600,603,606,609,611,614,618,621],{"class":504,"line":564},4,[502,566,516],{"class":515},[502,568,569],{"class":519}," credits ",[502,571,524],{"class":523},[502,573,574],{"class":519}," tx.",[502,576,577],{"class":530},"map",[502,579,580],{"class":519}," { _, t ",[502,582,583],{"class":523},"->",[502,585,586],{"class":530}," KeyValue",[502,588,589],{"class":519},"(t.to,   ",[502,591,593],{"class":592},"s-TwI","1L",[502,595,596],{"class":519},") }.",[502,598,599],{"class":530},"groupByKey",[502,601,602],{"class":519},"().",[502,604,605],{"class":530},"reduce",[502,607,608],{"class":519}," { a, b ",[502,610,583],{"class":523},[502,612,613],{"class":519}," a ",[502,615,617],{"class":616},"si04Y","+",[502,619,620],{"class":519}," b }   ",[502,622,623],{"class":508},"\u002F\u002F received\n",[502,625,627,629,632,634,636,638,640,642,644,647,649,651,653,655,657,659,661,663,665,667],{"class":504,"line":626},5,[502,628,516],{"class":515},[502,630,631],{"class":519}," debits  ",[502,633,524],{"class":523},[502,635,574],{"class":519},[502,637,577],{"class":530},[502,639,580],{"class":519},[502,641,583],{"class":523},[502,643,586],{"class":530},[502,645,646],{"class":519},"(t.from, ",[502,648,593],{"class":592},[502,650,596],{"class":519},[502,652,599],{"class":530},[502,654,602],{"class":519},[502,656,605],{"class":530},[502,658,608],{"class":519},[502,660,583],{"class":523},[502,662,613],{"class":519},[502,664,617],{"class":616},[502,666,620],{"class":519},[502,668,669],{"class":508},"\u002F\u002F sent\n",[502,671,673],{"class":504,"line":672},6,[502,674,561],{"emptyLinePlaceholder":560},[502,676,678,680,683,685,688,691,694,696,699,702,705,708,711,713,716],{"class":504,"line":677},7,[502,679,516],{"class":515},[502,681,682],{"class":519}," balance ",[502,684,524],{"class":523},[502,686,687],{"class":519}," credits.",[502,689,690],{"class":530},"outerJoin",[502,692,693],{"class":519},"(debits) { c, d ",[502,695,583],{"class":523},[502,697,698],{"class":519}," (c ?: ",[502,700,701],{"class":592},"0",[502,703,704],{"class":519},") ",[502,706,707],{"class":616},"-",[502,709,710],{"class":519}," (d ?: ",[502,712,701],{"class":592},[502,714,715],{"class":519},") }   ",[502,717,718],{"class":508},"\u002F\u002F per-account net\n",[502,720,722,724,727,729,732,735,738,740,742,745,748,751,753,756,758,760,762,765,767,769,771],{"class":504,"line":721},8,[502,723,516],{"class":515},[502,725,726],{"class":519}," total   ",[502,728,524],{"class":523},[502,730,731],{"class":519}," balance.",[502,733,734],{"class":530},"groupBy",[502,736,737],{"class":519}," { _, b ",[502,739,583],{"class":523},[502,741,586],{"class":530},[502,743,744],{"class":519},"(",[502,746,747],{"class":550},"\"ALL\"",[502,749,750],{"class":519},", b) }.",[502,752,605],{"class":530},[502,754,755],{"class":519},"({ a, b ",[502,757,583],{"class":523},[502,759,613],{"class":519},[502,761,617],{"class":616},[502,763,764],{"class":519}," b }, { a, b ",[502,766,583],{"class":523},[502,768,613],{"class":519},[502,770,707],{"class":616},[502,772,773],{"class":519}," b })\n",[352,775,776,779,780,782,783,785,786,788,789,792,793,795],{},[369,777,778],{},"balance"," for any account is always in ",[369,781,426],{}," for a ring of ",[369,784,371],{}," transfers. And ",[369,787,385],{},", the sum over all accounts, is ",[355,790,791],{},"always 0"," — every credit has a matching debit. That is the invariant. It is not an approximation that holds \"eventually\"; it holds at ",[453,794,455],{}," point in time, because it is a conservation law.",[352,797,798,799,801,802],{},"So here is the question: when your streaming engine emits a ",[369,800,385],{}," value, ",[355,803,804],{},"is it 0?",[481,806,808],{"id":807},"what-internal-consistency-means","What \"internal consistency\" means",[352,810,811,812,815,816,818,819,822,823,825],{},"Brandon's definition: a system is ",[355,813,814],{},"internally consistent"," if ",[453,817,455],{}," output it produces is the correct output for ",[355,820,821],{},"some"," subset of the inputs it has seen so far. Not the latest subset, necessarily — but ",[453,824,471],{}," coherent one. Every emitted row is a photograph of a real moment, never a double-exposure that blends two.",[352,827,828,829,832],{},"This is ",[355,830,831],{},"strictly stronger than eventual consistency",", and the two don't imply each other. An eventually-consistent system is allowed to emit nonsense in the meantime as long as it converges. An internally consistent system is never allowed to emit nonsense at all — every intermediate output is itself a valid answer.",[352,834,835,836,842,843,846,847,387],{},"For the money test, internal consistency means: ",[355,837,838,839,841],{},"every emitted ",[369,840,385],{}," is 0."," If you ever see ",[369,844,845],{},"total = 3",", the engine has shown you a world in which three dollars materialized from nothing — a subset of the inputs that never existed. That output is not \"a little stale\". It is ",[453,848,849],{},"wrong",[352,851,852],{},"Brandon ran the test across engines, and the counts are the argument.",[352,854,855,858,859,861],{},[355,856,857],{},"Materialize"," (and the differential dataflow it is built on) passes: 599 balance updates, and ",[369,860,385],{}," is emitted exactly once, because the correct answer never changes.",[352,863,864,867,868,870,871,874],{},[355,865,866],{},"The Flink Table API"," emits 18,999,012 balance updates and 37,978,385 ",[369,869,385],{}," values. ",[355,872,873],{},"13,325 of them are 0"," — 0.035%. In one run, 26.3% of the emissions were a single value, 256, which the author could not explain and which differs run to run.",[352,876,877,879,880,882,883,886,887,541,890,892,893,896,897,387],{},[355,878,407],{}," emits 340 ",[369,881,385],{}," values, and in Brandon's words, ",[453,884,885],{},"\"none of the outputs are correct until the last.\""," On his simplified dataset its per-account balances — which can only ever be ",[369,888,889],{},"−1",[369,891,701],{}," or ",[369,894,895],{},"+1"," — reach ",[355,898,899],{},"−135,051 … +2,848",[352,901,902,904,905,910],{},[355,903,411],{}," he couldn't get to produce output for the query at all (",[471,906,909],{"href":907,"rel":908},"https:\u002F\u002Fissues.apache.org\u002Fjira\u002Fbrowse\u002FKAFKA-12594",[475],"KAFKA-12594","), which is why we built our own twin below.",[481,912,914],{"id":913},"why-no-kafka-shaped-engine-has-it","Why no Kafka-shaped engine has it",[352,916,917],{},"The failure isn't a bug in any of these engines. It's architectural, and it has the same root everywhere.",[352,919,920,923,924,927,928,931,932,935,936,939,940,943,944,946,947,949],{},[369,921,922],{},"balance = credits ⋈ debits"," is a join of two aggregations that ",[355,925,926],{},"fan out from one source",". In Kafka Streams (and anything shaped like it), the re-key sends the two halves of a single transfer down ",[355,929,930],{},"two separate repartition topics",", consumed independently, each feeding an aggregation with its own state store and its own record cache that ",[355,933,934],{},"evicts and emits an intermediate"," whenever it fills. The join is driven by whichever half arrives — so when a transaction updates both, the join will, sooner or later, read the ",[453,937,938],{},"new"," credit against the ",[453,941,942],{},"old"," debit — one side at a different logical time than the other. The result is a ",[369,945,778],{}," that never existed, and a ",[369,948,385],{}," that isn't 0.",[352,951,952,953,956,957,960],{},"There is ",[355,954,955],{},"no cut that spans the topology",". Each operator has its own notion of \"now\". The record cache consolidates the ",[453,958,959],{},"volume"," of output — Kafka Streams emits far fewer rows than Flink — but each row it does emit is still a blend of two moments. Consolidated garbage is still garbage.",[352,962,963,964,966,967,969,970,972,973,975],{},"Our own Kafka Streams twin of the benchmark reproduces exactly this: ",[355,965,422],{}," (individual values reaching 180 where only ",[369,968,426],{}," is possible), and ",[369,971,385],{}," wandering to ",[355,974,418],{},". The same phenomenon the article finds in ksqlDB, whose balances reach 135,051 on the same impossible quantity — less extreme here only because our churn rate is lower.",[352,977,978],{},[979,980],"img",{"alt":981,"src":982},"Two arms of one fan-out — credits and debits — reaching a join at different moments. A step chart tracks how many transfers each arm has delivered; wherever the two lines sit apart, the shaded gap is exactly how far the sum of all balances is from zero. At the highlighted moment the join has absorbed five credits against two debits and answers with a balance of +2 for an account that can only ever be −1, 0 or +1.","\u002Fassets\u002Fblog\u002Finternal-consistency_fan-out-blend.svg",[984,985,987],"h3",{"id":986},"the-load-bearing-insight-exactly-once-is-not-internal-consistency","The load-bearing insight: exactly-once is not internal consistency",[352,989,990],{},"It is tempting to think exactly-once processing fixes this. It does not, and the reason is worth internalizing:",[352,992,993,1000,1001,1004,1005,1008],{},[355,994,995,996,999],{},"Flink's ",[369,997,998],{},"EXACTLY_ONCE"," sink atomically commits all 19 million garbage rows."," Transactional atomicity of an output ",[453,1002,1003],{},"batch"," says nothing about whether the ",[453,1006,1007],{},"contents"," of that batch are a coherent view of anything. Exactly-once guarantees you won't see a row twice or lose one. It says nothing about whether any individual row was ever true. They are orthogonal properties, and every Kafka-shaped engine ships the first without the second.",[984,1010,1012],{"id":1011},"the-fair-caveat-this-is-a-property-of-the-dsl-not-of-the-engine","The fair caveat: this is a property of the DSL, not of the engine",[352,1014,1015,1016,1019,1020,1023,1024,541,1026,892,1028,1030,1031,1033,1034],{},"Brandon's article carries an update worth reading before you conclude Flink can't do this. Vasia Kalavri showed him an internally consistent implementation of the same example in Flink's ",[355,1017,1018],{},"DataStream"," API, built out of custom operators with ",[369,1021,1022],{},"ProcessFunction"," — balances only ever ",[369,1025,889],{},[369,1027,701],{},[369,1029,895],{},", and ",[369,1032,385],{}," always 0. His own conclusion: ",[453,1035,1036],{},"\"given that the datastream api gives you the tools to build consistent systems, I'm curious as to why the table api doesn't use them.\"",[352,1038,1039,1040,1043],{},"So Flink's checkpoint barriers can carry a consistent cut. What doesn't carry it is the ",[355,1041,1042],{},"declarative layer"," — the Table API, Flink SQL, the Kafka Streams DSL, ksqlDB. Every one of them forwards eagerly, and getting a coherent answer out of them means dropping a level and hand-compiling your topology into operators you write yourself.",[352,1045,1046,1047,1050],{},"That is the honest shape of the claim, and it's the one we're making: not that nobody else can be internally consistent, but that nobody else gives it to you ",[355,1048,1049],{},"from the DSL, on Kafka, as a flag you turn on",". Our advantage over Flink here is narrower than our advantage over Kafka Streams, and it is worth being precise about which one it is.",[481,1052,1054],{"id":1053},"why-stoatflow-is-different","Why StoatFlow is different",[352,1056,1057],{},"StoatFlow is a single-instance engine: one replica per application, all state global, parallelism from Project Loom virtual threads rather than from partitioned tasks. That architecture turns out to be exactly what internal consistency needs, for four independent reasons:",[1059,1060,1061,1071],"ol",{},[362,1062,1063,1066,1067,1070],{},[355,1064,1065],{},"The commit barrier is a genuine consistent cut."," StoatFlow processes in epochs delimited by a barrier that flows through the whole topology; when it commits, it commits a ",[355,1068,1069],{},"per-partition offset prefix"," — a Chandy–Lamport cut of the input, snapshotted on a single thread between the last dispatched record and the barrier broadcast. There is one \"now\" for the entire topology, not one per operator.",[362,1072,1073,1076,1077,1084],{},[355,1074,1075],{},"The cascade-join synchronizes across streams."," When two chains fan out from a source and merge at a join, StoatFlow ",[355,1078,1079,1080,1083],{},"withholds the barrier from the join's sub-topology until ",[453,1081,1082],{},"both"," contributing chains have pushed their epoch-N output into it."," The join never processes its barrier — never commits — having seen the new credit but not yet the new debit. This cross-stream synchronization is precisely the primitive the article says nobody has. We didn't build it for this; we built it for exactly-once, and it happens to be exactly what internal consistency requires.",[352,1086,1087],{},[979,1088],{"alt":1089,"src":1090},"The same fan-out, now with a commit barrier travelling downstream. The barrier is injected while the two arms are still pushing epoch-N credits and debits into the join, and the join's sub-topology is shaded for the whole span in which it is deliberately not given the barrier. Only once the contributing sub-topology has delivered every last half does the gate release it — so the committed cut holds six credits against six debits, and the balances sum to exactly zero.","\u002Fassets\u002Fblog\u002Finternal-consistency_cascade-join.svg",[1059,1092,1093,1099],{"start":557},[362,1094,1095,1098],{},[355,1096,1097],{},"Watermarks are inserted at the edge."," One global watermark, the minimum across non-idle partitions, broadcast identically to every sub-topology. Brandon's fourth failure mode — per-operator watermarks that drop different data at different operators — cannot occur here by construction.",[362,1100,1101,1104,1105,1108,1109,1112,1113,1116,1117,387],{},[355,1102,1103],{},"The cache never evicts."," When StoatFlow's state cache fills, it does ",[355,1106,1107],{},"not"," flush an eldest entry and emit an intermediate (the Kafka Streams behavior that produces mid-epoch blends). It ",[355,1110,1111],{},"cuts the epoch short"," — fires an early barrier — and commits a coherent cut. Under memory pressure it commits ",[453,1114,1115],{},"sooner","; it never leaks a garbage intermediate. This is what turns barrier-gated emission from best-effort into a ",[453,1118,1119],{},"guarantee",[352,1121,1122,1123,1126,1127,1129,1130,1133,1134,387],{},"What was missing was small by comparison: like every Kafka-shaped engine, ",[355,1124,1125],{},"every non-monotonic operator forwarded its output eagerly"," — one row per input change. So we added the gate: an opt-in ",[369,1128,439],{}," (and a per-operator ",[369,1131,1132],{},"Suppressed.untilBarrier()",") that holds a KTable operator's emissions and releases a single consolidated value ",[355,1135,1136],{},"per consistent cut",[481,1138,1140],{"id":1139},"the-measured-result","The measured result",[352,1142,1143,1144,1146,1147,1150,1151,1150,1154,1150,1156,1158,1159,1162,1163,1165,1166,1169,1170,387],{},"We ported Brandon's benchmark verbatim — 10 accounts, ",[369,1145,371],{}," transfers, the ",[369,1148,1149],{},"credits","\u002F",[369,1152,1153],{},"debits",[369,1155,778],{},[369,1157,385],{}," topology above — and ran it on a 5-node cluster at 2,000 transactions\u002Fsecond under exactly-once, with a ",[369,1160,1161],{},"read_committed"," verifier that flags any ",[369,1164,778],{}," outside ",[369,1167,1168],{},"[−1, 1]"," and any ",[369,1171,1172],{},"total ≠ 0",[352,1174,1175],{},[355,1176,1177],{},"Per-account balances — StoatFlow is consistent even without the valve:",[1179,1180,1181,1201],"table",{},[1182,1183,1184],"thead",{},[1185,1186,1187,1191,1194,1197],"tr",{},[1188,1189,1190],"th",{},"Metric",[1188,1192,1193],{},"StoatFlow EAGER",[1188,1195,1196],{},"StoatFlow ON_BARRIER",[1188,1198,1199],{},[355,1200,411],{},[1202,1203,1204,1223,1239,1258],"tbody",{},[1185,1205,1206,1210,1214,1218],{},[1207,1208,1209],"td",{},"Impossible balances (|balance| > 1)",[1207,1211,1212],{},[355,1213,701],{},[1207,1215,1216],{},[355,1217,701],{},[1207,1219,1220],{},[355,1221,1222],{},"160",[1185,1224,1225,1228,1231,1234],{},[1207,1226,1227],{},"Max |individual balance|",[1207,1229,1230],{},"1 (in range)",[1207,1232,1233],{},"1",[1207,1235,1236],{},[355,1237,1238],{},"180",[1185,1240,1241,1244,1249,1253],{},[1207,1242,1243],{},"Max |Σ balance| excursion",[1207,1245,1246],{},[355,1247,1248],{},"5",[1207,1250,1251],{},[355,1252,701],{},[1207,1254,1255],{},[355,1256,1257],{},"1,792",[1185,1259,1260,1263,1266,1269],{},[1207,1261,1262],{},"Balance output rate",[1207,1264,1265],{},"6,860\u002Fs",[1207,1267,1268],{},"6,876\u002Fs",[1207,1270,1271],{},"45\u002Fs",[352,1273,1274,1275,1278,1279,1282,1283,1286,1287,1290,1291,1294,1295,1297],{},"Even in ",[355,1276,1277],{},"EAGER"," mode — one emission per change, the same eager forwarding Kafka Streams uses — StoatFlow emits ",[355,1280,1281],{},"zero impossible balances"," — none in 1.67 million emissions — and ",[369,1284,1285],{},"Σ balance"," never leaves ",[369,1288,1289],{},"[0, +5]",". That residual +5 isn't an inconsistency: it's the verifier observing an epoch's ~10 per-account updates one at a time. At every ",[453,1292,1293],{},"committed"," cut, ",[369,1296,385],{}," is exactly 0. Kafka Streams, on the identical workload, emits 160 balances that cannot exist.",[352,1299,1300,1301,1304,1305,1307,1308,541,1310,1312,1313,387],{},"The two engines from the article aren't columns here, because Brandon doesn't report per-account balance statistics in a form that lines up with ours — and his dataset isn't ours, so putting his numbers in the same row would invite a comparison neither of us measured. For the record: the Flink Table API's balance histogram runs to ",[355,1302,1303],{},"33",", and on his simplified dataset ksqlDB's balances reach ",[355,1306,899],{},". Both on a quantity whose only legal values are ",[369,1309,889],{},[369,1311,701],{}," and ",[369,1314,895],{},[352,1316,1317,1323,1324,1326,1327,1329],{},[355,1318,1319,1320,1322],{},"The ",[369,1321,385],{}," view — the article's headline test."," Now consolidate to one ",[369,1325,385],{}," per cut with ",[369,1328,439],{},":",[1179,1331,1332,1358],{},[1182,1333,1334],{},[1185,1335,1336,1342,1344,1348,1352,1355],{},[1188,1337,1338,1339,1341],{},"Metric (",[369,1340,385],{}," view)",[1188,1343,1193],{},[1188,1345,1346],{},[355,1347,1196],{},[1188,1349,1350],{},[355,1351,411],{},[1188,1353,1354],{},"Flink Table API (article)",[1188,1356,1357],{},"ksqlDB (article)",[1202,1359,1360,1381,1403],{},[1185,1361,1362,1365,1368,1372,1376,1379],{},[1207,1363,1364],{},"Max |total|",[1207,1366,1367],{},"+5…8",[1207,1369,1370],{},[355,1371,701],{},[1207,1373,1374],{},[355,1375,418],{},[1207,1377,1378],{},"not published",[1207,1380,1378],{},[1185,1382,1383,1388,1391,1394,1397,1400],{},[1207,1384,1385,1387],{},[369,1386,385],{}," emissions in the run",[1207,1389,1390],{},"1,674,738",[1207,1392,1393],{},"210",[1207,1395,1396],{},"191",[1207,1398,1399],{},"37,978,385",[1207,1401,1402],{},"340",[1185,1404,1405,1408,1411,1416,1419,1422],{},[1207,1406,1407],{},"Of those, exactly 0",[1207,1409,1410],{},"266,731 (15.9%)",[1207,1412,1413],{},[355,1414,1415],{},"210 (100%)",[1207,1417,1418],{},"175 (91.6%)",[1207,1420,1421],{},"13,325 (0.035%)",[1207,1423,1233],{},[352,1425,1426,1427,1429,1430,1433,1434,1437],{},"Two of those columns are Brandon's, not ours, and they carry a gap: neither the Flink nor the ksqlDB ",[369,1428,385],{}," has a published minimum and maximum. He reports Flink's qualitatively — the error ",[453,1431,1432],{},"\"seems to be increasing over time\"",", oscillating ",[453,1435,1436],{},"\"between a number of stable attractors\""," — and ksqlDB's not at all. What both do publish is how many values came out and how many of them were right, which is the row that matters most anyway.",[352,1439,1440,1441,541,1444,1449,1450,1452],{},"Under ",[369,1442,1443],{},"on-barrier",[355,1445,838,1446,1448],{},[369,1447,385],{}," is exactly 0"," — verified across both a high-churn workload and a unique-key (worst-case) workload, at ~one emission per epoch (a ~680× reduction in output volume versus EAGER, each one a true consistent-cut value). This is the property the article says no Kafka-shaped engine has: ",[369,1451,385],{}," never leaves 0, on Kafka.",[352,1454,1455,1456,432,1459,1461],{},"Kafka Streams, meanwhile, sends the ",[453,1457,1458],{},"same",[369,1460,385],{}," view to −1,619 … +1,792. Its record cache consolidates the stream — few emissions — but the emissions it does make are garbage totals.",[352,1463,1464],{},"Those numbers span four orders of magnitude and end at zero, which is hard to see in a table. On a log axis, where every gridline is a 10× step, the shape of the result is the whole argument:",[1466,1467],"consistency-excursion",{},[352,1469,1470,1471,1474,1475,1478],{},"Read it as an error bar: the invariant says ",[369,1472,1473],{},"total = 0",", so a bar's length ",[453,1476,1477],{},"is"," how wrong the engine got. Two decades separate Kafka Streams from StoatFlow's eager mode — but that is a difference of degree. The step that matters is the last one, from small to none.",[352,1480,1481],{},"Magnitude is only half of the question, though, and it's the half where the engines Brandon tested can't be plotted at all. The other half — how often an engine is right in the first place — is the one axis where every engine in this story has published numbers:",[1483,1484],"consistency-correctness",{},[352,1486,1487],{},"That chart is the reason both halves are here. Kafka Streams is right 91.6% of the time and StoatFlow's eager mode only 15.9% — but the eager mode's errors are never larger than +5, and Kafka Streams' reach ±1,792. Being wrong rarely and being wrong slightly are different virtues, and neither is the one you want. There is exactly one row that needs no such trade-off.",[352,1489,1490],{},"A single number per engine still understates it, because it suggests a system that is steadily a bit wrong. That is not the failure mode. Here is the same measurement over time, one engine per chart — same run, same 210-second window, same invariant. These are the three engines we measured ourselves; the Flink and ksqlDB figures above are Brandon's.",[984,1492,1494],{"id":1493},"kafka-streams-right-almost-always-and-wildly-wrong-the-rest-of-the-time","Kafka Streams: right almost always, and wildly wrong the rest of the time",[1496,1497],"consistency-time-series",{"panel":1498},"ks",[352,1500,1501],{},"175 of its 191 emissions read exactly 0. The other sixteen reach −1,619 and +1,792. That is the shape of the problem: not a drift you could bound and compensate for, but a correct-looking line that is briefly, unpredictably, wildly wrong.",[352,1503,1504],{},"The record cache is what makes it look so calm. It consolidates each commit interval back to a consistent 0, so the garbage totals surface only on the mid-interval evictions — which is precisely what makes this failure mode hard to catch in production. If you sampled this dashboard once a second you would call it healthy and be wrong about one sample in twelve.",[984,1506,1508],{"id":1507},"stoatflow-eager-wrong-constantly-and-never-by-more-than-5","StoatFlow EAGER: wrong constantly, and never by more than +5",[1496,1510],{"panel":1511},"eager",[352,1513,1514,1515,1518],{},"Read the axis before the shape: this chart is zoomed 333× against the one above, because at Kafka Streams' amplitude StoatFlow's eager mode is indistinguishable from the zero line. Its ",[369,1516,1517],{},"+5"," would be a fortieth of a pixel up there.",[352,1520,1521,1522,1524,1525,1527,1528,1294,1530,1532],{},"On that axis, ",[369,1523,1277],{}," looks worse than Kafka Streams, and on one measure it is: 84.1% of its 1.67 million emissions are non-zero, against Kafka Streams' 8.4%. But the band never leaves ",[369,1526,1289],{},", and it never goes negative. That residual isn't an inconsistency in the engine — it's the verifier watching an epoch's ~10 per-account updates land one at a time. At every ",[453,1529,1293],{},[369,1531,385],{}," is already exactly 0. Wrong often and wrong slightly is a different failure from wrong rarely and wrong by 1,792, and neither is the one you want.",[984,1534,1536],{"id":1535},"stoatflow-on_barrier-exactly-0-at-every-emission","StoatFlow ON_BARRIER: exactly 0, at every emission",[1496,1538],{"panel":1443},[352,1540,1541,1542,1544],{},"Same zoom, same axis, same 210 seconds as the chart above — so the empty space here is precisely the space ",[369,1543,1277],{},"'s band fills. Hold the two side by side and that gap is the whole feature.",[352,1546,1547],{},"There is no line to trace because there is no variation to show. All 210 emissions are 0, one per commit barrier, ~1.00 s apart, each one a true consistent-cut value. Not a small error. The absence of one.",[481,1549,1551],{"id":1550},"the-honest-part-the-bug-we-had-to-fix","The honest part: the bug we had to fix",[352,1553,1554,1555,1557,1558,1560,1561],{},"The first time we ran the ",[369,1556,1443],{}," benchmark, it wasn't perfect. ",[369,1559,385],{}," was 0 at 406 of 412 emissions on the churn workload — and at 390 of 412 on unique keys. A small residue of tiny (±2…±4) violations survived the valve: ",[355,1562,1563],{},"6 on churn, 22 on unique keys.",[352,1565,1566,1567,1570,1571,1574],{},"That residue was easy to hand-wave away as a startup transient. It wasn't. The full-topic dump showed the violations were scattered through the run, all positive (a credit reflected without its matching debit), and — the tell — the ",[453,1568,1569],{},"slower"," workload had ",[453,1572,1573],{},"more"," of them. That ruled out startup and churn and pointed at something structural.",[352,1576,1577,1578,1581,1582,1585,1586,1588,1589,1592],{},"It was: the commit barrier was a consistent cut ",[453,1579,1580],{},"within"," a sub-topology, but ",[355,1583,1584],{},"not across a sub-topology boundary",". The ",[369,1587,385],{}," view sits two aggregation levels below the fan-out-merge join, behind a repartition. When the barrier cascaded across that boundary on a detached thread, an upstream lane could resume the instant it reported the barrier and forward a ",[453,1590,1591],{},"next","-epoch record that overtook the barrier into the downstream cut — leaking a sliver of the following epoch into the committed transaction. Under crash recovery those leaked records would even double-count.",[352,1594,1595],{},"The fix (we call it receiver-side epoch hold-back) tags every record that crosses a sub-topology boundary with the epoch it belongs to; the receiving side briefly holds back any record that ran ahead of the barrier until the barrier arrives there too. No pauses, no coordinator changes — the cut is realigned across the boundary by construction.",[352,1597,1598,1599,1602,1603,1605,1606,1609],{},"We re-ran the benchmark on the fixed build. ",[355,1600,1601],{},"Churn: 6 → 0. Unique keys: 22 → 0."," Every one of the ~412 committed ",[369,1604,385],{}," emissions is now exactly 0, on both workloads, at 2,000 tx\u002Fs. We're writing this down because \"near-perfect\" is not the claim — ",[453,1607,1608],{},"perfect, and here's the run that proves it"," is.",[481,1611,1613],{"id":1612},"how-to-turn-it-on","How to turn it on",[352,1615,1616],{},"Internal consistency is opt-in and per-operator, because eager emission is the right default for latency-sensitive views and matches Kafka Streams exactly. When you want a coherent view, gate it:",[493,1618,1620],{"className":495,"code":1619,"language":497,"meta":498,"style":498},"\u002F\u002F Per operator:\ntotal.suppress(Suppressed.untilBarrier())   \u002F\u002F emit one consolidated value per consistent cut\n\n\u002F\u002F Or topology-wide, in application.yaml:\nstoatflow:\n  emit:\n    mode: on-barrier\n",[369,1621,1622,1627,1647,1651,1656,1661,1666],{"__ignoreMap":498},[502,1623,1624],{"class":504,"line":505},[502,1625,1626],{"class":508},"\u002F\u002F Per operator:\n",[502,1628,1629,1632,1635,1638,1641,1644],{"class":504,"line":512},[502,1630,1631],{"class":519},"total.",[502,1633,1634],{"class":530},"suppress",[502,1636,1637],{"class":519},"(Suppressed.",[502,1639,1640],{"class":530},"untilBarrier",[502,1642,1643],{"class":519},"())   ",[502,1645,1646],{"class":508},"\u002F\u002F emit one consolidated value per consistent cut\n",[502,1648,1649],{"class":504,"line":557},[502,1650,561],{"emptyLinePlaceholder":560},[502,1652,1653],{"class":504,"line":564},[502,1654,1655],{"class":508},"\u002F\u002F Or topology-wide, in application.yaml:\n",[502,1657,1658],{"class":504,"line":626},[502,1659,1660],{"class":519},"stoatflow:\n",[502,1662,1663],{"class":504,"line":672},[502,1664,1665],{"class":519},"  emit:\n",[502,1667,1668,1671,1674,1676],{"class":504,"line":677},[502,1669,1670],{"class":519},"    mode: ",[502,1672,1673],{"class":537},"on",[502,1675,707],{"class":616},[502,1677,1678],{"class":519},"barrier\n",[352,1680,1681,1682,1685],{},"Under the hood this buffers a KTable operator's emissions per lane and releases them at the commit barrier — riding the same Kafka transaction as the offsets that produced them, so the consolidated emission is exactly-once ",[453,1683,1684],{},"and"," internally consistent. The buffer is bounded by the same early-barrier mechanism as everything else: it never grows without cutting the epoch.",[481,1687,1688],{"id":1149},"Credits",[352,1690,1691,1692,1698],{},"This work started with Jamie Brandon's ",[471,1693,1695],{"href":473,"rel":1694},[475],[453,1696,1697],{},"Internal consistency in streaming systems"," — the benchmark, the definition, and the framing are his, and the article is the clearest statement of the problem we know of. Read it.",[352,1700,1701,1702,1707,1708,1715,1716,1721,1722,1725],{},"The investigation was prompted by ",[471,1703,1706],{"href":1704,"rel":1705},"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fralph-m-debusmann-63204675\u002F",[475],"Ralph M. Debusmann",", whose Berlin Buzzwords 2026 talk ",[471,1709,1712],{"href":1710,"rel":1711},"https:\u002F\u002F2026.berlinbuzzwords.de\u002Fsession\u002Fkafi-streams-complex-stream-processing-made-simple\u002F",[475],[453,1713,1714],{},"Kafi Streams – Complex Stream Processing Made Simple"," (",[471,1717,1720],{"href":1718,"rel":1719},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=_oKQaDyP1CQ",[475],"recording",") and the conversations that followed put the question of whether a Kafka-shaped engine ",[453,1723,1724],{},"could"," be internally consistent in front of us. The answer, it turns out, is yes — and it was mostly already there.",[1727,1728],"hr",{},[352,1730,1731],{},[453,1732,1733,1734,1738,1739,387],{},"StoatFlow is a single-replica, Kafka Streams–compatible stream processor built on JDK 25 virtual threads. Internal-consistency mode is available now. ",[471,1735,1737],{"href":1736},"\u002Fcontact","Get in touch"," for early access, or head to ",[471,1740,5],{"href":6},[1742,1743,1744],"style",{},"html pre.shiki code .s8zF2, html code.shiki .s8zF2{--shiki-default:#A0ADA0}html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .suHK_, html code.shiki .suHK_{--shiki-default:#393A34}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .spphp, html code.shiki .spphp{--shiki-default:#B56959}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":498,"searchDepth":512,"depth":512,"links":1746},[1747,1748,1749,1753,1754,1759,1760,1761],{"id":483,"depth":512,"text":484},{"id":807,"depth":512,"text":808},{"id":913,"depth":512,"text":914,"children":1750},[1751,1752],{"id":986,"depth":557,"text":987},{"id":1011,"depth":557,"text":1012},{"id":1053,"depth":512,"text":1054},{"id":1139,"depth":512,"text":1140,"children":1755},[1756,1757,1758],{"id":1493,"depth":557,"text":1494},{"id":1507,"depth":557,"text":1508},{"id":1535,"depth":557,"text":1536},{"id":1550,"depth":512,"text":1551},{"id":1612,"depth":512,"text":1613},{"id":1149,"depth":512,"text":1688},"2026-07-13","Money can only be moved, never created — so a stream that tracks balances should read total = 0 at every consistent cut. The Flink Table API gets it right 0.035% of the time; our Kafka Streams twin sends total to −1,619 … +1,792. StoatFlow holds it at exactly 0, at every one of its committed cuts. Here is why, and the measured proof.","md",{"src":1766},"\u002Fassets\u002Fblog\u002Fog\u002Finternal-consistency.png",{},"\u002Fblog\u002Finternal-consistency",{"title":336,"description":1763},"3.blog\u002F8.internal-consistency","DQNWXoI7DRAv7V66FoRuJlVWt4b8_IShKaCF-WijgnQ",[1773,1778],{"title":1774,"path":1775,"stem":1776,"description":1777,"children":-1},"StoatFlow AI Assistant Skills: correct StoatFlow, not hallucinated Kafka Streams","\u002Fblog\u002Fai-assistant-skills","3.blog\u002F9.ai-assistant-skills","A free, open skills pack that makes AI coding assistants write correct StoatFlow instead of hallucinated Kafka Streams.",{"title":1779,"path":1780,"stem":1781,"description":1782,"children":-1},"In-place engine restart: the primitive behind multi-standby HA","\u002Fblog\u002Fin-place-restart-multi-standby","3.blog\u002F7.in-place-restart-multi-standby","The failover-testing post ended on an exploratory idea — rebuild the processing engine in the same process, no JVM exit — and a constraint: stay at two standbys. Both have moved. The in-place restart shipped, and a lag-aware leader election shipped on top, so a StoatFlow HA cluster now runs one active and any number of warm standbys, scaling past two elects exactly one successor instead of crash-looping, and a graceful role swap no longer bounces the pod.",1786987416107]