<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: alginte software</title>
    <description>The latest articles on DEV Community by alginte software (@alginte_sw).</description>
    <link>https://dev.to/alginte_sw</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4109378%2F151d1de6-a5b0-465b-a8a8-434b8cdd0a72.png</url>
      <title>DEV Community: alginte software</title>
      <link>https://dev.to/alginte_sw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alginte_sw"/>
    <language>en</language>
    <item>
      <title>The compiler was never what you wanted</title>
      <dc:creator>alginte software</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:27:31 +0000</pubDate>
      <link>https://dev.to/alginte/the-compiler-was-never-what-you-wanted-4bf4</link>
      <guid>https://dev.to/alginte/the-compiler-was-never-what-you-wanted-4bf4</guid>
      <description>&lt;p&gt;You have an &lt;code&gt;orders&lt;/code&gt; topic on a Kafka cluster, its values encoded with &lt;a href="https://avro.apache.org/" rel="noopener noreferrer"&gt;Avro&lt;/a&gt; against a schema in the &lt;a href="https://docs.confluent.io/platform/current/schema-registry/index.html" rel="noopener noreferrer"&gt;Schema Registry&lt;/a&gt;. You want the orders worth more than fifty euros on a topic of their own, and you have decided to do it with &lt;a href="https://kafka.apache.org/documentation/streams/" rel="noopener noreferrer"&gt;Kafka Streams&lt;/a&gt; — a JVM library, your code, your deployment.&lt;/p&gt;

&lt;p&gt;The schema has five fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.alginte.demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"priceEur"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"double"&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want one line of logic over them: &lt;code&gt;quantity * priceEur &amp;gt; 50&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is everything standing between that line and a topic of big orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven steps
&lt;/h2&gt;

&lt;p&gt;The route &lt;a href="https://github.com/confluentinc/kafka-streams-examples/blob/v8.4.0-2/pom.xml#L426-L441" rel="noopener noreferrer"&gt;Confluent's own examples&lt;/a&gt; take, and many projects with them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get the schema out of the registry&lt;/strong&gt; and into your repository as an &lt;code&gt;.avsc&lt;/code&gt; — or, if your team owns the schema in the repository and publishes it to the registry, the other way round. Whichever copy you call the source, there are now two that can disagree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the code generator&lt;/strong&gt; to your build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure it&lt;/strong&gt; — source and output directories, and the string type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt;, producing &lt;code&gt;Order.java&lt;/code&gt; under &lt;code&gt;target/generated-sources&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the topology&lt;/strong&gt; against the generated class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package&lt;/strong&gt; the application, with the schema, the class and the serde.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt; it somewhere that runs a JVM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 2 and 3 are this, once — in Maven, though Gradle's equivalent has the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.avro&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;avro-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.12.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;phase&amp;gt;&lt;/span&gt;generate-sources&lt;span class="nt"&gt;&amp;lt;/phase&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&amp;lt;goal&amp;gt;&lt;/span&gt;schema&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;sourceDirectory&amp;gt;&lt;/span&gt;${project.basedir}/src/main/avro&lt;span class="nt"&gt;&amp;lt;/sourceDirectory&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!-- without this, string fields generate as CharSequence, not String;
           Confluent's own examples set it for the same reason --&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stringType&amp;gt;&lt;/span&gt;String&lt;span class="nt"&gt;&amp;lt;/stringType&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And step 5 is the part you actually wanted to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Consumed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Serdes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;orderSerde&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getQuantity&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPriceEur&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"big-orders"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Produced&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Serdes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;orderSerde&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven steps, one of them the predicate. Two of them — adding the plugin and configuring it — you do that once.&lt;/p&gt;

&lt;p&gt;The filter is deliberately trivial — a real topology joins, aggregates and branches — but the seven steps are identical for twenty operators, because they are charged per project rather than per line of logic. And &lt;code&gt;application.id&lt;/code&gt;, &lt;code&gt;bootstrap.servers&lt;/code&gt;, the registry URL and your cluster's authentication are missing from the list because they are the price of running against a real cluster and registry, not the price of generating classes.&lt;/p&gt;

&lt;p&gt;Those two are not the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then you want to use a new field
&lt;/h2&gt;

&lt;p&gt;Somebody adds &lt;code&gt;region&lt;/code&gt; to the schema. While it sits there unused you are fine — Avro resolves the writer's schema against yours, your generated class does not know the field exists, and nothing needs rebuilding. Schema evolution is doing its job.&lt;/p&gt;

&lt;p&gt;Then somebody asks for EU orders only.&lt;/p&gt;

&lt;p&gt;The change to your logic is one term: &lt;code&gt;&amp;amp;&amp;amp; order.getRegion().equals("EU")&lt;/code&gt;. The change to your project is steps 1, 4, 5, 6 and 7 — pull the new schema, regenerate, rewrite, repackage, redeploy.&lt;/p&gt;

&lt;p&gt;That is the actual price, and it is charged not per schema change but per schema change &lt;em&gt;you need&lt;/em&gt;. Which, over the life of a pipeline, is many of them: fields get added because somebody intends to use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the seven steps buy
&lt;/h2&gt;

&lt;p&gt;They buy the compiler. &lt;code&gt;order.getQuantiy()&lt;/code&gt; does not compile. Rename a field, regenerate, and every stale use site turns red before anything runs. The IDE completes field names. Refactoring works.&lt;/p&gt;

&lt;p&gt;That is worth having. It is also answering a narrower question than the one you actually have.&lt;/p&gt;

&lt;p&gt;The compiler can tell you that &lt;code&gt;getQuantiy()&lt;/code&gt; is not a method. It cannot tell you whether &lt;code&gt;quantity * priceEur &amp;gt; 50&lt;/code&gt; is the predicate you meant, whether it matches any record on the topic, or whether the field you are multiplying holds what you think it holds. For that, the seven steps have one answer: deploy it and look.&lt;/p&gt;

&lt;p&gt;So the loop you are really in is not &lt;em&gt;edit, compile&lt;/em&gt;. It is &lt;strong&gt;edit, compile, package, deploy, produce a record, read the output&lt;/strong&gt; — and it costs the same whether the expression was right or wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tighter loop
&lt;/h2&gt;

&lt;p&gt;There is another shape for this, and it is the one we build: &lt;a href="https://www.alginte.com" rel="noopener noreferrer"&gt;Alginte&lt;/a&gt;, a browser-based topology builder that assembles it at runtime from topics you pick, instead of compiling it into an application you ship.&lt;/p&gt;

&lt;p&gt;Point it at &lt;code&gt;orders&lt;/code&gt;. The schema comes from the registry at runtime, the five field names arrive as completions, and the predicate is a string — written in &lt;a href="https://docs.spring.io/spring-framework/reference/core/expressions.html" rel="noopener noreferrer"&gt;SpEL&lt;/a&gt;, Spring's expression language, evaluated once per record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;value.get('quantity') * value.get('priceEur') &amp;gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That expression is evaluated against a real record from the topic while it is being typed — a record off the partition rather than a mock or a fixture, with the answer beside it.&lt;/p&gt;

&lt;p&gt;When it is right, it deploys as a Kafka Streams topology: the same library, the same &lt;code&gt;KafkaStreams&lt;/code&gt; client, the same rebalances, state stores and changelog topics you would have got from the seven steps. No separate engine is involved. The only thing that changed is how the topology was written.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;.avsc&lt;/code&gt; in a repository, no plugin, no &lt;code&gt;target/generated-sources&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://alginte.com/blog/kafka-streams-without-codegen/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo1opd47mz8x04qgcvajh.webp" alt="The sample preview panel on a map node, showing a real record drawn from the orders topic on the in line. Typing value.get('item') makes the out line read Kettle; replacing the expression with value.get('quantity') * value.get('priceEur') makes the out line read 51. The editor reports no issues throughout and nothing is deployed." width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The record on the &lt;code&gt;in&lt;/code&gt; line is a real one off &lt;code&gt;orders&lt;/code&gt; — &lt;code&gt;quantity&lt;/code&gt; 3, &lt;code&gt;priceEur&lt;/code&gt; 17. The &lt;code&gt;out&lt;/code&gt; line is whatever the expression currently returns, and it follows the expression as it is edited: &lt;code&gt;51&lt;/code&gt;, which is the number the &lt;code&gt;&amp;gt; 50&lt;/code&gt; filter is about to judge. Nothing has been deployed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mistakes surface in the same place. A method that does not exist on the type the record actually carries is reported while it is being typed, naming the type — &lt;a href="https://alginte.com/blog/building-a-spel-editor/" rel="noopener noreferrer"&gt;the post about building that editor&lt;/a&gt; has that one on camera.&lt;/p&gt;

&lt;p&gt;That is not a compiler. It is the question the compiler could not answer, asked against real data, answered in seconds. And when somebody adds a sixth field and you want to use it, you type its name.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up
&lt;/h2&gt;

&lt;p&gt;The compiler.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;value.get('quantiy')&lt;/code&gt; is a valid expression — javac never sees it, so nothing rejects it before it runs. Rename across the project goes too, and the unit tests that constructed &lt;code&gt;Order&lt;/code&gt; objects.&lt;/p&gt;

&lt;p&gt;What you do not lose is the typo itself. On Avro a misspelt field throws at access — &lt;code&gt;Not a valid schema field: quantiy&lt;/code&gt; — so it fails in the editor and fails deployed, identically. That is not the compiler's guarantee, which is made before anything runs. It is the same failure arriving in both places at the same moment: a weaker promise, and a real one.&lt;/p&gt;

&lt;p&gt;Some of it returns in a different form — field names completed from the registry, so the typo is never offered; the expression checked against a real record as you type; the same failure surfacing in the editor that would surface in the deployed topology. A different guarantee, weaker in some places, stronger in one.&lt;/p&gt;

&lt;p&gt;What does not go is the review. A topology drawn here exports as JSON and imports back, so the thing a reviewer reads and the thing git keeps is a file, not a browser session — the build step is gone, the artifact is not.&lt;/p&gt;

&lt;p&gt;You can have the generic half of this without any of the rest. Nothing stops you writing a Java topology that reads &lt;a href="https://avro.apache.org/docs/1.12.0/api/java/org/apache/avro/generic/GenericRecord.html" rel="noopener noreferrer"&gt;&lt;code&gt;GenericRecord&lt;/code&gt;&lt;/a&gt; and never generates a class: steps 1 to 4 disappear, and so does the compiler, since &lt;code&gt;record.get("quantiy")&lt;/code&gt; is a string lookup that javac will not check either. What you are left with is the worst of both — no type safety, and steps 6 and 7 still in front of you.&lt;/p&gt;

&lt;p&gt;The generic types are not the point. The loop around them is: the completions, the record on screen, and the deploy that is a click rather than a pipeline.&lt;/p&gt;

&lt;p&gt;What that buys is a faster answer to the question you actually had.&lt;/p&gt;

&lt;p&gt;And if you would rather write the Java regardless — the stream is one part of a larger application, or your team works that way, or any of the other good reasons — the loop is still worth having first. Draw it here, get the expressions right against real records, then spend the seven steps on logic you already know works. That is a better use of an afternoon than finding out after the deploy.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This came out of building &lt;a href="https://www.alginte.com" rel="noopener noreferrer"&gt;Alginte&lt;/a&gt;, a visual Kafka Streams builder — self-hosted, free to run.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>java</category>
      <category>programming</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
