<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>apolzek</title><link>https://apolzek.github.io/</link><description>Production systems, observability and reliability engineering. Documenting technical experiences and, outside production, exploring philosophy and reflections on life.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 13 Oct 2026 20:08:45 GMT</lastBuildDate><atom:link href="https://apolzek.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>Dialogue on the Mentor and Trust</title><link>https://apolzek.github.io/en/2026/10/12/dialogue-on-the-mentor-and-trust/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/10/12/dialogue-on-the-mentor-and-trust/</guid><pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate><description>&lt;p&gt;&lt;em&gt;Setting: late afternoon in the agora. Glaucon is sitting on a step with two papyrus scrolls on his lap, neither of them unrolled. Socrates comes over and sits beside him.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SOCRATES:&lt;/strong&gt; Two scrolls and neither one open. Either both are bad, or you do not know which to read first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GLAUCON:&lt;/strong&gt; Neither, Socrates. They are too good, and that is the problem. This one is by Damon, with whom I have studied for three years. That one is by Theophilus, whom I met last winter. I have never heard anyone explain things the way those two do.&lt;/p&gt;</description><content:encoded><![CDATA[<p><em>Setting: late afternoon in the agora. Glaucon is sitting on a step with two papyrus scrolls on his lap, neither of them unrolled. Socrates comes over and sits beside him.</em></p>
<p><strong>SOCRATES:</strong> Two scrolls and neither one open. Either both are bad, or you do not know which to read first.</p>
<p><strong>GLAUCON:</strong> Neither, Socrates. They are too good, and that is the problem. This one is by Damon, with whom I have studied for three years. That one is by Theophilus, whom I met last winter. I have never heard anyone explain things the way those two do.</p>
<p><strong>SOCRATES:</strong> And what is the affliction in having two good masters?</p>
<p><strong>GLAUCON:</strong> Yesterday I quoted Theophilus in front of Damon. He went silent, then said Theophilus is a charlatan and that I would be wasting my time. Today I told Theophilus, and he laughed and said Damon is an old man repeating what he read at twenty. Each of them is everything I admire. And each despises the other.</p>
<p><strong>SOCRATES:</strong> And what do you conclude from that?</p>
<p><strong>GLAUCON:</strong> That one of the two is wrong, and that I need to find out which, so I can follow the right one.</p>
<p><strong>SOCRATES:</strong> Tell me something first. Why did you look for a master?</p>
<p><strong>GLAUCON:</strong> Because one does not learn alone. Whoever has already walked the road spares the others from tripping on the same stones.</p>
<p><strong>SOCRATES:</strong> That is true, and it is good that you looked. But answer honestly: when you found Damon, was it only the road that interested you, or also the rest?</p>
<p><strong>GLAUCON:</strong> What rest?</p>
<p><strong>SOCRATES:</strong> The rest of no longer having to decide. Having someone to ask &ldquo;is this right?&rdquo; and accepting the answer. Sleeping in the knowledge that, if you err, the error belongs to whoever taught you.</p>
<p><strong>GLAUCON:</strong> I do not like how it sounds, but yes. There was rest in it.</p>
<p><strong>SOCRATES:</strong> Do not be ashamed. Every man wants that rest, and that is why every man looks for a mentor in such a hurry. It is not only knowledge we seek; it is someone on whom to lay the weight of judging. And notice: as long as you had a single master, the rest worked. What broke it was the second one.</p>
<p><strong>GLAUCON:</strong> Because now one says one thing and the other says another.</p>
<p><strong>SOCRATES:</strong> No. On most things, the two say the same, or you would not admire both. What broke your rest was something else: you discovered that the admiration you feel for one does not oblige the other to feel the same. You wanted your masters to love each other as you love them. They have no such duty.</p>
<p><strong>GLAUCON:</strong> But if Damon is wise, and says Theophilus is a charlatan, should I not at least consider it?</p>
<p><strong>SOCRATES:</strong> You should, and you should also consider the opposite. But let us go slowly. When Damon teaches you geometry, where does his authority come from?</p>
<p><strong>GLAUCON:</strong> From knowing geometry.</p>
<p><strong>SOCRATES:</strong> And when he tells you Theophilus is a charlatan, where does the authority come from?</p>
<p><strong>GLAUCON:</strong> From&hellip; knowing Theophilus, I suppose.</p>
<p><strong>SOCRATES:</strong> And does he? Has he heard him teach? Has he read what he writes? Or does he know the reputation, a sentence spoken in an argument, the memory of an old feud nobody remembers how it started?</p>
<p><strong>GLAUCON:</strong> I do not know. I never asked.</p>
<p><strong>SOCRATES:</strong> Then here is the first thing to keep. A man can be a master in one art and as ignorant as anyone in a second. Knowing geometry makes no one a judge of character. When you accept Damon&rsquo;s judgment of Theophilus with the same trust you give the theorem, you are extending his authority into ground where he never earned it. And he, if honest, would not claim it.</p>
<p><strong>GLAUCON:</strong> So neither one is wrong about geometry. They are wrong about each other.</p>
<p><strong>SOCRATES:</strong> Perhaps. Or perhaps one of them is right, and you do not yet know. But notice what you just did: a minute ago, the problem was &ldquo;which of the two to follow&rdquo;. Now it is &ldquo;what does each one actually know, and about what&rdquo;. It is a better question.</p>
<p><strong>GLAUCON:</strong> It is a question that hands the work back to me.</p>
<p><strong>SOCRATES:</strong> That work was always yours, Glaucon. You had only lent it out. We talked, on another afternoon, about the tablet in the square: the label someone sticks on you does not excuse you from examining. The same holds for the label one master sticks on another. &ldquo;Charlatan&rdquo; and &ldquo;old man who repeats&rdquo; are thrown stones, not reasons. Ask for the reasons. If they come, weigh them. If they do not, leave the stones on the ground.</p>
<p><strong>GLAUCON:</strong> And if I weigh them and conclude that Damon, who taught me so much, was wrong about Theophilus? What do I do with that?</p>
<p><strong>SOCRATES:</strong> What do you do when your horse stumbles?</p>
<p><strong>GLAUCON:</strong> I get it up and carry on. I do not sell the horse.</p>
<p><strong>SOCRATES:</strong> And why would you sell the master? Glaucon, listen well to this, because it is the part that costs the young the most: <em>your master will err</em>. Not &ldquo;may err&rdquo;, will. He will err about people, he will err about facts, he will have days of jealousy, of tiredness and of stubbornness. And when that happens, you will have two wrong ways out and one right one. The first wrong one is pretending you did not see, to protect the rest. The second wrong one is tearing down the whole man, because one column cracked. The right one is harder: keep learning geometry from the one who knows geometry, and keep for yourself the judgment about people.</p>
<p><strong>GLAUCON:</strong> It sounds like having less trust in him.</p>
<p><strong>SOCRATES:</strong> It sounds like having the right trust, instead of whole trust. Whole trust is what one gives the gods, and not even them that much. Giving a man the trust that belongs to a god is not honoring him. It is condemning him to disappoint you, because he has no way to pay a debt you yourself invented.</p>
<p><strong>GLAUCON:</strong> So the mentor is a man like me.</p>
<p><strong>SOCRATES:</strong> Who went a little further down a road. Only that, and that is already a lot. Notice that it is precisely the error that lets you recognize an equal in him. A master who never erred would not be a master, he would be an oracle, and before oracles one does not learn, one obeys. A man who errs and corrects, who shows where he stumbled, teaches you twice: he teaches the art and he teaches that the art fits inside a human life. The error is the proof that the road he walked is a road you can also walk.</p>
<p><strong>GLAUCON:</strong> I had never thought of the master&rsquo;s error as a gift.</p>
<p><strong>SOCRATES:</strong> It is not a gift, it is a fact. But it is a fact that frees you, if you accept it. As long as your master is perfect, you will be a pupil forever. On the day you see him err, and still respect him anyway, you will have begun to have an opinion of your own. And having your own opinion about people, including those you most admire, is the beginning of being someone, and not someone&rsquo;s shadow.</p>
<p><strong>GLAUCON:</strong> And Theophilus? Do I keep listening to him?</p>
<p><strong>SOCRATES:</strong> Keep listening to both. Learn from each what each one knows. And when one speaks ill of the other, do what you would do with any other claim: ask &ldquo;why?&rdquo;, listen, and decide for yourself. It is possible that one day you will find one of them was right. It is possible you will find both were being small, as men sometimes are. In either case you will have learned something neither of them could teach you: that your eyes count too.</p>
<p><strong>GLAUCON:</strong> And if my eyes err?</p>
<p><strong>SOCRATES:</strong> They will, and then you will be like them. <strong>That is what makes us human, Glaucon: not the absence of error, but the duty to judge even knowing we may err, and the decency to keep honoring those who erred before us</strong>.</p>
<p>..then Glaucon put both scrolls in the same bag, Socrates remarked that the bag looked lighter, and the two went for a beer.</p>
<h2>Glossary<span class="hx:absolute hx:-mt-20" id="glossary"></span>
    <a href="#glossary" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><em>agora</em>: the central square of Greek cities, where the people gathered</li>
<li><em>papyrus</em>: the material people wrote on before paper, kept in rolled scrolls</li>
<li><em>oracle</em>: a place or person that, in ancient Greece, gave answers held to come from the gods</li>
</ul>
<h2>Note<span class="hx:absolute hx:-mt-20" id="note"></span>
    <a href="#note" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This dialogue is a continuation of the <a href="/en/2026/04/11/dialogue-on-labels-and-truth/">Dialogue on Labels and Truth</a>. There, the question was the label others stick on us. Here, it is the label the people we admire stick on one another.</p>
<p>Everyone looks for a mentor, and the need is legitimate: nobody learns everything alone. The problem is not in trusting, it is in the amount. It is very easy to hand someone we admire not only what they know, but also what they think, including about other people. And when two people we respect do not respect each other, the comfort of having a guide collapses, because we discover that their judgment of the world was never a single one.</p>
<p>The way out is neither picking a side nor abandoning both. It is accepting that everyone errs, mentor included, and that having our own opinion about people, formed with our own reasons and our own eyes, is not disrespect. It is the only form of respect that does not demand the other be more than human.</p>
]]></content:encoded><category>philosophy</category><category>dialogues</category></item><item><title>Will, Excellence and Cardiology</title><link>https://apolzek.github.io/en/2026/10/09/will-excellence-and-cardiology/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/10/09/will-excellence-and-cardiology/</guid><pubDate>Fri, 09 Oct 2026 00:00:00 GMT</pubDate><description>&lt;p&gt;I have my national heroes. Brazilians who, through their patriotism, their determination and the way they led their lives, became examples of what I one day want to be.&lt;/p&gt;
&lt;p&gt;I have always admired medicine, especially those doctors who devote their efforts, their time and every cell of their own bodies to the pursuit of excellence. Among the specialties, one has always stirred my admiration: cardiology.&lt;/p&gt;
&lt;p&gt;One of the names I carry as a reference is &lt;strong&gt;Enéas Carneiro&lt;/strong&gt;. Many admired him for his intelligence, but I always saw in him something I consider even more important and less debatable: &lt;strong&gt;will&lt;/strong&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I have my national heroes. Brazilians who, through their patriotism, their determination and the way they led their lives, became examples of what I one day want to be.</p>
<p>I have always admired medicine, especially those doctors who devote their efforts, their time and every cell of their own bodies to the pursuit of excellence. Among the specialties, one has always stirred my admiration: cardiology.</p>
<p>One of the names I carry as a reference is <strong>Enéas Carneiro</strong>. Many admired him for his intelligence, but I always saw in him something I consider even more important and less debatable: <strong>will</strong>.</p>
<p>Enéas had will. He knew what he wanted, defended his convictions and wished for a better Brazil. Beyond that, he dedicated part of his life to the study of the heart, becoming a cardiologist.</p>
<p>But life found a way to turn that admiration into something much deeper.</p>
<p>On <strong>November 13, 2025</strong>, my daughter Luna was born with a severe congenital heart disease. She had to undergo heart surgery at Hospital Vila da Serra, in Nova Lima, Minas Gerais.</p>
<p>At that moment, everything I admired in medicine stopped being something distant, almost abstract, and became part of my own story.</p>
<p><strong>What I once admired, today I venerate.</strong></p>
<p>I believe God blessed the team of cardiologists at that hospital. Men and women who devoted their lives to knowledge, to science and to the refinement of their skills and who, through everything the human being has been able to learn and develop, were given the opportunity to save my daughter&rsquo;s life.</p>
<p>It is hard to find words that express the size of my gratitude. There is something deeply admirable in someone who dedicates their own existence to acquiring knowledge and skills that, one day, may mean the difference between life and death for another person.</p>
<p>I leave here my deepest respect and my eternal gratitude to healthcare professionals, especially the team that cared for Luna, and to everyone inside hospitals driven by a true intention and a greater purpose: <strong>saving lives</strong>.</p>
<p>If before I admired medicine for the knowledge and excellence of its professionals, today I also admire it for something no theory could ever teach me: the chance to keep watching my daughter grow.</p>
<blockquote>
  <p>&ldquo;The most important fact that sets the human being apart from all its brothers and cousins on the phylogenetic scale is knowledge; only knowledge frees man, only through knowledge is man free and, being free, he can aspire to a better condition of life for himself and all his fellow beings. I can only understand a society in which knowledge is the foremost reason the government gives for the formation of the citizen. My message is a positive one: that man must know, must understand, and in understanding, he is free.&rdquo;</p>
<p>— Dr. Enéas Ferreira Carneiro (Rio Branco, November 5, 1938 — Rio de Janeiro, May 6, 2007)</p>

</blockquote>
]]></content:encoded><category>personal</category><category>health</category></item><item><title>Repository Checklist</title><link>https://apolzek.github.io/en/2026/09/01/repository-checklist/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/09/01/repository-checklist/</guid><pubDate>Tue, 01 Sep 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;A checklist for what a well-maintained repository should ship, in its
documentation, its code, and its automation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to use it:&lt;/strong&gt; not every repo needs every item. Items marked &lt;code&gt;(A)&lt;/code&gt; are
&lt;em&gt;advanced&lt;/em&gt;, worth it for OSS projects, monorepos, or anything with external
contributors. Everything unmarked is the baseline.&lt;/p&gt;
&lt;p&gt;Sections marked &lt;em&gt;conditional&lt;/em&gt; only apply when the repo is that kind of project
(an HTTP service, a containerized app, a Kubernetes workload).&lt;/p&gt;</description><content:encoded><![CDATA[<p>A checklist for what a well-maintained repository should ship, in its
documentation, its code, and its automation.</p>
<p><strong>How to use it:</strong> not every repo needs every item. Items marked <code>(A)</code> are
<em>advanced</em>, worth it for OSS projects, monorepos, or anything with external
contributors. Everything unmarked is the baseline.</p>
<p>Sections marked <em>conditional</em> only apply when the repo is that kind of project
(an HTTP service, a containerized app, a Kubernetes workload).</p>
<h2>Index<span class="hx:absolute hx:-mt-20" id="index"></span>
    <a href="#index" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="#1-documentation">1. Documentation</a>
<ul>
<li><a href="#11-readmemd">1.1 README.md</a></li>
<li><a href="#12-governance-and-policy-files">1.2 Governance and policy files</a></li>
<li><a href="#13-deeper-docs-docs">1.3 Deeper docs (<code>docs/</code>)</a></li>
<li><a href="#14-changelog-and-releases">1.4 Changelog and releases</a></li>
</ul>
</li>
<li><a href="#2-code">2. Code</a>
<ul>
<li><a href="#21-layout-and-conventions">2.1 Layout and conventions</a></li>
<li><a href="#22-configuration">2.2 Configuration</a></li>
<li><a href="#23-single-source-of-truth--generated-code-a">2.3 Single source of truth / generated code (A)</a></li>
</ul>
</li>
<li><a href="#3-testing">3. Testing</a>
<ul>
<li><a href="#31-unit-tests">3.1 Unit tests</a></li>
<li><a href="#32-integration-and-end-to-end">3.2 Integration and end-to-end</a></li>
<li><a href="#33-performance-and-robustness-a">3.3 Performance and robustness (A)</a></li>
<li><a href="#34-coverage">3.4 Coverage</a></li>
</ul>
</li>
<li><a href="#4-api-artifacts-conditional-httpgrpc-services">4. API artifacts (conditional: HTTP/gRPC services)</a></li>
<li><a href="#5-build-and-runtime-conditional-containerized-apps">5. Build and runtime (conditional: containerized apps)</a>
<ul>
<li><a href="#51-docker">5.1 Docker</a></li>
<li><a href="#52-docker-compose">5.2 Docker Compose</a></li>
<li><a href="#53-kubernetes-conditional">5.3 Kubernetes (conditional)</a></li>
</ul>
</li>
<li><a href="#6-cicd">6. CI/CD</a>
<ul>
<li><a href="#61-basic-checks">6.1 Basic checks</a></li>
<li><a href="#62-security">6.2 Security</a></li>
<li><a href="#63-dependencies">6.3 Dependencies</a></li>
<li><a href="#64-repository-automation-a">6.4 Repository automation (A)</a></li>
</ul>
</li>
<li><a href="#7-repository-metadata">7. Repository metadata</a></li>
<li><a href="#minimum-viable-set">Minimum viable set</a></li>
</ul>
<h2>1. Documentation<span class="hx:absolute hx:-mt-20" id="1-documentation"></span>
    <a href="#1-documentation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>1.1 README.md<span class="hx:absolute hx:-mt-20" id="11-readmemd"></span>
    <a href="#11-readmemd" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Project overview: what it does and, more importantly, <em>why</em> it exists</li>
<li><input disabled="" type="checkbox"> Status badges: CI build, test coverage, latest release, stability level</li>
<li><input disabled="" type="checkbox"> Stability / maturity level (<code>development</code> → <code>alpha</code> → <code>beta</code> → <code>stable</code> → <code>deprecated</code>)</li>
<li><input disabled="" type="checkbox"> Support policy: who maintains it, where to ask, expected response time</li>
<li><input disabled="" type="checkbox"> Installation instructions</li>
<li><input disabled="" type="checkbox"> Running the app locally</li>
<li><input disabled="" type="checkbox"> Configuring your environment (<code>.env.example</code> committed, <code>.env</code> git-ignored)</li>
<li><input disabled="" type="checkbox"> Usage examples (copy-pasteable, actually tested)</li>
<li><input disabled="" type="checkbox"> Architecture diagram or a short &ldquo;how it fits together&rdquo; section</li>
<li><input disabled="" type="checkbox"> Link to contribution guidelines</li>
<li><input disabled="" type="checkbox"> License information</li>
<li><input disabled="" type="checkbox"> A README per module/subpackage, not only at the root (A)</li>
<li><input disabled="" type="checkbox"> Autogenerated status sections, delimited by markers, so they never drift (A)</li>
</ul>
<h3>1.2 Governance and policy files<span class="hx:absolute hx:-mt-20" id="12-governance-and-policy-files"></span>
    <a href="#12-governance-and-policy-files" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> <code>LICENSE</code> (+ <code>NOTICE</code> if you redistribute third-party code)</li>
<li><input disabled="" type="checkbox"> <code>CONTRIBUTING.md</code>: local setup, how to run tests, PR title/description
rules, review expectations</li>
<li><input disabled="" type="checkbox"> <code>CODEOWNERS</code>: ownership per directory, drives automatic review requests</li>
<li><input disabled="" type="checkbox"> <code>SECURITY.md</code>: how to report a vulnerability, privately</li>
<li><input disabled="" type="checkbox"> <code>CODE_OF_CONDUCT.md</code></li>
<li><input disabled="" type="checkbox"> <code>AGENTS.md</code> / <code>CLAUDE.md</code>: AI-assisted contribution policy (disclosure
trailer, what agents may and may not do)</li>
<li><input disabled="" type="checkbox"> <code>.github/pull_request_template.md</code></li>
<li><input disabled="" type="checkbox"> <code>.github/ISSUE_TEMPLATE/</code>: bug report, feature request, plus <code>config.yml</code>
pointing to support channels</li>
<li><input disabled="" type="checkbox"> Issue triage policy: labels, priorities, stale policy (A)</li>
<li><input disabled="" type="checkbox"> Maintainer/approver roles and how to become one (A)</li>
</ul>
<h3>1.3 Deeper docs (<code>docs/</code>)<span class="hx:absolute hx:-mt-20" id="13-deeper-docs-docs"></span>
    <a href="#13-deeper-docs-docs" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Release process, written down and repeatable</li>
<li><input disabled="" type="checkbox"> Testing guide: how to run each test tier locally</li>
<li><input disabled="" type="checkbox"> Onboarding guide for new modules/components (A)</li>
<li><input disabled="" type="checkbox"> ADRs (Architecture Decision Records): the <em>why</em> behind the design</li>
<li><input disabled="" type="checkbox"> Runbook / operational docs: alerts, dashboards, common failures</li>
<li><input disabled="" type="checkbox"> Generated reference docs: every config option, default, and emitted
metric, produced from the source of truth rather than hand-written (A)</li>
</ul>
<h3>1.4 Changelog and releases<span class="hx:absolute hx:-mt-20" id="14-changelog-and-releases"></span>
    <a href="#14-changelog-and-releases" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> <code>CHANGELOG.md</code> following SemVer / Keep a Changelog</li>
<li><input disabled="" type="checkbox"> Changelog fragments: one small file per PR, assembled at release time.
Kills merge conflicts and forces the author to state user impact (A)</li>
<li><input disabled="" type="checkbox"> Separate changelog for API/breaking changes vs. user-facing changes (A)</li>
<li><input disabled="" type="checkbox"> Tagged releases with release notes generated from the changelog</li>
<li><input disabled="" type="checkbox"> Documented versioning and deprecation policy</li>
</ul>
<h2>2. Code<span class="hx:absolute hx:-mt-20" id="2-code"></span>
    <a href="#2-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>2.1 Layout and conventions<span class="hx:absolute hx:-mt-20" id="21-layout-and-conventions"></span>
    <a href="#21-layout-and-conventions" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Consistent, documented project layout</li>
<li><input disabled="" type="checkbox"> License/SPDX header on every source file, enforced by the linter</li>
<li><input disabled="" type="checkbox"> Package-level doc comment on each package (<code>doc.go</code> or equivalent)</li>
<li><input disabled="" type="checkbox"> Linter config committed, with an explicit linter set and justified
per-path exceptions, not the tool&rsquo;s defaults</li>
<li><input disabled="" type="checkbox"> Formatter enforced in CI, not left to the reviewer</li>
<li><input disabled="" type="checkbox"> Import restrictions enforced (no forbidden/internal imports leaking out) (A)</li>
<li><input disabled="" type="checkbox"> A task runner (Makefile / justfile / npm scripts) with standard targets:
<code>test</code>, <code>lint</code>, <code>fmt</code>, <code>build</code>, <code>run</code>, <code>integration-test</code>, <code>tidy</code></li>
<li><input disabled="" type="checkbox"> The same commands work at the root and inside each module (A)</li>
</ul>
<h3>2.2 Configuration<span class="hx:absolute hx:-mt-20" id="22-configuration"></span>
    <a href="#22-configuration" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Typed config struct with an explicit <code>Validate()</code> and clear error messages</li>
<li><input disabled="" type="checkbox"> Fail fast on invalid config at startup, never mid-request</li>
<li><input disabled="" type="checkbox"> Committed example config, loaded by a real test</li>
<li><input disabled="" type="checkbox"> Published config schema (JSON Schema / equivalent) for editor autocomplete
and CI validation (A)</li>
<li><input disabled="" type="checkbox"> <code>examples/</code> with complete, scenario-based configurations: not snippets</li>
<li><input disabled="" type="checkbox"> Secrets never in config files or in the repo; injected at runtime</li>
<li><input disabled="" type="checkbox"> Structured logging, metrics, and tracing wired in and documented</li>
</ul>
<h3>2.3 Single source of truth / generated code (A)<span class="hx:absolute hx:-mt-20" id="23-single-source-of-truth--generated-code-a"></span>
    <a href="#23-single-source-of-truth--generated-code-a" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Declarative metadata file per component (owners, stability, config, emitted
telemetry)</li>
<li><input disabled="" type="checkbox"> Code, docs, and README sections generated from it</li>
<li><input disabled="" type="checkbox"> CI verifies the generated output is up to date: drift becomes a failing
build instead of stale documentation</li>
</ul>
<h2>3. Testing<span class="hx:absolute hx:-mt-20" id="3-testing"></span>
    <a href="#3-testing" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>3.1 Unit tests<span class="hx:absolute hx:-mt-20" id="31-unit-tests"></span>
    <a href="#31-unit-tests" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Unit tests that actually assert behavior, not just execute lines</li>
<li><input disabled="" type="checkbox"> Table-driven tests for branchy logic</li>
<li><input disabled="" type="checkbox"> Deterministic: no sleeps, no real clock, no network</li>
<li><input disabled="" type="checkbox"> Test fixtures (<code>testdata/</code>) versioned, with a command to regenerate them</li>
</ul>
<h3>3.2 Integration and end-to-end<span class="hx:absolute hx:-mt-20" id="32-integration-and-end-to-end"></span>
    <a href="#32-integration-and-end-to-end" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Integration tests isolated behind a build tag / separate command so they
don&rsquo;t slow the unit run</li>
<li><input disabled="" type="checkbox"> Real dependencies via ephemeral containers (Testcontainers or equivalent)</li>
<li><input disabled="" type="checkbox"> End-to-end tests against a realistic deployment (A)</li>
<li><input disabled="" type="checkbox"> Contract tests for consumed and provided APIs (A)</li>
</ul>
<h3>3.3 Performance and robustness (A)<span class="hx:absolute hx:-mt-20" id="33-performance-and-robustness-a"></span>
    <a href="#33-performance-and-robustness-a" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Benchmarks for hot paths, with a documented way to compare runs</li>
<li><input disabled="" type="checkbox"> Load tests with a stated performance baseline</li>
<li><input disabled="" type="checkbox"> Fuzz tests for anything that parses external input</li>
<li><input disabled="" type="checkbox"> Leak detection: goroutines, connections, file descriptors</li>
<li><input disabled="" type="checkbox"> Race detector enabled in CI</li>
</ul>
<h3>3.4 Coverage<span class="hx:absolute hx:-mt-20" id="34-coverage"></span>
    <a href="#34-coverage" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Coverage report generated in CI and published</li>
<li><input disabled="" type="checkbox"> Coverage threshold committed to the repo, split into <em>project</em> and <em>patch</em>
targets (e.g. 85% overall, 95% on the diff)</li>
<li><input disabled="" type="checkbox"> Per-module/per-component coverage, so a large repo doesn&rsquo;t hide a bad
corner behind a good average (A)</li>
</ul>
<h2>4. API artifacts <em>(conditional: HTTP/gRPC services)</em><span class="hx:absolute hx:-mt-20" id="4-api-artifacts-conditional-httpgrpc-services"></span>
    <a href="#4-api-artifacts-conditional-httpgrpc-services" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><input disabled="" type="checkbox"> OpenAPI / Swagger spec, versioned in the repo</li>
<li><input disabled="" type="checkbox"> Spec generated from or validated against the code, so it can&rsquo;t drift</li>
<li><input disabled="" type="checkbox"> Postman / Insomnia / Bruno collection</li>
<li><input disabled="" type="checkbox"> <code>.http</code> request files (runnable from the editor)</li>
<li><input disabled="" type="checkbox"> <code>curl.txt</code> with copy-pasteable examples</li>
<li><input disabled="" type="checkbox"> Documented authentication flow</li>
<li><input disabled="" type="checkbox"> Error catalogue: codes, meanings, and how to recover</li>
<li><input disabled="" type="checkbox"> Documented versioning and deprecation path for endpoints</li>
</ul>
<h2>5. Build and runtime <em>(conditional: containerized apps)</em><span class="hx:absolute hx:-mt-20" id="5-build-and-runtime-conditional-containerized-apps"></span>
    <a href="#5-build-and-runtime-conditional-containerized-apps" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>5.1 Docker<span class="hx:absolute hx:-mt-20" id="51-docker"></span>
    <a href="#51-docker" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> <code>Dockerfile</code> with multi-stage build</li>
<li><input disabled="" type="checkbox"> Minimal, pinned base image (distroless/slim), non-root user</li>
<li><input disabled="" type="checkbox"> <code>.dockerignore</code></li>
<li><input disabled="" type="checkbox"> Healthcheck defined</li>
<li><input disabled="" type="checkbox"> Reproducible builds: pinned dependencies, no <code>latest</code></li>
<li><input disabled="" type="checkbox"> Image vulnerability scan in CI (A)</li>
<li><input disabled="" type="checkbox"> SBOM generated and published with the image (A)</li>
</ul>
<h3>5.2 Docker Compose<span class="hx:absolute hx:-mt-20" id="52-docker-compose"></span>
    <a href="#52-docker-compose" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> <code>docker-compose.yml</code> that brings the whole stack up with one command</li>
<li><input disabled="" type="checkbox"> Environment-specific overrides (<code>docker-compose.dev.yml</code>, <code>.prod.yml</code>)</li>
<li><input disabled="" type="checkbox"> Dependencies (DB, broker, cache) included so a new dev needs nothing else</li>
</ul>
<h3>5.3 Kubernetes <em>(conditional)</em><span class="hx:absolute hx:-mt-20" id="53-kubernetes-conditional"></span>
    <a href="#53-kubernetes-conditional" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Manifests, or a Helm chart with a documented <code>values.yaml</code></li>
<li><input disabled="" type="checkbox"> Resource requests and limits set</li>
<li><input disabled="" type="checkbox"> Liveness / readiness / startup probes</li>
<li><input disabled="" type="checkbox"> Secrets management (External Secrets, SOPS, sealed-secrets, never plain)</li>
<li><input disabled="" type="checkbox"> Ingress configuration</li>
<li><input disabled="" type="checkbox"> Per-environment overlays (Kustomize) or value files</li>
<li><input disabled="" type="checkbox"> <code>NetworkPolicy</code>, <code>PodDisruptionBudget</code>, security context (A)</li>
<li><input disabled="" type="checkbox"> Manifests linted and validated in CI (<code>kubeconform</code>, <code>helm lint</code>) (A)</li>
</ul>
<h2>6. CI/CD<span class="hx:absolute hx:-mt-20" id="6-cicd"></span>
    <a href="#6-cicd" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>6.1 Basic checks<span class="hx:absolute hx:-mt-20" id="61-basic-checks"></span>
    <a href="#61-basic-checks" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Lint</li>
<li><input disabled="" type="checkbox"> Format check</li>
<li><input disabled="" type="checkbox"> Unit tests</li>
<li><input disabled="" type="checkbox"> Coverage report + threshold enforcement</li>
<li><input disabled="" type="checkbox"> Build the artifact (binary and/or image)</li>
<li><input disabled="" type="checkbox"> Integration tests, in a separate job</li>
<li><input disabled="" type="checkbox"> Required status checks configured on the protected branch</li>
<li><input disabled="" type="checkbox"> Build matrix across supported OS/versions (A)</li>
<li><input disabled="" type="checkbox"> Scope detection: only run what the diff actually affects (A)</li>
<li><input disabled="" type="checkbox"> Job sharding to keep wall-clock time sane on large repos (A)</li>
</ul>
<h3>6.2 Security<span class="hx:absolute hx:-mt-20" id="62-security"></span>
    <a href="#62-security" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Dependency vulnerability scanning</li>
<li><input disabled="" type="checkbox"> SAST (CodeQL or equivalent)</li>
<li><input disabled="" type="checkbox"> Secret scanning enabled</li>
<li><input disabled="" type="checkbox"> License compliance check on dependencies</li>
<li><input disabled="" type="checkbox"> Workflow files themselves linted (<code>actionlint</code>, <code>zizmor</code>) (A)</li>
<li><input disabled="" type="checkbox"> Shell scripts linted (<code>shellcheck</code>) (A)</li>
<li><input disabled="" type="checkbox"> Minimal <code>permissions:</code> per workflow; third-party actions pinned to a SHA</li>
<li><input disabled="" type="checkbox"> OpenSSF Scorecard (A)</li>
</ul>
<h3>6.3 Dependencies<span class="hx:absolute hx:-mt-20" id="63-dependencies"></span>
    <a href="#63-dependencies" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Automated updates (Renovate / Dependabot) with a grouping strategy</li>
<li><input disabled="" type="checkbox"> Lockfile committed</li>
<li><input disabled="" type="checkbox"> Automated tidy/prune check so the manifest can&rsquo;t drift</li>
<li><input disabled="" type="checkbox"> Public API compatibility check between versions (A)</li>
</ul>
<h3>6.4 Repository automation (A)<span class="hx:absolute hx:-mt-20" id="64-repository-automation-a"></span>
    <a href="#64-repository-automation-a" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><input disabled="" type="checkbox"> Auto-assign reviewers from <code>CODEOWNERS</code></li>
<li><input disabled="" type="checkbox"> Auto-label PRs and issues by affected area</li>
<li><input disabled="" type="checkbox"> Ping owners on new issues touching their area</li>
<li><input disabled="" type="checkbox"> Stale issue/PR handling</li>
<li><input disabled="" type="checkbox"> First-time contributor welcome</li>
<li><input disabled="" type="checkbox"> Merge freeze during a release window</li>
<li><input disabled="" type="checkbox"> Periodic repo health report (open issues, unowned areas)</li>
<li><input disabled="" type="checkbox"> Release preparation automated (version bump, changelog assembly, tag)</li>
</ul>
<h2>7. Repository metadata<span class="hx:absolute hx:-mt-20" id="7-repository-metadata"></span>
    <a href="#7-repository-metadata" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><input disabled="" type="checkbox"> Topics/tags with the technologies used, for discoverability</li>
<li><input disabled="" type="checkbox"> Clear, one-line repository description</li>
<li><input disabled="" type="checkbox"> Branch protection rules on the default branch</li>
<li><input disabled="" type="checkbox"> Signed commits or signed tags (A)</li>
<li><input disabled="" type="checkbox"> Archive/deprecation notice when a repo stops being maintained</li>
</ul>
<h2>Minimum viable set<span class="hx:absolute hx:-mt-20" id="minimum-viable-set"></span>
    <a href="#minimum-viable-set" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you only do ten things, do these:</p>
<ol>
<li>README that gets someone running locally in under five minutes</li>
<li><code>LICENSE</code></li>
<li><code>CONTRIBUTING.md</code></li>
<li><code>.env.example</code> committed, secrets out of the repo</li>
<li>Unit tests with a coverage threshold enforced in CI</li>
<li>Lint + format enforced in CI</li>
<li>Multi-stage <code>Dockerfile</code> and a one-command <code>docker-compose up</code></li>
<li><code>CHANGELOG.md</code> and tagged releases</li>
<li>Dependency and vulnerability scanning</li>
<li>Branch protection with required checks</li>
</ol>
]]></content:encoded><category>software-engineering</category><category>git</category><category>open-source</category></item><item><title>Dialogue on the New Tool</title><link>https://apolzek.github.io/en/2026/09/01/dialogue-on-the-new-tool/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/09/01/dialogue-on-the-new-tool/</guid><pubDate>Tue, 01 Sep 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;&lt;em&gt;Setting: late afternoon, a bar table near the office. Glaucon arrives with his laptop under his arm and opens it before even sitting down.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GLAUCON:&lt;/strong&gt; Socrates, I found the tool that solves everything. I read the documentation last night, got an example running in twenty minutes and it worked on the first try. I am going to rewrite the whole system on top of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SOCRATES:&lt;/strong&gt; Twenty minutes. When you test drive a car, how long does it last?&lt;/p&gt;</description><content:encoded><![CDATA[<p><em>Setting: late afternoon, a bar table near the office. Glaucon arrives with his laptop under his arm and opens it before even sitting down.</em></p>
<p><strong>GLAUCON:</strong> Socrates, I found the tool that solves everything. I read the documentation last night, got an example running in twenty minutes and it worked on the first try. I am going to rewrite the whole system on top of it.</p>
<p><strong>SOCRATES:</strong> Twenty minutes. When you test drive a car, how long does it last?</p>
<p><strong>GLAUCON:</strong> About fifteen minutes. A lap around the block with the salesman beside you.</p>
<p><strong>SOCRATES:</strong> And have you ever found a defect in a car on a lap around the block?</p>
<p><strong>GLAUCON:</strong> Never. Defects show up later. On the highway, in the rain, in stalled traffic.</p>
<p><strong>SOCRATES:</strong> So in those twenty minutes, did you get to know the tool, or did you get to know the route its author chose for you to take?</p>
<p><strong>GLAUCON:</strong> The route. But I liked the route.</p>
<p><strong>SOCRATES:</strong> Liking it is already something. Let me ask you a few questions, then. Just do not answer with what you imagine. Answer with what you know.</p>
<p><strong>GLAUCON:</strong> Go ahead.</p>
<p><strong>SOCRATES:</strong> How many records went through it in your test?</p>
<p><strong>GLAUCON:</strong> About a thousand.</p>
<p><strong>SOCRATES:</strong> And on an ordinary day, in production?</p>
<p><strong>GLAUCON:</strong> About ten million.</p>
<p><strong>SOCRATES:</strong> And what does it do when it cannot keep up? Hold what is coming in, drop it, or go down with it?</p>
<p><strong>GLAUCON:</strong> I do not know. I never got to that point.</p>
<p><strong>SOCRATES:</strong> Nobody does, in a test. And notice: scale does not invent any defects. It promotes the ones that were already there and nobody saw. A slow operation over a thousand records is invisible. Over ten million, it is the whole system. You will not discover new defects in production, you will discover the real size of the ones you already had.</p>
<p><strong>SOCRATES:</strong> What language was it written in?</p>
<p><strong>GLAUCON:</strong> Does it matter? It runs in a container, I do not even need to know.</p>
<p><strong>SOCRATES:</strong> It is the one part of it you will not be able to swap later. The language has already decided, before you arrived, a lot of things you will only feel in production. Which is it?</p>
<p><strong>GLAUCON:</strong> Rust.</p>
<p><strong>SOCRATES:</strong> Then it is compiled, and that gives you good things for free. It will arrive as a single file, with no interpreter or loose dependency to carry along, and it starts in a blink. An interpreted tool forces you to look after it and its interpreter for the rest of your life.</p>
<p><strong>GLAUCON:</strong> Great. So it is the right choice.</p>
<p><strong>SOCRATES:</strong> It is a choice, and every choice has the other side facing down. That single file carries inside it the libraries the author chose, in the versions he chose, on the day he compiled it. When a problem shows up in one of those libraries, you do not swap the part. You wait for him to compile again.</p>
<p><strong>GLAUCON:</strong> I had not thought of that.</p>
<p><strong>SOCRATES:</strong> It is the same package, looked at from the other side. Second thing: Rust has no garbage collector.</p>
<p><strong>GLAUCON:</strong> That is good, isn&rsquo;t it?</p>
<p><strong>SOCRATES:</strong> It is good for one specific thing. A language with a collector has a janitor who cleans the house when he decides to, not when you ask, and that shows up as a slowness that arrives unannounced and leaves on its own, right when volume grows. Without a collector, response time is predictable, and predictable is worth a lot.</p>
<p><strong>GLAUCON:</strong> So I have nothing to worry about regarding memory.</p>
<p><strong>SOCRATES:</strong> You do, and of a quieter kind. Without a collector nothing gets swept up by accident: whatever the tool holds, it holds until the end. An unbounded queue or a cache nobody told to stop do not show up as a pause, they show up as a line slowly climbing for a week until someone gets woken up in the middle of the night. A garbage collector is noisy and warns you. Its absence is silent.</p>
<p><strong>GLAUCON:</strong> Can that be known beforehand?</p>
<p><strong>SOCRATES:</strong> Two questions settle it. How much memory it consumes after a week running, and not in the first ten minutes. And how many cores it actually uses, because it is one thing for the language to know how to use eight, and another for the tool to have been written for that.</p>
<p><strong>GLAUCON:</strong> That changes the whole design.</p>
<p><strong>SOCRATES:</strong> It does, and it is far better to change it now, while it is still paper. Next: how does a person prove to it that they are who they say they are?</p>
<p><strong>GLAUCON:</strong> Username and password, created right there inside it.</p>
<p><strong>SOCRATES:</strong> And in your company, how do people prove that everywhere else?</p>
<p><strong>GLAUCON:</strong> Through LDAP.</p>
<p><strong>SOCRATES:</strong> Then it will be the only place in the company where a person exists twice, and whoever gets let go on a Friday will leave every place except one. Now the other half: after the person gets in, who decides what they can do?</p>
<p><strong>GLAUCON:</strong> Whoever gets in can do everything, I think.</p>
<p><strong>SOCRATES:</strong> You just treated as one thing what are two. One is who gets in. The other is what each one does after getting in. A tool for a single person does not need to separate the two, because the person is you.</p>
<p><strong>GLAUCON:</strong> It is just that nobody on my team would cause damage on purpose.</p>
<p><strong>SOCRATES:</strong> It is almost never on purpose. Permissions do not exist out of distrust, they exist to limit the size of a distracted person&rsquo;s mistake on a Tuesday. When everyone can do everything, everyone carries the power to bring everything down, and nobody asked for that power.</p>
<p><strong>GLAUCON:</strong> I had never thought of it that way.</p>
<p><strong>SOCRATES:</strong> And when someone deletes what they should not have, do you find out who it was?</p>
<p><strong>GLAUCON:</strong> I do not know whether it keeps that.</p>
<p><strong>SOCRATES:</strong> Ask. A log does not exist to accuse anyone, it exists precisely so that nobody needs to be accused. Without it, on the day of the problem everyone who had access is a suspect, and the conversation becomes memory against memory. Memory is the most biased witness there is.</p>
<p><strong>SOCRATES:</strong> When did its last version come out?</p>
<p><strong>GLAUCON:</strong> About two months ago, I think.</p>
<p><strong>SOCRATES:</strong> And the one before that?</p>
<p><strong>GLAUCON:</strong> I did not look.</p>
<p><strong>SOCRATES:</strong> Look, because the interval between versions is the clock you will tie yours to. And look at something finer: between one big version and the next, do small versions come out, with fixes only?</p>
<p><strong>GLAUCON:</strong> I did not notice.</p>
<p><strong>SOCRATES:</strong> Notice. If they do, on the day of a serious problem you update a decimal place and go back to sleep. If they do not, the fix only arrives together with the next big version, with everything else it changes thrown in, and an afternoon&rsquo;s problem becomes a whole migration on a day you did not choose.</p>
<p><strong>GLAUCON:</strong> That I can check today.</p>
<p><strong>SOCRATES:</strong> Check it along with this one: how many people actually maintain it?</p>
<p><strong>GLAUCON:</strong> The repository has eight thousand stars and hundreds of contributors.</p>
<p><strong>SOCRATES:</strong> A star is applause, and applause fixes nothing. A contributor, on most of those lists, is anyone who ever fixed a comma in the documentation. Count something else: in the last six months, how many different names touched the <em>core</em> of the tool? And of those, how many are left after you remove the first one?</p>
<p><strong>GLAUCON:</strong> I do not know. I imagine few.</p>
<p><strong>SOCRATES:</strong> If one is left, you did not choose a tool. You chose a person, and that person signed nothing with you.</p>
<p><strong>GLAUCON:</strong> Are there many questions left?</p>
<p><strong>SOCRATES:</strong> One. You said it does almost everything you need. And the almost?</p>
<p><strong>GLAUCON:</strong> Whatever is missing, I open an issue and ask.</p>
<p><strong>SOCRATES:</strong> Always? Every time a new need appears, the path will be to ask and wait?</p>
<p><strong>GLAUCON:</strong> It is what can be done.</p>
<p><strong>SOCRATES:</strong> Then notice what you accepted: the pace of what you deliver becomes decided by people who do not know your problem and owe you nothing. There is another path, and it is the difference between a tool and a wall: a place where you yourself write what was missing. A plugin, a hook, a script it calls at the right moment. When that place exists, what is missing today is your work. When it does not, what is missing today is waiting.</p>
<p><strong>GLAUCON:</strong> Socrates, I could hardly answer anything.</p>
<p><strong>SOCRATES:</strong> And it is the best thing that happened to you today. Notice that you did not discover that the tool is bad. You discovered that you do not know it. Those are very different things, and only one of them can be fixed.</p>
<p><strong>GLAUCON:</strong> But I was confident.</p>
<p><strong>SOCRATES:</strong> You were confident of what you saw, and you saw twenty minutes chosen by someone else. Everyone is confident at the start. The difference is that some discover the questions before installing, and others discover them afterwards, with the whole system standing on top of them.</p>
<p><strong>GLAUCON:</strong> So I give up on it?</p>
<p><strong>SOCRATES:</strong> That is not what I said. Maybe it is excellent. Go after every answer and come back with them. If they are good, you install it knowing what you installed. If they are bad, you already know where it will hurt, and sometimes you install it anyway, on purpose, because hurting in that spot is acceptable.</p>
<p><strong>GLAUCON:</strong> And if I am wrong even after asking everything?</p>
<p><strong>SOCRATES:</strong> You will be wrong now and then, and that is fine. Put the new thing where a mistake is cheap, on an edge you can switch off in an afternoon without waking anyone, and keep the middle boring and predictable. <strong>Before asking what a tool does, ask how much it costs to leave it</strong>. That is the one answer nobody writes in the documentation.</p>
<p><strong>GLAUCON:</strong> I am going to redo the diagram.</p>
<p><strong>SOCRATES:</strong> Not today. Keep it for a week and open it again. Enthusiasm is good fuel and a terrible driver.</p>
<p>..then Socrates closed the laptop, Glaucon saved the diagram without deleting anything, and the two went for a beer.</p>
<h2>Note<span class="hx:absolute hx:-mt-20" id="note"></span>
    <a href="#note" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The idea was to take the dialogue form of <strong>Plato&rsquo;s Republic</strong> and apply it to a technology case. Only the form is borrowed: short questions, one idea at a time, the interlocutor&rsquo;s answer opening the next question.</p>
<p>The case is familiar to anyone who works with this. Every week a new tool appears, open source, with a pretty interface, an example that boots in five minutes and a star chart going up. At that point everything seems solved, and the enthusiasm is legitimate.</p>
<p>Things get more complicated when the questions begin. How it behaves at scale. What happens on the day you need something the author did not foresee. How long a bug report takes to become a fix, and whether anyone even replies. How many real maintainers stand behind the repository numbers, and from how many different companies. Who decides the direction of the project, and where that decision is discussed. How it does authentication, and whether that talks to the login the rest of the company already uses. What it does when a piece of it goes down. Whether you can audit afterwards who did what inside it. And how much work it actually is to integrate it with what you already have running.</p>
<p>Almost none of these answers are on the first page. Most show up about six months later, when switching is no longer free.</p>
]]></content:encoded><category>philosophy</category><category>dialogues</category><category>technology</category></item><item><title>Data and ML Stack Tools by Category</title><link>https://apolzek.github.io/en/2026/09/01/data-and-ml-stack-tools/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/09/01/data-and-ml-stack-tools/</guid><pubDate>Tue, 01 Sep 2026 12:00:00 GMT</pubDate><description>&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Tools / Technologies&lt;/th&gt;
&lt;th&gt;Primary Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine Learning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MLflow, Kubeflow, Feast, Ray, TensorFlow, PyTorch, Scikit-learn, XGBoost, LightGBM&lt;/td&gt;
&lt;td&gt;Training, tracking, serving, feature store and MLOps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Science / Analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python, Jupyter, Pandas, Polars, NumPy, SciPy, R, Apache Arrow&lt;/td&gt;
&lt;td&gt;Data exploration, statistical analysis and data preparation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BI / Analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Superset, Metabase, Grafana, Power BI, Tableau&lt;/td&gt;
&lt;td&gt;Dashboards, data exploration and visualization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Engineering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Spark, Apache Flink, Apache Beam, Apache Kafka, dbt&lt;/td&gt;
&lt;td&gt;Data processing, pipelines and transformations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Ingestion / CDC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Kafka, Debezium, Kafka Connect, Apache NiFi, Airbyte, Fivetran&lt;/td&gt;
&lt;td&gt;Data ingestion, CDC and data integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workflow Orchestration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Airflow, Dagster, Prefect, Argo Workflows&lt;/td&gt;
&lt;td&gt;Pipeline and workflow orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Transformation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;dbt, Apache Spark, SQLMesh, Dataform&lt;/td&gt;
&lt;td&gt;Data transformation, modeling and ELT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Great Expectations, Soda, dbt Tests, Deequ&lt;/td&gt;
&lt;td&gt;Data validation and quality checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Catalog / Governance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenMetadata, DataHub, Apache Atlas, Amundsen, OpenLineage&lt;/td&gt;
&lt;td&gt;Data catalog, lineage and governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Lake Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Amazon S3, MinIO, Google Cloud Storage, Azure Data Lake Storage&lt;/td&gt;
&lt;td&gt;Object storage and data lake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File Formats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Parquet, Apache ORC, Apache Avro, Apache Arrow&lt;/td&gt;
&lt;td&gt;Data storage and interchange formats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open Table Formats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Iceberg, Apache Hudi, Delta Lake, Apache Paimon&lt;/td&gt;
&lt;td&gt;ACID tables, schema evolution, snapshots and time travel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lakehouse Catalog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Polaris, Project Nessie, AWS Glue Catalog, Hive Metastore&lt;/td&gt;
&lt;td&gt;Table catalog and metadata management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query Engines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trino, Presto, DuckDB, ClickHouse, Apache Doris, StarRocks&lt;/td&gt;
&lt;td&gt;SQL analytics and OLAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lakehouse Platforms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Databricks, Dremio, Starburst, Snowflake&lt;/td&gt;
&lt;td&gt;Data lakehouse and analytics platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Feature Store&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Feast, Hopsworks, Tecton&lt;/td&gt;
&lt;td&gt;ML feature management and serving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ML Pipelines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kubeflow Pipelines, MLflow, Airflow, Metaflow, Flyte&lt;/td&gt;
&lt;td&gt;Machine learning pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model Serving&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;KServe, Seldon, NVIDIA Triton, Ray Serve, BentoML, vLLM&lt;/td&gt;
&lt;td&gt;Model inference and serving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Experiment Tracking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MLflow, Weights &amp;amp; Biases, Neptune, Comet&lt;/td&gt;
&lt;td&gt;Experiment tracking and metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model Registry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MLflow, Kubeflow, Vertex AI, SageMaker, Azure ML&lt;/td&gt;
&lt;td&gt;Model versioning and lifecycle management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vector Databases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Qdrant, Milvus, Weaviate, pgvector, Pinecone&lt;/td&gt;
&lt;td&gt;Embeddings, semantic search and RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Streaming / Event Processing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Kafka, Apache Flink, Redpanda, Apache Pulsar&lt;/td&gt;
&lt;td&gt;Real-time data processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Warehouse&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Snowflake, BigQuery, Redshift, ClickHouse, DuckDB&lt;/td&gt;
&lt;td&gt;Structured analytics and OLAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Observability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenTelemetry, Grafana, Prometheus, OpenLineage, Marquez&lt;/td&gt;
&lt;td&gt;Data and pipeline observability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Reference architectures&lt;span class="hx:absolute hx:-mt-20" id="reference-architectures"&gt;&lt;/span&gt;
&lt;a href="#reference-architectures" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Three ways to wire the tools above into a working stack. All of them have the
same three layers, ingestion, processing and storage, plus whatever consumes
the result.&lt;/p&gt;</description><content:encoded><![CDATA[<table>
  <thead>
      <tr>
          <th>Category</th>
          <th>Tools / Technologies</th>
          <th>Primary Use</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Machine Learning</strong></td>
          <td>MLflow, Kubeflow, Feast, Ray, TensorFlow, PyTorch, Scikit-learn, XGBoost, LightGBM</td>
          <td>Training, tracking, serving, feature store and MLOps</td>
      </tr>
      <tr>
          <td><strong>Data Science / Analytics</strong></td>
          <td>Python, Jupyter, Pandas, Polars, NumPy, SciPy, R, Apache Arrow</td>
          <td>Data exploration, statistical analysis and data preparation</td>
      </tr>
      <tr>
          <td><strong>BI / Analytics</strong></td>
          <td>Apache Superset, Metabase, Grafana, Power BI, Tableau</td>
          <td>Dashboards, data exploration and visualization</td>
      </tr>
      <tr>
          <td><strong>Data Engineering</strong></td>
          <td>Apache Spark, Apache Flink, Apache Beam, Apache Kafka, dbt</td>
          <td>Data processing, pipelines and transformations</td>
      </tr>
      <tr>
          <td><strong>Data Ingestion / CDC</strong></td>
          <td>Apache Kafka, Debezium, Kafka Connect, Apache NiFi, Airbyte, Fivetran</td>
          <td>Data ingestion, CDC and data integration</td>
      </tr>
      <tr>
          <td><strong>Workflow Orchestration</strong></td>
          <td>Apache Airflow, Dagster, Prefect, Argo Workflows</td>
          <td>Pipeline and workflow orchestration</td>
      </tr>
      <tr>
          <td><strong>Data Transformation</strong></td>
          <td>dbt, Apache Spark, SQLMesh, Dataform</td>
          <td>Data transformation, modeling and ELT</td>
      </tr>
      <tr>
          <td><strong>Data Quality</strong></td>
          <td>Great Expectations, Soda, dbt Tests, Deequ</td>
          <td>Data validation and quality checks</td>
      </tr>
      <tr>
          <td><strong>Data Catalog / Governance</strong></td>
          <td>OpenMetadata, DataHub, Apache Atlas, Amundsen, OpenLineage</td>
          <td>Data catalog, lineage and governance</td>
      </tr>
      <tr>
          <td><strong>Data Lake Storage</strong></td>
          <td>Amazon S3, MinIO, Google Cloud Storage, Azure Data Lake Storage</td>
          <td>Object storage and data lake</td>
      </tr>
      <tr>
          <td><strong>File Formats</strong></td>
          <td>Apache Parquet, Apache ORC, Apache Avro, Apache Arrow</td>
          <td>Data storage and interchange formats</td>
      </tr>
      <tr>
          <td><strong>Open Table Formats</strong></td>
          <td>Apache Iceberg, Apache Hudi, Delta Lake, Apache Paimon</td>
          <td>ACID tables, schema evolution, snapshots and time travel</td>
      </tr>
      <tr>
          <td><strong>Lakehouse Catalog</strong></td>
          <td>Apache Polaris, Project Nessie, AWS Glue Catalog, Hive Metastore</td>
          <td>Table catalog and metadata management</td>
      </tr>
      <tr>
          <td><strong>Query Engines</strong></td>
          <td>Trino, Presto, DuckDB, ClickHouse, Apache Doris, StarRocks</td>
          <td>SQL analytics and OLAP</td>
      </tr>
      <tr>
          <td><strong>Lakehouse Platforms</strong></td>
          <td>Databricks, Dremio, Starburst, Snowflake</td>
          <td>Data lakehouse and analytics platforms</td>
      </tr>
      <tr>
          <td><strong>Feature Store</strong></td>
          <td>Feast, Hopsworks, Tecton</td>
          <td>ML feature management and serving</td>
      </tr>
      <tr>
          <td><strong>ML Pipelines</strong></td>
          <td>Kubeflow Pipelines, MLflow, Airflow, Metaflow, Flyte</td>
          <td>Machine learning pipelines</td>
      </tr>
      <tr>
          <td><strong>Model Serving</strong></td>
          <td>KServe, Seldon, NVIDIA Triton, Ray Serve, BentoML, vLLM</td>
          <td>Model inference and serving</td>
      </tr>
      <tr>
          <td><strong>Experiment Tracking</strong></td>
          <td>MLflow, Weights &amp; Biases, Neptune, Comet</td>
          <td>Experiment tracking and metrics</td>
      </tr>
      <tr>
          <td><strong>Model Registry</strong></td>
          <td>MLflow, Kubeflow, Vertex AI, SageMaker, Azure ML</td>
          <td>Model versioning and lifecycle management</td>
      </tr>
      <tr>
          <td><strong>Vector Databases</strong></td>
          <td>Qdrant, Milvus, Weaviate, pgvector, Pinecone</td>
          <td>Embeddings, semantic search and RAG</td>
      </tr>
      <tr>
          <td><strong>Streaming / Event Processing</strong></td>
          <td>Apache Kafka, Apache Flink, Redpanda, Apache Pulsar</td>
          <td>Real-time data processing</td>
      </tr>
      <tr>
          <td><strong>Data Warehouse</strong></td>
          <td>Snowflake, BigQuery, Redshift, ClickHouse, DuckDB</td>
          <td>Structured analytics and OLAP</td>
      </tr>
      <tr>
          <td><strong>Data Observability</strong></td>
          <td>OpenTelemetry, Grafana, Prometheus, OpenLineage, Marquez</td>
          <td>Data and pipeline observability</td>
      </tr>
  </tbody>
</table>
<h2>Reference architectures<span class="hx:absolute hx:-mt-20" id="reference-architectures"></span>
    <a href="#reference-architectures" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Three ways to wire the tools above into a working stack. All of them have the
same three layers, ingestion, processing and storage, plus whatever consumes
the result.</p>
<h3>1. Batch lakehouse<span class="hx:absolute hx:-mt-20" id="1-batch-lakehouse"></span>
    <a href="#1-batch-lakehouse" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Nightly and hourly ELT. The default when latency is measured in hours.</p>
<pre class="mermaid hx:mt-6">
  flowchart TB
  subgraph ing[&#34;Ingestion&#34;]
    airbyte[&#34;Airbyte&#34;]
    debezium[&#34;Debezium + Kafka Connect&#34;]
  end
  subgraph proc[&#34;Processing&#34;]
    spark[&#34;Apache Spark&#34;]
    dbt[&#34;dbt&#34;]
    ge[&#34;Great Expectations&#34;]
  end
  subgraph sto[&#34;Storage&#34;]
    iceberg[&#34;Apache Iceberg&#34;]
    s3[&#34;MinIO / S3&#34;]
    polaris[&#34;Apache Polaris&#34;]
  end
  subgraph out[&#34;Consumption&#34;]
    trino[&#34;Trino&#34;]
    superset[&#34;Apache Superset&#34;]
  end

  airbyte --&gt; spark
  debezium --&gt; spark
  spark --&gt; dbt --&gt; ge --&gt; iceberg
  s3 -.- iceberg
  polaris -.- iceberg
  iceberg --&gt; trino --&gt; superset
</pre><p>Apache Airflow sits outside the picture, triggering every arrow above.</p>
<h3>2. Streaming<span class="hx:absolute hx:-mt-20" id="2-streaming"></span>
    <a href="#2-streaming" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Seconds instead of hours. Two branches out of one job: the table for history,
the OLAP store for the dashboard.</p>
<pre class="mermaid hx:mt-6">
  flowchart TB
  subgraph ing[&#34;Ingestion&#34;]
    debezium[&#34;Debezium&#34;]
    kafka[&#34;Apache Kafka&#34;]
  end
  subgraph proc[&#34;Processing&#34;]
    flink[&#34;Apache Flink&#34;]
  end
  subgraph sto[&#34;Storage&#34;]
    paimon[&#34;Apache Paimon on S3&#34;]
    clickhouse[&#34;ClickHouse&#34;]
  end
  subgraph out[&#34;Consumption&#34;]
    trino[&#34;Trino&#34;]
    grafana[&#34;Grafana&#34;]
  end

  debezium --&gt; kafka --&gt; flink
  flink --&gt; paimon --&gt; trino
  flink --&gt; clickhouse --&gt; grafana
</pre><p>Airflow is still needed here, not to schedule the job but to run the compaction
and snapshot expiration the table needs because the job never stops writing.</p>
<h3>3. ML and inference<span class="hx:absolute hx:-mt-20" id="3-ml-and-inference"></span>
    <a href="#3-ml-and-inference" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Features written by the same pipeline that feeds training, and a model served
out of a registry.</p>
<pre class="mermaid hx:mt-6">
  flowchart TB
  subgraph ing[&#34;Ingestion&#34;]
    kafka[&#34;Apache Kafka&#34;]
  end
  subgraph proc[&#34;Processing&#34;]
    flink[&#34;Apache Flink&#34;]
    spark[&#34;Apache Spark&#34;]
  end
  subgraph sto[&#34;Storage&#34;]
    feast[&#34;Feast&#34;]
    delta[&#34;Delta Lake on S3&#34;]
    qdrant[&#34;Qdrant&#34;]
  end
  subgraph train[&#34;Training&#34;]
    ray[&#34;Ray&#34;]
    mlflow[&#34;MLflow Registry&#34;]
  end
  subgraph serve[&#34;Serving&#34;]
    kserve[&#34;KServe&#34;]
    vllm[&#34;vLLM&#34;]
  end

  kafka --&gt; flink
  kafka --&gt; spark
  flink --&gt; feast
  flink --&gt; qdrant
  spark --&gt; delta
  delta --&gt; ray --&gt; mlflow --&gt; kserve
  feast --&gt; kserve
  qdrant --&gt; vllm
</pre><h2>Official repositories<span class="hx:absolute hx:-mt-20" id="official-repositories"></span>
    <a href="#official-repositories" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="https://github.com/airbytehq/airbyte"target="_blank" rel="noopener">Airbyte</a>: Connector-based ELT for loading data into a warehouse or lake.</li>
<li><a href="https://github.com/amundsen-io/amundsen"target="_blank" rel="noopener">Amundsen</a>: Data discovery and metadata search for analysts.</li>
<li><a href="https://github.com/apache/airflow"target="_blank" rel="noopener">Apache Airflow</a>: Schedules and monitors pipelines written as Python DAGs.</li>
<li><a href="https://github.com/apache/arrow"target="_blank" rel="noopener">Apache Arrow</a>: In-memory columnar format shared between engines and languages.</li>
<li><a href="https://github.com/apache/atlas"target="_blank" rel="noopener">Apache Atlas</a>: Metadata and lineage governance for the Hadoop ecosystem.</li>
<li><a href="https://github.com/apache/avro"target="_blank" rel="noopener">Apache Avro</a>: Row-oriented serialization format that carries its schema.</li>
<li><a href="https://github.com/apache/beam"target="_blank" rel="noopener">Apache Beam</a>: One pipeline model that runs on several batch and streaming engines.</li>
<li><a href="https://github.com/apache/doris"target="_blank" rel="noopener">Apache Doris</a>: MPP analytical database aimed at real-time reporting.</li>
<li><a href="https://github.com/apache/flink"target="_blank" rel="noopener">Apache Flink</a>: Stateful stream processing with event time and exactly-once.</li>
<li><a href="https://github.com/apache/hudi"target="_blank" rel="noopener">Apache Hudi</a>: Lake table format with upserts and incremental reads.</li>
<li><a href="https://github.com/apache/iceberg"target="_blank" rel="noopener">Apache Iceberg</a>: Open table format with ACID commits, schema evolution and time travel.</li>
<li><a href="https://github.com/apache/kafka"target="_blank" rel="noopener">Apache Kafka</a>: Distributed log for publishing and consuming event streams.</li>
<li><a href="https://github.com/apache/nifi"target="_blank" rel="noopener">Apache NiFi</a>: Flow-based routing and transformation with a visual editor.</li>
<li><a href="https://github.com/apache/orc"target="_blank" rel="noopener">Apache ORC</a>: Columnar file format built for large Hive-style reads.</li>
<li><a href="https://github.com/apache/paimon"target="_blank" rel="noopener">Apache Paimon</a>: Lake table format designed to be written continuously by Flink.</li>
<li><a href="https://github.com/apache/parquet-format"target="_blank" rel="noopener">Apache Parquet</a>: The columnar file format most of the lake is stored in.</li>
<li><a href="https://github.com/apache/polaris"target="_blank" rel="noopener">Apache Polaris</a>: Open catalog so several engines see the same Iceberg tables.</li>
<li><a href="https://github.com/apache/pulsar"target="_blank" rel="noopener">Apache Pulsar</a>: Messaging and streaming with storage separated from serving.</li>
<li><a href="https://github.com/apache/spark"target="_blank" rel="noopener">Apache Spark</a>: General engine for batch, SQL and ML over large datasets.</li>
<li><a href="https://github.com/apache/superset"target="_blank" rel="noopener">Apache Superset</a>: Web BI with dashboards and SQL exploration.</li>
<li><a href="https://github.com/argoproj/argo-workflows"target="_blank" rel="noopener">Argo Workflows</a>: Runs container-native workflows as Kubernetes objects.</li>
<li><a href="https://github.com/bentoml/BentoML"target="_blank" rel="noopener">BentoML</a>: Packages a model and its dependencies into a service.</li>
<li><a href="https://github.com/ClickHouse/ClickHouse"target="_blank" rel="noopener">ClickHouse</a>: Column store for fast analytical queries at scale.</li>
<li><a href="https://github.com/dagster-io/dagster"target="_blank" rel="noopener">Dagster</a>: Orchestrator built around data assets rather than tasks.</li>
<li><a href="https://github.com/dataform-co/dataform"target="_blank" rel="noopener">Dataform</a>: SQL transformation workflows with dependency management.</li>
<li><a href="https://github.com/datahub-project/datahub"target="_blank" rel="noopener">DataHub</a>: Metadata platform for catalog, lineage and ownership.</li>
<li><a href="https://github.com/dbt-labs/dbt-core"target="_blank" rel="noopener">dbt</a>: Turns SQL selects into tested, documented and versioned models.</li>
<li><a href="https://github.com/debezium/debezium"target="_blank" rel="noopener">Debezium</a>: Reads database change logs and publishes them as events.</li>
<li><a href="https://github.com/awslabs/deequ"target="_blank" rel="noopener">Deequ</a>: Data quality checks expressed as Spark jobs.</li>
<li><a href="https://github.com/delta-io/delta"target="_blank" rel="noopener">Delta Lake</a>: Transactional storage layer over Parquet files.</li>
<li><a href="https://github.com/dremio/dremio-oss"target="_blank" rel="noopener">Dremio</a>: Query engine and semantic layer over lake storage.</li>
<li><a href="https://github.com/duckdb/duckdb"target="_blank" rel="noopener">DuckDB</a>: Analytical database that runs inside your process, no server.</li>
<li><a href="https://github.com/feast-dev/feast"target="_blank" rel="noopener">Feast</a>: Feature store serving the same features to training and inference.</li>
<li><a href="https://github.com/flyteorg/flyte"target="_blank" rel="noopener">Flyte</a>: Typed, versioned workflows for data and ML on Kubernetes.</li>
<li><a href="https://github.com/grafana/grafana"target="_blank" rel="noopener">Grafana</a>: Dashboards over metrics, logs and traces.</li>
<li><a href="https://github.com/fivetran/great_expectations"target="_blank" rel="noopener">Great Expectations</a>: Declares expectations about data and validates them.</li>
<li><a href="https://github.com/apache/hive"target="_blank" rel="noopener">Hive Metastore</a>: The table and partition catalog many engines still read from.</li>
<li><a href="https://github.com/logicalclocks/hopsworks"target="_blank" rel="noopener">Hopsworks</a>: Feature store and ML platform.</li>
<li><a href="https://github.com/jupyter/notebook"target="_blank" rel="noopener">Jupyter</a>: Notebooks for interactive analysis.</li>
<li><a href="https://github.com/kserve/kserve"target="_blank" rel="noopener">KServe</a>: Serves models on Kubernetes with autoscaling and canaries.</li>
<li><a href="https://github.com/kubeflow/kubeflow"target="_blank" rel="noopener">Kubeflow</a>: ML platform assembled from Kubernetes components.</li>
<li><a href="https://github.com/kubeflow/pipelines"target="_blank" rel="noopener">Kubeflow Pipelines</a>: Composes and runs ML pipelines as containers.</li>
<li><a href="https://github.com/lightgbm-org/LightGBM"target="_blank" rel="noopener">LightGBM</a>: Gradient boosting tuned for speed on large tabular data.</li>
<li><a href="https://github.com/MarquezProject/marquez"target="_blank" rel="noopener">Marquez</a>: Reference server that collects and stores OpenLineage events.</li>
<li><a href="https://github.com/metabase/metabase"target="_blank" rel="noopener">Metabase</a>: BI that lets non-engineers ask questions of the database.</li>
<li><a href="https://github.com/Netflix/metaflow"target="_blank" rel="noopener">Metaflow</a>: Python workflows for data science, from Netflix.</li>
<li><a href="https://github.com/milvus-io/milvus"target="_blank" rel="noopener">Milvus</a>: Vector database for similarity search at scale.</li>
<li><a href="https://github.com/minio/minio"target="_blank" rel="noopener">MinIO</a>: S3-compatible object storage you run yourself.</li>
<li><a href="https://github.com/mlflow/mlflow"target="_blank" rel="noopener">MLflow</a>: Tracks experiments, packages models and holds the registry.</li>
<li><a href="https://github.com/numpy/numpy"target="_blank" rel="noopener">NumPy</a>: Arrays and numerical computing, the base of the Python stack.</li>
<li><a href="https://github.com/triton-inference-server/server"target="_blank" rel="noopener">NVIDIA Triton</a>: Inference server for models on GPU and CPU.</li>
<li><a href="https://github.com/OpenLineage/OpenLineage"target="_blank" rel="noopener">OpenLineage</a>: Open standard for pipelines to report what they read and wrote.</li>
<li><a href="https://github.com/open-metadata/OpenMetadata"target="_blank" rel="noopener">OpenMetadata</a>: Catalog, lineage, quality and governance in one place.</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-specification"target="_blank" rel="noopener">OpenTelemetry</a>: Vendor-neutral standard for traces, metrics and logs.</li>
<li><a href="https://github.com/pandas-dev/pandas"target="_blank" rel="noopener">Pandas</a>: DataFrames for data manipulation in Python.</li>
<li><a href="https://github.com/pgvector/pgvector"target="_blank" rel="noopener">pgvector</a>: Vector types and indexes inside PostgreSQL.</li>
<li><a href="https://github.com/pola-rs/polars"target="_blank" rel="noopener">Polars</a>: Fast DataFrames written in Rust, with a lazy query engine.</li>
<li><a href="https://github.com/PrefectHQ/prefect"target="_blank" rel="noopener">Prefect</a>: Orchestration for Python workflows, built for dynamic runs.</li>
<li><a href="https://github.com/prestodb/presto"target="_blank" rel="noopener">Presto</a>: Distributed SQL engine that queries data where it lives.</li>
<li><a href="https://github.com/projectnessie/nessie"target="_blank" rel="noopener">Project Nessie</a>: Git-like branches and commits for lake tables.</li>
<li><a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a>: Time series database and alerting for metrics.</li>
<li><a href="https://github.com/python/cpython"target="_blank" rel="noopener">Python</a>: The language most of this stack is written and driven in.</li>
<li><a href="https://github.com/pytorch/pytorch"target="_blank" rel="noopener">PyTorch</a>: Deep learning framework with dynamic graphs.</li>
<li><a href="https://github.com/qdrant/qdrant"target="_blank" rel="noopener">Qdrant</a>: Vector search engine with filtering, written in Rust.</li>
<li><a href="https://github.com/r-devel/r-svn"target="_blank" rel="noopener">R</a>: Language and environment for statistics.</li>
<li><a href="https://github.com/ray-project/ray"target="_blank" rel="noopener">Ray</a>: Distributed Python runtime for training, tuning and serving.</li>
<li><a href="https://github.com/redpanda-data/redpanda"target="_blank" rel="noopener">Redpanda</a>: Kafka-compatible broker with no JVM and no ZooKeeper.</li>
<li><a href="https://github.com/scikit-learn/scikit-learn"target="_blank" rel="noopener">Scikit-learn</a>: Classical machine learning algorithms in Python.</li>
<li><a href="https://github.com/scipy/scipy"target="_blank" rel="noopener">SciPy</a>: Scientific computing built on NumPy.</li>
<li><a href="https://github.com/SeldonIO/seldon-core"target="_blank" rel="noopener">Seldon</a>: Deploys and monitors models on Kubernetes.</li>
<li><a href="https://github.com/sodadata/soda-core"target="_blank" rel="noopener">Soda</a>: Data quality checks written as readable rules.</li>
<li><a href="https://github.com/SQLMesh/sqlmesh"target="_blank" rel="noopener">SQLMesh</a>: Transformations with column-level lineage and virtual environments.</li>
<li><a href="https://github.com/StarRocks/starrocks"target="_blank" rel="noopener">StarRocks</a>: MPP OLAP database for sub-second queries over the lake.</li>
<li><a href="https://github.com/tensorflow/tensorflow"target="_blank" rel="noopener">TensorFlow</a>: Deep learning framework with a production toolchain.</li>
<li><a href="https://github.com/trinodb/trino"target="_blank" rel="noopener">Trino</a>: Distributed SQL that federates queries across many sources.</li>
<li><a href="https://github.com/vllm-project/vllm"target="_blank" rel="noopener">vLLM</a>: High-throughput serving for large language models.</li>
<li><a href="https://github.com/weaviate/weaviate"target="_blank" rel="noopener">Weaviate</a>: Vector database with built-in embedding modules.</li>
<li><a href="https://github.com/dmlc/xgboost"target="_blank" rel="noopener">XGBoost</a>: Gradient boosting library, the workhorse for tabular data.</li>
</ul>
<p>Kafka Connect ships inside apache/kafka, dbt Tests inside dbt-core and Ray Serve inside ray-project/ray, so they share the repositories above.</p>
<p>No public repository, these are proprietary or managed services: Power BI, Tableau, Fivetran, Amazon S3, Google Cloud Storage, Azure Data Lake Storage, AWS Glue Catalog, Databricks, Starburst, Snowflake, BigQuery, Redshift, Tecton, Pinecone, Weights &amp; Biases, Neptune, Comet, Vertex AI, SageMaker and Azure ML.</p>
]]></content:encoded><category>data-engineering</category><category>machine-learning</category></item><item><title>Apache Flink: Learning in Public, Part 1</title><link>https://apolzek.github.io/en/2026/08/30/apache-flink-learning-in-public-part-1/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/08/30/apache-flink-learning-in-public-part-1/</guid><pubDate>Sun, 30 Aug 2026 12:00:00 GMT</pubDate><description>&lt;h2&gt;Index&lt;span class="hx:absolute hx:-mt-20" id="index"&gt;&lt;/span&gt;
&lt;a href="#index" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#index"&gt;Index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#introduction"&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#my-goal"&gt;My goal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-problem-flink-solves"&gt;The problem Flink solves&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#real-use-cases"&gt;Real use cases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-architecture-in-a-few-pieces"&gt;The architecture, in a few pieces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#sources-and-sinks"&gt;Sources and sinks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#parallelism"&gt;Parallelism&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#redistribution-and-data-skew"&gt;Redistribution and data skew&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#operator-chaining"&gt;Operator chaining&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#rich-functions"&gt;Rich functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#time-the-part-that-breaks-intuition"&gt;Time: the part that breaks intuition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#watermarks"&gt;Watermarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#windows"&gt;Windows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#processfunction"&gt;ProcessFunction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#state"&gt;State&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#checkpoints-and-savepoints"&gt;Checkpoints and savepoints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#flink-sql"&gt;Flink SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#in-praxi-in-practice"&gt;In praxi (&amp;ldquo;in practice&amp;rdquo;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#deployment"&gt;Deployment&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#execution-modes"&gt;Execution modes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#kubernetes"&gt;Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#memory"&gt;Memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#cpu-and-slots"&gt;CPU and slots&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#local-disk"&gt;Local disk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#s3-and-the-remote-file-system"&gt;S3 and the remote file system&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-ecosystem-around-it"&gt;The ecosystem around it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#complexities"&gt;Complexities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-stayed-with-me"&gt;What stayed with me&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Introduction&lt;span class="hx:absolute hx:-mt-20" id="introduction"&gt;&lt;/span&gt;
&lt;a href="#introduction" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;This text is the result of what I studied and experimented with over this week. It is not a tutorial by someone who has mastered Apache Flink, nor does it intend to be a reference on the subject. It is more like the notebook of someone who is learning, testing a few things in practice and sharing what they managed to understand and discover so far.&lt;/p&gt;</description><content:encoded><![CDATA[<h2>Index<span class="hx:absolute hx:-mt-20" id="index"></span>
    <a href="#index" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="#index">Index</a></li>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#my-goal">My goal</a></li>
<li><a href="#the-problem-flink-solves">The problem Flink solves</a></li>
<li><a href="#real-use-cases">Real use cases</a></li>
<li><a href="#the-architecture-in-a-few-pieces">The architecture, in a few pieces</a></li>
<li><a href="#sources-and-sinks">Sources and sinks</a></li>
<li><a href="#parallelism">Parallelism</a></li>
<li><a href="#redistribution-and-data-skew">Redistribution and data skew</a></li>
<li><a href="#operator-chaining">Operator chaining</a></li>
<li><a href="#rich-functions">Rich functions</a></li>
<li><a href="#time-the-part-that-breaks-intuition">Time: the part that breaks intuition</a></li>
<li><a href="#watermarks">Watermarks</a></li>
<li><a href="#windows">Windows</a></li>
<li><a href="#processfunction">ProcessFunction</a></li>
<li><a href="#state">State</a></li>
<li><a href="#checkpoints-and-savepoints">Checkpoints and savepoints</a></li>
<li><a href="#flink-sql">Flink SQL</a></li>
<li><a href="#in-praxi-in-practice">In praxi (&ldquo;in practice&rdquo;)</a></li>
<li><a href="#deployment">Deployment</a>
<ul>
<li><a href="#execution-modes">Execution modes</a></li>
<li><a href="#kubernetes">Kubernetes</a></li>
<li><a href="#memory">Memory</a></li>
<li><a href="#cpu-and-slots">CPU and slots</a></li>
<li><a href="#local-disk">Local disk</a></li>
<li><a href="#s3-and-the-remote-file-system">S3 and the remote file system</a></li>
</ul>
</li>
<li><a href="#the-ecosystem-around-it">The ecosystem around it</a></li>
<li><a href="#complexities">Complexities</a></li>
<li><a href="#what-stayed-with-me">What stayed with me</a></li>
</ul>
<h2>Introduction<span class="hx:absolute hx:-mt-20" id="introduction"></span>
    <a href="#introduction" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This text is the result of what I studied and experimented with over this week. It is not a tutorial by someone who has mastered Apache Flink, nor does it intend to be a reference on the subject. It is more like the notebook of someone who is learning, testing a few things in practice and sharing what they managed to understand and discover so far.</p>
<p>I have always believed that explaining is the best way to find out what we really understood. When I write, the holes show up on their own..</p>
<p>Apache Flink is interesting mainly because it treated <strong>streaming</strong> as the general case, and not as a patch on top of batch processing. While its competitors at the time simulated continuous flow with micro-batches, Flink processes record by record, with low latency and high throughput, keeping distributed state consistent through asynchronous snapshots (a variation of the <em>Chandy-Lamport</em> algorithm) that guarantee exactly-once semantics even after failures. It also offers a notion of event time with <em>watermarks</em>, which allows it to correctly handle data that arrives out of order or late, something essential in the real world. Its origin is academic: it was born in 2010 as Stratosphere, a research project at TU Berlin in partnership with Humboldt University and the Hasso Plattner Institute, led by Volker Markl. In 2014 the code was donated to the Apache Software Foundation, renamed Flink (&ldquo;nimble&rdquo; in German, hence the squirrel in the logo) and promoted to a top-level project in December of that same year. The creators founded <a href="https://github.com/dataArtisans"target="_blank" rel="noopener">data Artisans</a> to commercialize it, a company bought by Alibaba in 2019 and renamed Ververica. Alibaba itself already maintained an internal fork (Blink) to handle the volume of Singles&rsquo; Day, and a good part of those improvements went back into the main project, helping consolidate Flink as the de facto standard for stream processing at companies like Netflix, Uber and Stripe.</p>
<h2>My goal<span class="hx:absolute hx:-mt-20" id="my-goal"></span>
    <a href="#my-goal" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>My goal is to use Apache Flink connected to Kafka to process OpenTelemetry OTLP data (traces, metrics and logs) and extract more intelligence from that telemetry in real time, instead of just storing it and querying it later. The idea is to use this pipeline as a lab to test cases such as anomaly detection in time series of metrics and latencies, alert generation with context and noise suppression, root cause analysis (RCA) correlating spans, logs and metrics of affected services, prediction of degradation and resource saturation before the incident happens, and detection of sensitive data improperly flowing through span attributes and log messages. Beyond that, I want to explore other scenarios where Flink fits well: enrichment and smart sampling of traces, deciding with state which traces to keep based on what happened in the entire request (tail-based sampling) instead of discarding randomly at the edge; continuous calculation of SLIs and error budgets per service, with sliding windows and aggregations by endpoint, team and deploy version; and detection of behavior changes after releases, comparing the traffic, error and latency profile between versions to identify regressions introduced by a specific deploy. I intend to detail all of this soon (:</p>
<h2>The problem Flink solves<span class="hx:absolute hx:-mt-20" id="the-problem-flink-solves"></span>
    <a href="#the-problem-flink-solves" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For a long time the data world was organized around the <em>batch</em>. You accumulate data during the day, run a job at midnight, and in the morning the report is ready. That works, and keeps working for a lot of things. The problem is when the answer needs to come earlier. Detecting a card fraud ten hours after the purchase is the same as not detecting it. An infrastructure alert that arrives the next day is a post-mortem, not an alert. In those cases, you do not want to ask the data from time to time, you want the data to answer continuously. That is the inversion that stream processing proposes:</p>
<ul>
<li><strong>Batch</strong>: the query is permanent, the data is finite. You run the query against a set that is standing still.</li>
<li><strong>Streaming</strong>: the data is infinite, the query is what stands still. It is registered once and reacts to every event that passes.</li>
</ul>
<p>Apache Flink is a distributed processing engine aimed at building that second kind of application. Its founding premise: everything is a stream. Yesterday&rsquo;s CSV file is a stream that has a beginning and an end (<em>bounded</em>). The Kafka topic with clicks from your site is a stream that never ends (<em>unbounded</em>). One of the points that caught my attention most during the study was the unified processing model: in Flink, batch can be understood as a particular case of streaming, in which the data flow has a defined beginning and end. This means the execution model does not depend on two completely distinct engines, but on a single architecture capable of processing both continuous flows and finite datasets.</p>
<h2>Real use cases<span class="hx:absolute hx:-mt-20" id="real-use-cases"></span>
    <a href="#real-use-cases" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>It is worth looking at where this shows up in practice, because the list helps understand what kind of problem Flink solves well:</p>
<ul>
<li>Fraud detection, in banks and fintechs. Every transaction is evaluated in milliseconds against the customer&rsquo;s history, kept as state per key (<code>keyBy</code> on the customer id). Rules like &ldquo;3 purchases in different cities within 10 minutes&rdquo; are expressed with windows, timers and the complex event pattern detection library (CEP).</li>
<li>CDC pipelines to a data lake. Flink CDC follows the database transaction log (the binlog in MySQL, the WAL via logical replication in Postgres) and replicates every insert, update and delete in real time to table formats like Apache Iceberg, Paimon or Hudi. Stripe is one of the public cases of change capture at scale with Flink.</li>
<li>Real-time analytics and dashboards. Windowed aggregations (orders per minute, revenue per region) feeding live dashboards. Uber, Netflix and Alibaba operate thousands of Flink jobs with this profile.</li>
<li>Event-driven applications. Here the business logic lives inside the stream, and not in a service that queries a database: ride matching, dynamic pricing, alerting engines, feature store updates for machine learning models.</li>
<li>AI over streams. Model inference applied to events as they pass, and Flink Agents, a new subproject aimed at AI agents that react to events in real time.</li>
</ul>
<h2>The architecture, in a few pieces<span class="hx:absolute hx:-mt-20" id="the-architecture-in-a-few-pieces"></span>
    <a href="#the-architecture-in-a-few-pieces" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th style="text-align: left">Piece</th>
          <th style="text-align: left">What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>JobManager</strong></td>
          <td style="text-align: left">The coordinator. Receives the job, turns the logical graph into an execution graph, distributes the tasks, triggers checkpoints and coordinates failure recovery.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>TaskManager</strong></td>
          <td style="text-align: left">The worker. It is a JVM process that actually executes the tasks and holds local state.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Task Slot</strong></td>
          <td style="text-align: left">The unit of parallelism inside a TaskManager. A TaskManager with 4 slots can run 4 slices of work in parallel.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Client</strong></td>
          <td style="text-align: left">Whoever packages the job and submits it to the JobManager. After that, it can go away.</td>
      </tr>
  </tbody>
</table>
<p>A job is represented as an <strong>operator graph</strong>, composed of sources (<code>source</code>), transformations (<code>map</code>, <code>filter</code>, <code>keyBy</code>, <code>window</code>) and sinks (<code>sink</code>). From that graph, Flink determines the parallelism of each operator and distributes its instances among the slots available in the cluster.</p>
<p><code>keyBy</code> deserves special attention because it is responsible for partitioning the data. When you define <code>keyBy(event -&gt; event.getUserId())</code>, you are determining that events with the same key are routed to the same parallel instance of the next operator. This partitioning is fundamental for keyed stateful processing, because it allows each instance to keep and manage the state associated with the keys under its responsibility, without the need for distributed coordination on every event.</p>
<h2>Sources and sinks<span class="hx:absolute hx:-mt-20" id="sources-and-sinks"></span>
    <a href="#sources-and-sinks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Source and sink are the two points where the job touches the world. In the 2.x line they go through a single API, <code>env.fromSource(...)</code> on one side and <code>stream.sinkTo(...)</code> on the other.</p>
<p>The three sources that appear in almost every project:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Source</th>
          <th style="text-align: left">What it is for</th>
          <th style="text-align: left">What it keeps in state</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>KafkaSource</strong></td>
          <td style="text-align: left">The common case. Reads one or more topics, with a consumer group.</td>
          <td style="text-align: left">The offset of each partition</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Flink CDC</strong></td>
          <td style="text-align: left">Reads a database transaction log and turns every row change into an event.</td>
          <td style="text-align: left">The position in the binlog or in the WAL</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>FileSource</strong></td>
          <td style="text-align: left">Reads files from a bucket or from HDFS, in batch or watching the directory.</td>
          <td style="text-align: left">Which files and which stretch of each one has already been read</td>
      </tr>
  </tbody>
</table>
<p>And the three sinks:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Sink</th>
          <th style="text-align: left">What it is for</th>
          <th style="text-align: left">Guarantee it offers</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>KafkaSink</strong></td>
          <td style="text-align: left">Publishes the result to another topic, for the next job or for whoever consumes it.</td>
          <td style="text-align: left">Exactly-once, via Kafka transactions</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>JdbcSink</strong></td>
          <td style="text-align: left">Writes to Postgres, MySQL and the like, typically the aggregated result.</td>
          <td style="text-align: left">Idempotence, when there is a primary key</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>FileSink</strong></td>
          <td style="text-align: left">Writes partitioned Parquet or JSON to object storage.</td>
          <td style="text-align: left">Exactly-once, committing the files at the checkpoint</td>
      </tr>
  </tbody>
</table>
<p>The shape of the code is always the same, a builder on one side and another on the other:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">KafkaSource</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="n">source</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">KafkaSource</span><span class="p">.</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="n">builder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setBootstrapServers</span><span class="p">(</span><span class="s">&#34;kafka:9092&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setTopics</span><span class="p">(</span><span class="s">&#34;otlp-spans&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setGroupId</span><span class="p">(</span><span class="s">&#34;flink-otlp&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setStartingOffsets</span><span class="p">(</span><span class="n">OffsetsInitializer</span><span class="p">.</span><span class="na">earliest</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setValueOnlyDeserializer</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">SimpleStringSchema</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">KafkaSink</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="n">sink</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">KafkaSink</span><span class="p">.</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="n">builder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setBootstrapServers</span><span class="p">(</span><span class="s">&#34;kafka:9092&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setRecordSerializer</span><span class="p">(</span><span class="n">KafkaRecordSerializationSchema</span><span class="p">.</span><span class="na">builder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="na">setTopic</span><span class="p">(</span><span class="s">&#34;otlp-alerts&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="na">setValueSerializationSchema</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">SimpleStringSchema</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="na">build</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setDeliveryGuarantee</span><span class="p">(</span><span class="n">DeliveryGuarantee</span><span class="p">.</span><span class="na">EXACTLY_ONCE</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setTransactionalIdPrefix</span><span class="p">(</span><span class="s">&#34;otlp-&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">env</span><span class="p">.</span><span class="na">fromSource</span><span class="p">(</span><span class="n">source</span><span class="p">,</span><span class="w"> </span><span class="n">watermarkStrategy</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;otlp-spans&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span><span class="p">.</span><span class="na">process</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">AnomalyDetector</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span><span class="p">.</span><span class="na">sinkTo</span><span class="p">(</span><span class="n">sink</span><span class="p">);</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Two things in this code are not syntax details. The first is that the source does not commit offsets on its own: the <strong>offset is Flink state</strong>, it goes into the checkpoint together with everything else and comes back with it on recovery. That is why you do not manage offsets by hand here, and it is also why Kafka&rsquo;s consumer lag dashboard may disagree with the job&rsquo;s reality, since the commit to Kafka is only informational.</p>
<p>The second is that exactly-once at the sink is a property of the sink, not of Flink. <code>KafkaSink</code> only achieves it because it writes inside a transaction that is committed when the checkpoint closes, and that is why it requires a <code>transactionalIdPrefix</code> that is stable across restarts. <code>FileSink</code> works by the same logic: the files keep an &ldquo;in progress&rdquo; suffix until the checkpoint commits, which tends to scare anyone looking at the bucket midway and thinking nothing is being written. <code>JdbcSink</code>, on the other hand, has no distributed transaction, so the guarantee it offers depends on you writing with a primary key and letting repetition be harmless.</p>
<p>It is worth insisting on the versioning part, because it bites. Except for the file system connectors and <code>datagen</code>, which ship inside the distribution, every connector is a separate project, with its own repository, its own release and numbering that has no relation whatsoever to Flink&rsquo;s. <code>flink-connector-kafka</code> is at 5.0.0 while Flink is at 2.3. The two numbers do not talk to each other, and there is no &ldquo;the connector version that matches mine&rdquo; by deduction.</p>
<p>What exists is a compatibility matrix, published on the downloads page, and checking that matrix is a mandatory step before choosing the Flink version for a new project. The reason is that the migration to the 2.x line was not simultaneous, and a good part of the ecosystem was left behind. A few examples of how this looked when I wrote this:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Connector</th>
          <th style="text-align: left">Version</th>
          <th style="text-align: left">Compatible Flink</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Kafka</td>
          <td style="text-align: left">5.0.0</td>
          <td style="text-align: left">2.1.x and 2.2.x</td>
      </tr>
      <tr>
          <td style="text-align: left">JDBC</td>
          <td style="text-align: left">4.1.0</td>
          <td style="text-align: left">2.1.x and 2.2.x</td>
      </tr>
      <tr>
          <td style="text-align: left">Elasticsearch</td>
          <td style="text-align: left">4.0.0</td>
          <td style="text-align: left">2.0.x</td>
      </tr>
      <tr>
          <td style="text-align: left">Prometheus</td>
          <td style="text-align: left">1.0.0</td>
          <td style="text-align: left">1.19.x and 1.20.x</td>
      </tr>
  </tbody>
</table>
<p>The Prometheus connector is the most eloquent case, and one that interests me closely, because it is the natural output for metrics derived from telemetry. It exists, it is official, and it writes through Prometheus&rsquo;s remote write API. It is sink only, there is no corresponding source, which makes sense: Prometheus is where the time series ends, not where it comes from. Except that 1.0.0 is still from the Flink 1.x line and has not crossed over to 2.x. Anyone who wants that sink today chooses between staying on 1.20, which is LTS, or writing the integration by hand.</p>
<p>And notice that not even Kafka lists 2.3 in the table above. A one-release lag between the core and the connectors is the normal state of things, not an exception. If your pipeline depends on a specific connector, it is the connector that decides which Flink version you can run, and not the other way around.</p>
<h2>Parallelism<span class="hx:absolute hx:-mt-20" id="parallelism"></span>
    <a href="#parallelism" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Parallelism is the number of instances of each operator running at the same time. A job with parallelism 8 has eight copies of the source, eight of the map and eight of the sink, each processing a slice of the data and occupying a slot. Each of those copies is a subtask, which is why the web interface shows repeated metrics with indices from 0 to 7.</p>
<p>It can be set at three levels, from the broadest to the most specific. For the whole job, with <code>env.setParallelism(8)</code> in the code or <code>parallelism.default</code> in <code>config.yaml</code>. At deploy time, through the <code>parallelism</code> field of the <code>FlinkDeployment</code>, which is what appears in the manifest further down. And operator by operator, chaining <code>.setParallelism(2)</code> onto the call, which overrides the previous ones. The typical case for the override is a sink that talks to a database that cannot handle eighty simultaneous connections: the rest of the job stays wide and only the write is throttled on purpose.</p>
<p>For the initial value, a reasonable starting point is matching the parallelism to the number of partitions of the input Kafka topic. The reason not to go above that is specific and worth understanding, because the symptom is confusing: the leftover source subtasks receive no partition, sit idle and never emit a watermark. Since an operator&rsquo;s watermark is the minimum across all its inputs, a stalled source holds back the clock of the whole job, and the result is a window that never closes and a job that looks stuck without a single error in the log. It can be worked around by declaring <code>withIdleness()</code> in the watermark strategy, which tells Flink to ignore the silent inputs, BUT IT IS SIMPLER NOT TO CREATE THE PROBLEM. Staying below the number of partitions also has a cost, a smaller one: you leave topic throughput on the table, because one subtask ends up reading more than one partition.</p>
<p>After that, tuning is by measurement, not by arithmetic. The two numbers that matter in the web interface are <em>backpressure</em>, which shows which operator is holding back the flow, and <em>busy time</em>, which shows the fraction of time each subtask was actually working instead of waiting. Raising the parallelism of an operator that spends the day idle improves nothing. I say more about this sizing in the CPU and slots section.</p>
<p>One piece is missing for rescaling to make sense, and it is internal: Flink does not distribute key by key. It runs each key through a hash and places it into a <strong>key group</strong>, and what gets distributed among the subtasks are the key groups, in contiguous ranges. The key group is the unit of state movement, that is, when you change the parallelism, what migrates from one machine to another are whole groups, never individual keys. You almost never touch this directly, but the term shows up in logs and in any conversation about rescaling.</p>
<p>The number of key groups that exist is exactly the <code>maxParallelism</code>. From there come the two properties that matter. It is the absolute ceiling of parallelism, because you cannot have more subtasks than groups to distribute among them. And it cannot change afterwards, because changing the number of groups changes which group each key belongs to, which invalidates all the state already saved.</p>
<p>The trap is in the default value. Without configuration, Flink computes that number from the initial parallelism: something around one and a half times it, rounded up, with a floor of 128 and a ceiling of 32768. A job that starts with parallelism 8 gets 128 and nobody notices, because nothing breaks. The problem arrives on the day that job needs parallelism 200, and the only way out is rebuilding the state from scratch.</p>
<p>Setting it by hand from the first run solves it, and <code>env.setMaxParallelism(1024)</code> is a comfortable value. What does not work is putting 32768 as a precaution: some state backends keep internal structures that grow with the number of key groups, so overdoing it costs performance. An extra care that comes for free is choosing a value divisible by the intended parallelism, because then the groups spread evenly. With 128 groups and parallelism 12, some subtasks get 11 and others 10, a small but avoidable asymmetry.</p>
<h2>Redistribution and data skew<span class="hx:absolute hx:-mt-20" id="redistribution-and-data-skew"></span>
    <a href="#redistribution-and-data-skew" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Between two operators there is always a decision about which next subtask each event goes to. Most of the time it is made by Flink without you asking, but you can choose, and the options are few:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Strategy</th>
          <th style="text-align: left">What it does</th>
          <th style="text-align: left">When it appears</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><code>forward</code></td>
          <td style="text-align: left">The event stays in the same subtask. Nothing crosses the network</td>
          <td style="text-align: left">Between adjacent operators of the same parallelism. It is what allows chaining</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>hash</code></td>
          <td style="text-align: left">Routes by key, always the same key to the same subtask</td>
          <td style="text-align: left">Every <code>keyBy</code>, that is, whenever there is keyed state</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>rebalance</code></td>
          <td style="text-align: left">Round-robin across all following subtasks</td>
          <td style="text-align: left">To rebalance after a source that arrives unbalanced</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>rescale</code></td>
          <td style="text-align: left">Round-robin, but only among neighboring subtasks, preserving locality</td>
          <td style="text-align: left">Same goal as <code>rebalance</code>, avoiding traffic between machines</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>broadcast</code></td>
          <td style="text-align: left">Every event goes to all following subtasks</td>
          <td style="text-align: left">Distributing rules or configuration to all instances</td>
      </tr>
  </tbody>
</table>
<p><code>broadcast</code> deserves a note because it is the basis of an entire pattern: a small stream of rules is broadcast to all subtasks, which keep it in state, while the big stream of events passes partitioned by key. That is how you change a job&rsquo;s logic without a redeploy.</p>
<p>It is also worth remembering that <code>rebalance</code> and <code>rescale</code> gained adaptive distribution in 2.3, looking at the real load of the destination instead of doing blind round-robin, and it is an option that needs to be switched on, as I describe in the post about Flink 2.3.</p>
<p>None of this solves the problem that hurts the most, which is <strong>data skew</strong>. Skew is when the distribution of keys is uneven: one customer who alone accounts for half the transactions, a product that went viral, a giant tenant in the middle of a thousand small ones. Since <code>keyBy</code> guarantees that the same key always goes to the same subtask, that subtask becomes the bottleneck, the others sit idle, and the whole job moves at the speed of the slowest one. Increasing parallelism does not help at all, because the heavy key keeps going to a single place.</p>
<p>The diagnosis is straightforward in the web interface, and that is why busy time matters so much: skew shows up as backpressure concentrated in one subtask and as one instance at 100% occupancy while its neighbors sit at 10%. If all of them were at 90%, the problem would be capacity, and the answer would be different.</p>
<p>There are three ways out, in order of effort. The first, and the most used, is aggregating in two phases: you append a random suffix to the key, something like <code>customer-42#0</code> through <code>customer-42#9</code>, aggregate partially over that inflated key, and only then aggregate again over the real key. The heavy work gets split across ten subtasks, and the second stage receives only ten partial results per key instead of millions of events. The second, when you are in SQL, is letting the engine do it: enabling mini-batch and the two-phase aggregation optimization solves the common case without rewriting a single query, and there is even a specific option to split <code>DISTINCT</code> aggregations, which is the automatic version of the same trick. The third is accepting that the whale key is a case apart and handling it outside the normal flow, with a dedicated pipeline (a classic).</p>
<h2>Operator chaining<span class="hx:absolute hx:-mt-20" id="operator-chaining"></span>
    <a href="#operator-chaining" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The graph you write is not exactly the graph that runs. Before executing, Flink merges adjacent operators into a single task, and that is called <em>operator chaining</em>. A sequence like <code>map</code>, <code>filter</code> and <code>flatMap</code> becomes a single thing, executed in the same thread.</p>
<p>The merge only happens under three conditions: the link between the operators needs to be <code>forward</code>, they need to have the same parallelism and they need to be in the same slot sharing group. A <code>keyBy</code> or a <code>rebalance</code> breaks the chain, because in those cases the record really does need to leave one instance and go to another.</p>
<p>The gain is bigger than it looks at first glance. Inside a chain, passing a record from one operator to the next is a method call, and the object stays the same in memory. Between two tasks, the record is serialized, goes into a network buffer and is deserialized on the other side, and that happens even when both are inside the same TaskManager. Chaining eliminates that whole cost.</p>
<p>It is also the explanation for a common confusion in the web interface: the boxes in the graph do not correspond to your operators, they correspond to the chains. You write six operators and see two boxes, each listing inside it the names it absorbed.</p>
<p>The trap appears when investigating a problem. Metrics and backpressure are measured per task, not per operator, so an entire chain reports a single number, and you cannot tell which of the merged operators is holding back the flow. To find out, you can break the chain on purpose: <code>.disableChaining()</code> isolates an operator on both sides, and <code>.startNewChain()</code> starts a new chain from it. This is a diagnostic tool, not a setting to leave on. The serialization cost you reintroduced is real, so it is worth reverting after measuring.</p>
<h2>Rich functions<span class="hx:absolute hx:-mt-20" id="rich-functions"></span>
    <a href="#rich-functions" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Every function interface has a &ldquo;rich&rdquo; version: <code>MapFunction</code> has <code>RichMapFunction</code>, <code>FilterFunction</code> has <code>RichFilterFunction</code>, and so on. The difference is that the rich version adds a lifecycle and context, and that is what separates a function that merely transforms a record from a function that needs external resources or memory.</p>
<p><code>open()</code> is called once per parallel instance, before the first event arrives. It is the place to open a connection to a database, load an enrichment cache, compile a regular expression and, above all, register the state descriptors. Note the &ldquo;per parallel instance&rdquo;: with parallelism 8 it runs eight times, once in each subtask, and what it creates is not shared among them. <code>close()</code> is its counterpart, called on shutdown, and serves to return what was opened.</p>
<p><code>getRuntimeContext()</code> is the entry point to what the runtime knows. Through it you reach keyed state, the custom metrics you want to expose, the index of the current subtask and the operator&rsquo;s parallelism.</p>
<p>If this sounds familiar when you get to the state section, it is because the example there already uses both: <code>AnomalyDetector</code> overrides <code>open()</code> and calls <code>getRuntimeContext().getState(...)</code> to create the <code>ValueState</code>. It does not have &ldquo;Rich&rdquo; in its name, but <code>KeyedProcessFunction</code> extends <code>AbstractRichFunction</code> and inherits the same lifecycle. Process functions come rich out of the box.</p>
<p>A version note that saves time: up to 1.x the signature was <code>open(Configuration parameters)</code>. In 2.x it was removed, and the method now receives an <code>OpenContext</code>. An example copied from an old tutorial does not compile because of this.</p>
<h2>Time: the part that breaks intuition<span class="hx:absolute hx:-mt-20" id="time-the-part-that-breaks-intuition"></span>
    <a href="#time-the-part-that-breaks-intuition" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This was the section that most made me stop and reread. In streaming there are, at a minimum, two different times:</p>
<ul>
<li><strong>Event time:</strong> when the event actually happened. It is recorded inside the event itself.</li>
<li><strong>Processing time:</strong> when Flink saw the event. It is the machine&rsquo;s clock.</li>
</ul>
<p>It seems like a detail, but it is not. Someone&rsquo;s phone lost signal in the subway and synced twenty minutes later. A broker had a latency spike. One Kafka partition fell behind the others. In all those cases, events arrive <strong>out of order</strong> and <strong>late</strong>.</p>
<p>If you group by processing time, the result depends on when the job ran. Reprocessing the same day tomorrow gives a different number. If you group by event time, the result is always the same, regardless of when the processing happened. That reproducibility is the reason event time is the default in almost every serious pipeline.</p>
<p>But that creates a new problem: if events arrive out of order, how do you know when a time window can be closed? No event ever arrives saying &ldquo;that&rsquo;s all, you can count now&rdquo;.</p>
<h2>Watermarks<span class="hx:absolute hx:-mt-20" id="watermarks"></span>
    <a href="#watermarks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Flink&rsquo;s answer is <strong>watermarks</strong>. A watermark is a marker that travels along with the stream carrying a claim:</p>
<blockquote>
  <p>&ldquo;I believe I will no longer receive events with a timestamp earlier than T.&rdquo;</p>

</blockquote>
<p>It is a bet, not a certainty. And it is precisely because it is a bet that it is useful: it turns an impossible question (&ldquo;has everything arrived?&rdquo;) into a configurable decision (&ldquo;how much delay do I tolerate?&rdquo;).</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">WatermarkStrategy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="o">&lt;</span><span class="n">Event</span><span class="o">&gt;</span><span class="n">forBoundedOutOfOrderness</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">10</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">withTimestampAssigner</span><span class="p">((</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">event</span><span class="p">.</span><span class="na">getTimestamp</span><span class="p">());</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This code says: tolerate up to 10 seconds of disorder. If the largest timestamp seen so far is <code>12:00:30</code>, the watermark is <code>12:00:20</code>, and any window ending before that can be closed and emitted.</p>
<p>The trade-off shows up immediately:</p>
<ul>
<li><strong>Aggressive</strong> watermark (little delay tolerated) → fast result, more events lost for arriving late.</li>
<li><strong>Conservative</strong> watermark (a lot of delay tolerated) → correct result, but you wait longer to see anything.</li>
</ul>
<p>There is no right value. There is the value that matches your business, and that is a product decision disguised as a technical setting.</p>
<p>For events that arrive after the watermark has advanced, Flink offers specific mechanisms for handling late data. <code>allowedLateness</code> keeps the window open for an additional period, allowing late events to be processed and already-produced results to be updated. Another alternative is to use a <strong>side output</strong>, routing the events that exceeded the lateness limit to a separate stream. That mechanism can be useful for auditing, data quality metrics or for identifying problems in the watermark strategy configuration.</p>
<h2>Windows<span class="hx:absolute hx:-mt-20" id="windows"></span>
    <a href="#windows" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Since the stream is infinite, aggregations only make sense within slices.</p>
<ul>
<li><strong>Tumbling:</strong> fixed windows that do not overlap. &ldquo;Every 5 minutes, how many orders?&rdquo;</li>
<li><strong>Sliding:</strong> fixed windows that do overlap. &ldquo;In the last 10 minutes, updating every 1 minute.&rdquo; The same event falls into several windows.</li>
<li><strong>Session:</strong> windows defined by inactivity. &ldquo;Group a user&rsquo;s activity until they stay idle for 30 minutes.&rdquo; The window size is given by the data, not by you.</li>
</ul>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">stream</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">keyBy</span><span class="p">(</span><span class="n">Event</span><span class="p">::</span><span class="n">getUserId</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">window</span><span class="p">(</span><span class="n">TumblingEventTimeWindows</span><span class="p">.</span><span class="na">of</span><span class="p">(</span><span class="n">Time</span><span class="p">.</span><span class="na">minutes</span><span class="p">(</span><span class="n">5</span><span class="p">)))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">aggregate</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">EventCounter</span><span class="p">());</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A detail I had not understood right away: prefer <code>reduce</code>/<code>aggregate</code> over <code>process</code> whenever you can. <code>process</code> receives all the events of the window at once, which means keeping all of them in state until it closes. <code>aggregate</code> keeps only the accumulator. In large windows, that difference is the difference between a stable job and a job that dies of OOM.</p>
<h2>ProcessFunction<span class="hx:absolute hx:-mt-20" id="processfunction"></span>
    <a href="#processfunction" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The ready-made operators cover a lot, but they impose a shape. <code>map</code> turns one event into another, <code>window</code> groups by a time slice, <code>aggregate</code> accumulates. When the logic does not fit any of those molds, the path is to go down a level and write a <code>ProcessFunction</code>, which is the lowest-level and most powerful operator in the DataStream API. It is Flink&rsquo;s Swiss army knife, and it is no exaggeration to say that almost all truly complicated business logic ends up inside one.</p>
<p>What it gives you, on every event, are three things the ready-made operators do not give together: direct access to the key&rsquo;s state, the ability to register timers and the ability to emit to side outputs. All three arrive through the same place, that <code>ctx</code> parameter in the <code>processElement</code> signature.</p>
<p>The timer is the part that changes what can be built, because it is what allows reacting to the passage of time, and not only to the arrival of an event. You register a future instant and Flink calls your <code>onTimer</code> when it arrives:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Override</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">processElement</span><span class="p">(</span><span class="n">Event</span><span class="w"> </span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">Context</span><span class="w"> </span><span class="n">ctx</span><span class="p">,</span><span class="w"> </span><span class="n">Collector</span><span class="o">&lt;</span><span class="n">Alert</span><span class="o">&gt;</span><span class="w"> </span><span class="n">out</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ctx</span><span class="p">.</span><span class="na">timerService</span><span class="p">().</span><span class="na">registerEventTimeTimer</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">getTimestamp</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">600_000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nd">@Override</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">onTimer</span><span class="p">(</span><span class="kt">long</span><span class="w"> </span><span class="n">timestamp</span><span class="p">,</span><span class="w"> </span><span class="n">OnTimerContext</span><span class="w"> </span><span class="n">ctx</span><span class="p">,</span><span class="w"> </span><span class="n">Collector</span><span class="o">&lt;</span><span class="n">Alert</span><span class="o">&gt;</span><span class="w"> </span><span class="n">out</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// ten minutes of event time have passed since that event</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Two properties of this mechanism matter. The first is that an event time timer does not fire when the machine&rsquo;s clock gets there, but when the watermark passes that instant, which ties timers to the same notion of time as windows: if the watermark does not advance, the timer does not fire, and we are back to the idle source problem I described earlier. The second is that timers are state, they belong to a key and go into the checkpoint. A job that restarts does not lose its pending timers, and that is what makes it viable to write &ldquo;if nothing happens in the next thirty minutes, alert&rdquo; and trust the sentence.</p>
<p>The side output is the second door. It already appeared here as the destination for late events, but it serves any fork: records that failed validation, events that triggered a rule, samples for auditing. You declare an <code>OutputTag</code>, call <code>ctx.output(tag, value)</code> and retrieve that flow later with <code>getSideOutput(tag)</code>. The advantage over a duplicated <code>filter</code> is that the event is examined only once, and the advantage over throwing an exception is that the job does not die because of a malformed record.</p>
<p>There are a few variants, and choosing the wrong one is a common mistake. <code>KeyedProcessFunction</code> is the one that comes after a <code>keyBy</code> and the only one with keyed state and timers. The plain <code>ProcessFunction</code> works over a non-partitioned stream and has neither. <code>CoProcessFunction</code> receives two streams and is the path to join a flow of events with a flow of rules or reference data. And <code>ProcessWindowFunction</code> is the one that runs when a window closes, with all of its events in hand.</p>
<p>The price of going down to that level is that Flink stops taking care of things for you. A window knows when to end and throws away what it accumulated; a state you created inside a <code>ProcessFunction</code> stays there forever, for every key that ever appeared, until you explicitly delete it or configure TTL. It is the same freedom and the same risk of any low-level abstraction, and it is the reason the recommendation remains to use the ready-made operator whenever it fits.</p>
<h2>State<span class="hx:absolute hx:-mt-20" id="state"></span>
    <a href="#state" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I started studying Flink by writing, before it, my own Kafka consumer. The idea was to answer a single question: <em>what does a distributed processing framework solve beyond reading events from a topic?</em></p>
<p>The answer came quickly, and it is a list. State management, parallelism, partitioning, fault tolerance, recovery after a crash, event time, watermarks, coordination between processing stages. In the manual consumer, every one of those items becomes your code. In Flink, almost all of them are part of the runtime, and what is left for the application is the processing logic.</p>
<p>Any more interesting stream processing needs to keep some kind of memory, whether to count events, deduplicate, detect patterns, correlate data or compare previous values. In a traditional consumer, that responsibility ends up much closer to the application. In Flink, <strong>state is managed by the runtime and can be distributed, partitioned by key, stored persistently and recovered after a failure through the checkpoint and recovery mechanisms</strong>. That changes quite a lot how you build a streaming application, especially when we start thinking about scale, failures and continuous processing.</p>
<p>Flink offers state as a first-class primitive, with types like <code>ValueState</code>, <code>ListState</code> and <code>MapState</code>, always associated with a key after the <code>keyBy</code>.</p>
<p><code>What the hell is a first-class primitive??</code></p>
<p>It comes from programming language theory. Something is first-class when the system treats it as a full citizen, with all rights, instead of an appendix you have to manage from the outside.. it means you do not need a database. Here is the part that really matters. The obvious alternative would be to keep that accumulation in an external Redis or Postgres. Flink does it differently: the state lives inside the operator itself, in the TaskManager process, managed by the framework. That brings three consequences.</p>
<p>First, it is local. Reading state is a memory access or an access to a RocksDB on the machine&rsquo;s local disk, not a network call. That is the difference between microseconds and milliseconds per event, and the reason Flink can process millions of events per second with logic that depends on history.</p>
<p>Second, it goes into checkpoints. Periodically Flink takes a consistent snapshot of all the state of all operators and writes it to durable storage (S3, HDFS). If a TaskManager dies, the job restarts from the last checkpoint with the state exactly as it was, and the Kafka offsets come back with it. That is what sustains the exactly-once semantics we mentioned earlier. With an external Redis, you would have to solve that consistency between &ldquo;what I have already consumed from Kafka&rdquo; and &ldquo;what I have already written to Redis&rdquo; by hand, and it is notoriously hard.</p>
<p>Third, it is resizable. When you increase the job&rsquo;s parallelism, Flink redistributes the state among the new instances automatically, from a savepoint.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">AnomalyDetector</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">KeyedProcessFunction</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">Event</span><span class="p">,</span><span class="w"> </span><span class="n">Alert</span><span class="o">&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kd">transient</span><span class="w"> </span><span class="n">ValueState</span><span class="o">&lt;</span><span class="n">Double</span><span class="o">&gt;</span><span class="w"> </span><span class="n">lastValue</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Override</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">open</span><span class="p">(</span><span class="n">OpenContext</span><span class="w"> </span><span class="n">openContext</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">lastValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">getRuntimeContext</span><span class="p">().</span><span class="na">getState</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">new</span><span class="w"> </span><span class="n">ValueStateDescriptor</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="s">&#34;last-value&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">Double</span><span class="p">.</span><span class="na">class</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Override</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">processElement</span><span class="p">(</span><span class="n">Event</span><span class="w"> </span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">Context</span><span class="w"> </span><span class="n">ctx</span><span class="p">,</span><span class="w"> </span><span class="n">Collector</span><span class="o">&lt;</span><span class="n">Alert</span><span class="o">&gt;</span><span class="w"> </span><span class="n">out</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="kd">throws</span><span class="w"> </span><span class="n">Exception</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Double</span><span class="w"> </span><span class="n">previous</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">lastValue</span><span class="p">.</span><span class="na">value</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">previous</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">event</span><span class="p">.</span><span class="na">getValue</span><span class="p">()</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">previous</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">3</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">out</span><span class="p">.</span><span class="na">collect</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">Alert</span><span class="p">(</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">previous</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">lastValue</span><span class="p">.</span><span class="na">update</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">getValue</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That state is <strong>partitioned by key</strong>: each user has their own <code>lastValue</code>, and the operator only sees the key of the current event. You can also configure TTL, which avoids the classic problem of state that only grows until it blows up the disk.</p>
<p>Where that state is kept is a choice, and there are three options in the 2.x line:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Backend</th>
          <th style="text-align: left">Where the state lives</th>
          <th style="text-align: left">For</th>
          <th style="text-align: left">Against</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>HashMapStateBackend</strong></td>
          <td style="text-align: left">JVM heap, as Java objects</td>
          <td style="text-align: left">The fastest, because it serializes nothing on each access</td>
          <td style="text-align: left">Limited by memory and subject to GC pauses. No incremental checkpoints</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>EmbeddedRocksDBStateBackend</strong></td>
          <td style="text-align: left">Local disk, in an embedded LSM tree</td>
          <td style="text-align: left">Scales up to the size of the disk and does incremental checkpoints</td>
          <td style="text-align: left">Serializes on every read and write, and wants a fast disk</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>ForStStateBackend</strong></td>
          <td style="text-align: left">Remote storage, with a local cache</td>
          <td style="text-align: left">Near-immediate rescale and recovery, lightweight TaskManager</td>
          <td style="text-align: left">New and still experimental. Network latency is compensated by asynchronous access</td>
      </tr>
  </tbody>
</table>
<p>The default, if you configure nothing, is <code>HashMapStateBackend</code>. That tends to surprise people who read that &ldquo;Flink uses RocksDB&rdquo;, and the switch is one line in <code>state.backend.type</code>, which accepts <code>hashmap</code>, <code>rocksdb</code> or <code>forst</code>.</p>
<p>The rule of thumb I took from this has three cases. State of a few gigabytes with critical latency calls for HashMap, because the absence of serialization is exactly what you are buying. Large state, from tens of gigabytes to terabytes, calls for RocksDB with incremental checkpoints, and it is worth noting that it is the only one of the two classics that offers this: with HashMap, every checkpoint copies the entire state. And huge state in the cloud, with aggressive autoscaling, is the case for evaluating ForSt, which is the bet of the 2.x series and which I write more about at the end.</p>
<h2>Checkpoints and savepoints<span class="hx:absolute hx:-mt-20" id="checkpoints-and-savepoints"></span>
    <a href="#checkpoints-and-savepoints" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>State is only reliable because there is a fault tolerance mechanism behind it.</p>
<p>Periodically, the JobManager injects <strong>checkpoint barriers</strong> into the sources. Those barriers flow through the graph together with the data; when an operator receives the barrier, it persists its state to durable storage (S3, HDFS, whatever) and passes the barrier along. When all operators finish, the checkpoint is complete.</p>
<p>The algorithm is a variation of <strong>Chandy-Lamport</strong> for distributed snapshots. I found it beautiful to discover that something so practical rests on a paper from 1985.</p>
<p>On failure, Flink restores the last complete checkpoint, repositions the source offsets and resumes. From that comes the famous <strong>exactly-once</strong>, which I misunderstood for quite a while. It does not mean every event is physically processed a single time. On recovery, events are indeed reprocessed. It means the <strong>effect on the state</strong> is as if each event had been processed only once. It is a guarantee about the result, not about the execution.</p>
<p>Extending that guarantee all the way to the sink is another matter, and depends on the destination supporting transactional or idempotent writes (Kafka with transactions, for example).</p>
<p>The difference between checkpoint and savepoint also took a while to settle:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left"></th>
          <th style="text-align: left">Checkpoint</th>
          <th style="text-align: left">Savepoint</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Who triggers it</td>
          <td style="text-align: left">Flink, automatically</td>
          <td style="text-align: left">You, manually</td>
      </tr>
      <tr>
          <td style="text-align: left">Purpose</td>
          <td style="text-align: left">Failure recovery</td>
          <td style="text-align: left">Upgrade, migration, forking the job</td>
      </tr>
      <tr>
          <td style="text-align: left">Lifecycle</td>
          <td style="text-align: left">Managed by Flink</td>
          <td style="text-align: left">Yours forever</td>
      </tr>
  </tbody>
</table>
<p>A savepoint is what allows you to stop a job, bring up a new version of the code and continue exactly where it left off, with the state intact. In practice, that is what makes a streaming job something you can operate for years.</p>
<h2>Flink SQL<span class="hx:absolute hx:-mt-20" id="flink-sql"></span>
    <a href="#flink-sql" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Not everything needs Java. Flink exposes a Table API and SQL over the same engine, and the same query runs both over finite data and over an infinite stream:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">TUMBLE_START</span><span class="p">(</span><span class="n">event_time</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;5&#39;</span><span class="w"> </span><span class="k">MINUTE</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">window_start</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">total</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">user_id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">TUMBLE</span><span class="p">(</span><span class="n">event_time</span><span class="p">,</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;5&#39;</span><span class="w"> </span><span class="k">MINUTE</span><span class="p">);</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The concept underpinning this is the <strong>stream/table duality</strong>: a stream is the changelog of a table, and a table is the accumulated state of a stream. They are the same information seen from two angles.</p>
<p>This stops being abstract as soon as you run a <code>GROUP BY</code> in the SQL client and look at the <code>op</code> column:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&#43;----&#43;------&#43;-------&#43;
| op | name | total |
&#43;----&#43;------&#43;-------&#43;
| &#43;I |    a |     1 |
| &#43;I |    b |     1 |
| -U |    a |     1 |
| &#43;U |    a |     2 |
&#43;----&#43;------&#43;-------&#43;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>a</code> comes in with 1, is retracted (<code>-U</code>) and re-emitted with 2 (<code>+U</code>). It is not a table being filled in bit by bit, it is its changelog passing on the screen. The result of a streaming query is not a final value, it is a sequence of corrections over the previous value.</p>
<p>There are four row kinds in total: <code>+I</code> for insertion, <code>-U</code> and <code>+U</code> for the before and after of an update, and <code>-D</code> for deletion. And it is the combination of kinds a query produces that defines what type of stream you are dealing with, which in practice decides where it can be written:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Mode</th>
          <th style="text-align: left">What comes out of the query</th>
          <th style="text-align: left">Example</th>
          <th style="text-align: left">Where it writes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Append-only</strong></td>
          <td style="text-align: left">Only <code>+I</code>. Nothing already emitted changes.</td>
          <td style="text-align: left">A raw click stream, a <code>SELECT</code> with a filter.</td>
          <td style="text-align: left">Any sink.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Retract</strong></td>
          <td style="text-align: left"><code>+I</code>, and each correction becomes the pair <code>-U</code> followed by <code>+U</code>.</td>
          <td style="text-align: left">A continuous <code>GROUP BY</code>, without a window, revising the total on every event.</td>
          <td style="text-align: left">A sink that understands retraction.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Upsert</strong></td>
          <td style="text-align: left">One row per key, meaning &ldquo;this is the current version&rdquo;.</td>
          <td style="text-align: left">The same <code>GROUP BY</code>, when the sink declares a primary key.</td>
          <td style="text-align: left">A sink with a primary key.</td>
      </tr>
  </tbody>
</table>
<p>The difference between retract and upsert is subtle and matters when choosing the destination. In retract mode, Flink emits two rows for each correction, and the receiver needs to know how to subtract the old one before adding the new one. In upsert mode, it emits only the new row, and the receiver overwrites what it had for that key. The second is cheaper and is what relational databases and search indexes expect, but it is only possible when there is a key that identifies the row.</p>
<p>That leads to the classic trap, which can be predicted before falling into it. You write a continuous <code>GROUP BY</code> and point the result at an ordinary Kafka topic. The standard Kafka connector is append-only, and the result of your query is a flow of corrections, not of insertions. The job does not even start: the planner rejects the query with a message like &ldquo;table sink doesn&rsquo;t support consuming update changes which is produced by node GroupAggregate&rdquo;. Failing at planning time is good news, because the silent way of getting this wrong would be writing the retractions as if they were normal events and having a consumer downstream adding everything up twice.</p>
<p>There are three ways out, and the choice says more about the problem than about Flink. The first is using a sink that accepts updates, like <code>upsert-kafka</code> or JDBC with a declared primary key. The second is making the result stop changing, by grouping by window instead of grouping continuously: when the window closes, the value is final and becomes a single insertion. The third, available from 2.3 onwards, is explicitly converting between the forms with <code>FROM_CHANGELOG</code> and <code>TO_CHANGELOG</code>, which is what I describe in the post about Flink 2.3.</p>
<p>For people coming from data, this is the fastest entry point, you can get far without writing a line of Java.</p>
<h2>In praxi (&ldquo;in practice&rdquo;)<span class="hx:absolute hx:-mt-20" id="in-praxi-in-practice"></span>
    <a href="#in-praxi-in-practice" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Bringing up a local cluster is simpler than I imagined. I started with two loose <code>docker run</code> commands, but switched to a <code>docker-compose.yml</code> as soon as I wanted more than one TaskManager:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">flink-lab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jobmanager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">flink:2.3.0-java21</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">jobmanager</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">jobmanager</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8081:8081&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        FLINK_PROPERTIES=
</span></span></span><span class="line"><span class="cl"><span class="sd">        jobmanager.rpc.address: jobmanager
</span></span></span><span class="line"><span class="cl"><span class="sd">        rest.address: 0.0.0.0
</span></span></span><span class="line"><span class="cl"><span class="sd">        rest.bind-address: 0.0.0.0
</span></span></span><span class="line"><span class="cl"><span class="sd">        jobmanager.memory.process.size: 1024m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD-SHELL&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;curl -sf http://localhost:8081/overview || exit 1&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">3s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">20</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">taskmanager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">flink:2.3.0-java21</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">taskmanager</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">jobmanager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        FLINK_PROPERTIES=
</span></span></span><span class="line"><span class="cl"><span class="sd">        jobmanager.rpc.address: jobmanager
</span></span></span><span class="line"><span class="cl"><span class="sd">        taskmanager.numberOfTaskSlots: 2
</span></span></span><span class="line"><span class="cl"><span class="sd">        taskmanager.memory.process.size: 1728m</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>One <code>docker compose up -d</code> and the Web UI answers at <code>localhost:8081</code>.</p>
<p>Three choices there are not details. Two TaskManager replicas with two slots each give four slots, and that is what makes parallelism stop being always 1: without spare slots, every job graph shows up in the UI as a straight line. The <code>healthcheck</code> combined with <code>depends_on: service_healthy</code> exists because, without it, the TaskManager starts before the JobManager is up, fails to register and fills the log with errors before working on the second try. And the memory limits are explicit because Flink&rsquo;s default is generous: with these values the whole cluster stays around 4.5 GB and fits on any machine.</p>
<p>Before submitting anything, the REST API tells you whether the cluster is whole more directly than the UI:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s localhost:8081/overview</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;taskmanagers&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="nt">&#34;slots-total&#34;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> <span class="nt">&#34;slots-available&#34;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> <span class="nt">&#34;flink-version&#34;</span><span class="p">:</span> <span class="s2">&#34;2.3.0&#34;</span><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The two registered TaskManagers and the four free slots are the confirmation that the cluster formed. The image still ships example jobs, so you can see a running graph without writing a line:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> jobmanager ./bin/flink run -d <span class="se">\
</span></span></span><span class="line"><span class="cl">  /opt/flink/examples/streaming/TopSpeedWindowing.jar</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With it running, <code>slots-available</code> drops to 3 and the UI finally has something to show.</p>
<p>The Web UI was where I learned the most. It shows the job graph, the real parallelism of each operator, the checkpoint history (duration and size) and, above all, <strong>backpressure</strong>: when an operator cannot keep up with the pace of the previous one and the pressure propagates backwards all the way to the source. Watching that happen in real time teaches more than any diagram.</p>
<p>For SQL, the shortest path is the interactive client:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -it jobmanager ./bin/sql-client.sh</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Deployment<span class="hx:absolute hx:-mt-20" id="deployment"></span>
    <a href="#deployment" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The <code>docker-compose.yml</code> above is a lab. It brings up a cluster, but hides almost all the decisions that appear when the job needs to run for months without anyone watching. This was the part that most forced me to read the manual, because here no intuition saves you: it is arithmetic, it is container limits and it is choosing where the state will end up.</p>
<h3>Execution modes<span class="hx:absolute hx:-mt-20" id="execution-modes"></span>
    <a href="#execution-modes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Before choosing where to run, you need to choose how. Flink has two living modes today, and the difference between them is the cluster&rsquo;s lifecycle.</p>
<p>In <strong>session mode</strong>, the cluster comes up first and stays up waiting. You submit several jobs to the same JobManager, and all of them compete for the same TaskManagers. It is what my compose does, and it is the right mode for exploration, for the SQL client and for short jobs that do not justify the cost of bringing up a new cluster. The price is the absence of isolation: a job that blows up memory takes down the TaskManager, and with it go the tasks of all the other jobs that had slots in that process.</p>
<p>In <strong>application mode</strong>, each application gets its own dedicated cluster, which is born with it and dies with it. The most important difference is subtle and lies in <code>main()</code>: instead of running on the machine of whoever submits, it runs inside the JobManager. That takes away from the client the responsibility of downloading dependencies and assembling the job graph, and it is what makes the mode suitable for automation, because there is no longer an external client process that needs to stay alive. For production, it is the default answer.</p>
<p>There was also a third mode, per-job, which also gave one cluster per job but kept <code>main()</code> on the client. It was discontinued and is no longer part of the 2.x line. If you find <code>-t yarn-per-job</code> in some tutorial, it is material from an earlier version.</p>
<p>Orthogonal to this is the resource manager: standalone (you bring up the processes yourself, which is the Docker Compose case), Kubernetes or YARN. The combination I see recommended most often today is application mode on Kubernetes.</p>
<h3>Kubernetes<span class="hx:absolute hx:-mt-20" id="kubernetes"></span>
    <a href="#kubernetes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On Kubernetes there are three paths, and it is worth understanding the difference because they solve distinct problems.</p>
<p>The first is <strong>standalone on Kubernetes</strong>: you write two <code>Deployment</code>s, one for the JobManager and another for the TaskManager, a <code>Service</code> and a <code>ConfigMap</code> with the <code>flink-conf.yaml</code>. Flink does not know it is on Kubernetes, it only sees processes that register. It works, and it is transparent, but scaling is on you: changing parallelism means changing replicas by hand, and upgrading with a savepoint is a sequence of commands someone needs to remember to run in the right order.</p>
<p>The second is <strong>native Kubernetes</strong>, in which the JobManager itself talks to the Kubernetes API and creates the TaskManager pods needed to satisfy the requested parallelism. You submit with <code>flink run-application -t kubernetes-application</code> and write no TaskManager manifest at all. The cluster sizes itself according to the job, and not the other way around.</p>
<p>The third, and the one I would use, is the <strong>Flink Kubernetes Operator</strong>. It installs CRDs and turns the job into a declarative cluster object:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">flink.apache.org/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">FlinkDeployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">otlp-processor</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">registry.example.com/otlp-processor:1.4.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">flinkVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v2_0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccount</span><span class="p">:</span><span class="w"> </span><span class="l">flink</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">flinkConfiguration</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">taskmanager.numberOfTaskSlots</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;4&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">state.backend.type</span><span class="p">:</span><span class="w"> </span><span class="l">rocksdb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">state.checkpoints.dir</span><span class="p">:</span><span class="w"> </span><span class="l">s3://flink-state/otlp-processor/checkpoints</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">state.savepoints.dir</span><span class="p">:</span><span class="w"> </span><span class="l">s3://flink-state/otlp-processor/savepoints</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">execution.checkpointing.interval</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;60s&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">high-availability.type</span><span class="p">:</span><span class="w"> </span><span class="l">kubernetes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">high-availability.storageDir</span><span class="p">:</span><span class="w"> </span><span class="l">s3://flink-state/otlp-processor/ha</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jobManager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resource</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">2048m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">taskManager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resource</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">8192m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">job</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jarURI</span><span class="p">:</span><span class="w"> </span><span class="l">local:///opt/flink/usrlib/otlp-processor.jar</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">parallelism</span><span class="p">:</span><span class="w"> </span><span class="m">12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">upgradeMode</span><span class="p">:</span><span class="w"> </span><span class="l">savepoint</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>What this manifest buys is operations. <code>upgradeMode: savepoint</code> means that, when I change the image tag and apply again, the operator triggers a savepoint, stops the job, brings up the new version and restores the state from it, without anyone needing to run <code>flink stop --savepointPath</code> by hand at three in the morning. It also takes care of rollback when the new job does not stabilize, and brings an autoscaler that watches the real backpressure of the operators and adjusts the parallelism, which is quite different from an HPA looking at the pod&rsquo;s CPU usage.</p>
<p>Two details that are not optional. The first is the <code>serviceAccount</code>: the JobManager needs RBAC permission to create pods and to read and write ConfigMaps, because it is in a ConfigMap that native high availability keeps who the leader is and what the last checkpoint is. The second is the <code>high-availability.storageDir</code>: the ConfigMap keeps only the pointer, the real metadata goes to durable storage. Without that directory configured, a JobManager that dies comes back not knowing where to resume from, and the entire state is lost despite the checkpoints existing.</p>
<h3>Memory<span class="hx:absolute hx:-mt-20" id="memory"></span>
    <a href="#memory" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is the arithmetic that takes the most work, because Flink does not have one memory number, it has a tree of them. The value you configure is <code>taskmanager.memory.process.size</code>, and it is the total the process can occupy in the operating system. Everything else is subdivision.</p>
<p>Inside it, Flink first separates what belongs to the JVM and not to Flink: the metaspace (<code>taskmanager.memory.jvm-metaspace.size</code>, 256m by default) and an overhead for thread stacks, internal buffers and native code (<code>taskmanager.memory.jvm-overhead.fraction</code>, 10% of the process size). What remains is Flink&rsquo;s memory proper, and it splits into four parts:</p>
<ul>
<li>The task heap, where your objects live, and the state when the backend is the heap one.</li>
<li>Managed memory, which is off-heap and is where RocksDB allocates its block caches and write buffers. In a batch job, it is also where sort and hash join happen.</li>
<li>Network memory, which is the buffers for exchanging data between tasks.</li>
<li>A slice reserved for the framework, which you rarely touch.</li>
</ul>
<p>The rule that matters above all others: <strong>the <code>process.size</code> has to fit inside the container&rsquo;s memory limit</strong>. If you give the pod an 8Gi limit and configure <code>process.size: 8192m</code>, the pod will be killed by OOMKill, because the cgroup also counts file pages, native RocksDB allocations outside what Flink accounts for and what the kernel itself uses. Leaving a 10 to 15% margin solves it. For an 8Gi limit, I would configure around 7000m and sleep better.</p>
<p>The second decision is how to split. If the state is large and the backend is RocksDB, managed memory is what makes the difference, because it determines the size of the cache before access turns into disk reads. A reasonable starting split is 40% managed memory and the rest between heap and network. If the state is small and fits in heap, the opposite path: <code>taskmanager.memory.managed.fraction: 0.1</code> and everything else to the heap.</p>
<p>Network memory is the most surprising one. It is 10% of Flink&rsquo;s memory by default, capped at 1 GB, and consumption grows with the number of connections between tasks, which in turn grows with the square of the parallelism in <code>keyBy</code>-style exchanges. A job with parallelism 4 that moves to 200 can start failing with an insufficient buffers error without a single line of code having changed. It is one of the few cases where increasing parallelism makes things worse before it makes them better.</p>
<h3>CPU and slots<span class="hx:absolute hx:-mt-20" id="cpu-and-slots"></span>
    <a href="#cpu-and-slots" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A slot is not a CPU. A slot is a slice of memory and a place in the scheduling, but the threads of all the slots in a TaskManager compete for the same cores of the JVM. There is no CPU isolation between slots.</p>
<p>That is why the starting rule is simple: <code>taskmanager.numberOfTaskSlots</code> equal to the number of cores the container has as its limit. Four cores, four slots. Going much above that does not increase throughput, it only increases context switching and the pause time the garbage collector has to manage.</p>
<p>The total parallelism of the job is then the number of TaskManagers times the slots of each one, and that multiplication is the number that appears in the manifest. Three TaskManagers of four slots give twelve, which is the parallelism of the example above.</p>
<p>To get to the right number of TaskManagers, I would not try to calculate from first principles. The path that makes sense is measuring: bring up the job with low parallelism, inject a known load and look at <code>numRecordsInPerSecond</code> per subtask in the UI or in Prometheus. That gives a throughput per slot. Divide the expected peak by that number, add a margin of about 30% for spikes and for reprocessing (because after a crash the job needs to run faster than production to catch up to the present) and the result is how many slots are needed. A filter-and-projection job can do hundreds of thousands of events per second in one slot; a job with a large window, state in RocksDB and JSON deserialization can do a few thousand. The difference between those two cases is orders of magnitude, and that is exactly why you cannot guess.</p>
<p>It is worth remembering that changing the parallelism of a stateful job later is not free: the state is redistributed among the new subtasks on restore, and the ceiling remains the <code>maxParallelism</code> fixed on the first run, for the reason I described in the parallelism section.</p>
<h3>Local disk<span class="hx:absolute hx:-mt-20" id="local-disk"></span>
    <a href="#local-disk" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With RocksDB, the working state is not in memory, it is in files on the TaskManager&rsquo;s local disk, pointed to by <code>io.tmp.dirs</code>. That is what allows having state larger than RAM, and it is also what makes the type of disk matter.</p>
<p>It needs to be local SSD, not a network volume. RocksDB is an LSM tree: it writes to sorted files and then compacts those files in the background, rewriting data that was already there. That write amplification, added to random reads on every state access, is exactly the I/O pattern a network disk serves badly. I have seen the same job go from stable to unusable just by switching the volume type.</p>
<p>For sizing, the arithmetic starts from the logical state: number of active keys times the state size per key, divided by the parallelism, gives the state per TaskManager. After that, multiply. Two to three times is a reasonable margin, because compaction needs space to write the new files before deleting the old ones, and because incremental checkpoints keep files referenced by earlier snapshots that have not yet been discarded. Ten million keys with 1 KB of state each give 10 GB of logical state; across three TaskManagers, that is something between 7 and 10 GB of disk on each.</p>
<p>On Kubernetes, that means choosing between <code>emptyDir</code> and a persistent volume. With <code>emptyDir</code>, the disk dies with the pod, and a restart forces the TaskManager to download the entire state from remote storage before it resumes processing, which for large states is the difference between seconds and many minutes of recovery. With a persistent volume and <code>state.backend.local-recovery: true</code>, the local copy survives the restart and recovery reads from disk instead of the network. It is more complexity in exchange for recovery time, and the choice depends on how much the delay costs.</p>
<h3>S3 and the remote file system<span class="hx:absolute hx:-mt-20" id="s3-and-the-remote-file-system"></span>
    <a href="#s3-and-the-remote-file-system" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Here is the confusion I myself had before studying: <strong>S3 is not where the state lives, it is where copies of it are kept</strong>. The hot state is in the heap or on the TaskManager&rsquo;s local disk, and that is where it is read and written on every event. S3 enters at four moments, and all of them are periodic or exceptional.</p>
<p>The first is checkpoints. At every configured interval, each operator writes its state to <code>state.checkpoints.dir</code>. That is what survives the simultaneous loss of all pods.</p>
<p>The second is savepoints, in <code>state.savepoints.dir</code>, which is the same mechanism with a different lifecycle: they are yours, they are not deleted by Flink, and it is from them that the upgrade without losing state comes.</p>
<p>The third is the high availability metadata, in <code>high-availability.storageDir</code>. The Kubernetes ConfigMap is too small to hold the job graph and the full pointers, so it keeps only the reference and the content lives in remote storage.</p>
<p>The fourth, when it exists, is the data itself: a source that reads Parquet from a bucket, or a sink that writes results partitioned by hour.</p>
<p>The reason it is S3, and not a shared disk, is that durability needs to be independent of the cluster. A checkpoint that is on the same hardware as the job does not protect against the loss of that hardware. Object storage is cheap, replicated and keeps existing after the whole cluster stops existing, which is precisely the property that makes a streaming job recoverable. GCS, Azure Blob and HDFS occupy the same place in the architecture, with a different letter in the URI scheme.</p>
<p>The configuration has a gotcha that costs time. Flink ships two S3 implementations, and they are not interchangeable. <code>flink-s3-fs-presto</code> is the one indicated for checkpoints, because it is fast with small files and does not depend on renaming objects, an operation S3 does not have atomically and emulates with a copy followed by a delete. <code>flink-s3-fs-hadoop</code> is the one that supports recoverable writes, which is what the file sink needs to do multipart uploads and commit only at the checkpoint. Many people install both and use <code>s3p://</code> for state and <code>s3a://</code> for data. Both are plugins and need to go to <code>/opt/flink/plugins/</code>, each in its own directory, and not to <code>lib/</code>. In <code>lib/</code> they load in the main classloader and conflict with each other.</p>
<p>The last point is about the checkpoint interval, which looks like a latency setting and is also a cost calculation. Every checkpoint writes at least one file per stateful subtask. A job with parallelism 200 and a checkpoint every 10 seconds produces on the order of seventy thousand objects per hour, and S3 charges per request besides rate-limiting per prefix. That shows up as rising checkpoint latency and, at the limit, as checkpoints expiring by timeout. Two defenses help: raising <code>state.storage.fs.memory-threshold</code> so that small states travel inside the metadata file instead of becoming separate objects, and accepting a longer interval. One minute between checkpoints is a common value, and what it costs is only the amount of work that needs to be redone after a failure.</p>
<h2>The ecosystem around it<span class="hx:absolute hx:-mt-20" id="the-ecosystem-around-it"></span>
    <a href="#the-ecosystem-around-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Flink rarely appears alone. It is a processing engine, and an engine needs something to feed it, somewhere to deposit and people who read what it produced. A good part of the work of assembling a pipeline is choosing those pieces, so it is worth mapping the ones that most often appear next to it and, above all, why they appear.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Tool</th>
          <th style="text-align: left">Where it comes in</th>
          <th style="text-align: left">Why it shows up alongside</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Apache Kafka</strong></td>
          <td style="text-align: left">Ingestion</td>
          <td style="text-align: left">Decouples whoever produces from whoever processes and keeps the history. It is what allows reprocessing a job from the beginning, and not just from now</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Debezium</strong></td>
          <td style="text-align: left">Ingestion</td>
          <td style="text-align: left">Reading binlog and WAL correctly, with an initial snapshot and without losing a transaction, is hard enough that nobody wants to rewrite it. Flink CDC uses it underneath</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Schema Registry, with Avro or Protobuf</strong></td>
          <td style="text-align: left">Contract</td>
          <td style="text-align: left">A job that deserializes JSON by hand breaks at the producer&rsquo;s first change. The registry makes the contract explicit and verifiable before deploy, not in production</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Apache Iceberg</strong></td>
          <td style="text-align: left">Storage</td>
          <td style="text-align: left">Gives transactions, schema evolution and time travel over files in object storage. Without it the job writes loose Parquet and readers see half-written data</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Apache Paimon</strong></td>
          <td style="text-align: left">Storage</td>
          <td style="text-align: left">Born inside the Flink community for the case Iceberg serves worst: high-rate primary key updates, with a changelog that can be read back</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Apache Hudi</strong></td>
          <td style="text-align: left">Storage</td>
          <td style="text-align: left">The third option in the same space, older, focused on upsert and incremental reads</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Hive Metastore, AWS Glue or Nessie</strong></td>
          <td style="text-align: left">Catalog</td>
          <td style="text-align: left">The table format knows which files make up a table. The catalog knows which tables exist. Without it, there is nothing to list in Flink SQL</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Trino</strong></td>
          <td style="text-align: left">Query</td>
          <td style="text-align: left">Flink is not an interactive query engine. Trino reads the same Iceberg tables to answer the question nobody had anticipated</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>ClickHouse, Druid or Pinot</strong></td>
          <td style="text-align: left">Analytical serving</td>
          <td style="text-align: left">A dashboard wants millisecond answers over recent data. Flink pre-aggregates and pushes there, and the user&rsquo;s query never touches the stream</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>PostgreSQL and MySQL</strong></td>
          <td style="text-align: left">Operational</td>
          <td style="text-align: left">Source via CDC on one side and destination of the aggregated result on the other, when the consumer is an application and not an analyst</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Redis</strong></td>
          <td style="text-align: left">Low-latency serving</td>
          <td style="text-align: left">Keeps what has already been computed for whoever needs to read outside the stream, in microseconds. It is also the typical online store of a feature store</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Feast</strong></td>
          <td style="text-align: left">Machine learning</td>
          <td style="text-align: left">Feature store. Guarantees that the feature used in training and the one used in inference are the same thing, computed the same way</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Prometheus and Grafana</strong></td>
          <td style="text-align: left">Operations</td>
          <td style="text-align: left">Metrics of the job itself (checkpoint duration, backpressure, lag per partition) and the natural destination for the derived metrics the job computes</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Airflow or Dagster</strong></td>
          <td style="text-align: left">Orchestration</td>
          <td style="text-align: left">A streaming job has no schedule, but everything around it does: backfill, table compaction, snapshot expiration, upgrade with savepoint</td>
      </tr>
  </tbody>
</table>
<p>Three of those choices have a detail that only appears later, and it is worth anticipating.</p>
<p>The first is the small files problem. The Iceberg sink commits what it wrote when the checkpoint closes, so the checkpoint interval stops being only a recovery decision and starts determining how many files and how many snapshots the table gains per hour. A checkpoint every ten seconds in a job with high parallelism produces a table with thousands of tiny files per day, which is the worst possible format for whoever reads it later. The solution is not avoiding Iceberg, it is accepting that compaction and snapshot expiration are recurring work, and it is precisely that work that usually justifies an Airflow next to a pipeline that, on paper, had nothing scheduled.</p>
<p>The second is the catalog, which is almost always remembered late. It is easy to think about the table format and forget that someone needs to know the table exists. Without a configured catalog, Flink SQL&rsquo;s <code>CREATE TABLE</code> lives only in the session and disappears when the client closes, and the Trino on the other side sees nothing of what was created. Choosing the catalog is what makes Flink and the other engines talk about the same table, instead of the same files by coincidence.</p>
<p>The third is the reason a feature store exists, which took me a while to understand. The problem it solves is not storage, it is consistency: if the feature &ldquo;average purchases in the last 30 minutes&rdquo; is computed by a Flink job at inference and by a SQL query at training, the two will disagree in some detail, and the model starts seeing in production data different from what it saw while learning. Keeping a single definition, fed by the same job, is the point. And it is worth noting that part of this is migrating into Flink itself with the model functions of 2.1 and 2.2, which I describe in the post about Flink 2.3.</p>
<h2>Complexities<span class="hx:absolute hx:-mt-20" id="complexities"></span>
    <a href="#complexities" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Fine-tuning RocksDB and the behavior of state when it grows far beyond memory.</li>
<li>Join strategies between streams, mainly interval joins and temporal joins.</li>
<li>The CEP library, mentioned up in the use cases and not yet tested by me.</li>
<li>How all of this really behaves in production under irregular load, the part no documentation teaches.</li>
</ul>
<h2>What stayed with me<span class="hx:absolute hx:-mt-20" id="what-stayed-with-me"></span>
    <a href="#what-stayed-with-me" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I ended the week with an impression I did not expect to have: the hard part of streaming is not distributing the processing. Flink solves that. The hard part is <strong>time</strong>: accepting that data arrives out of order, that &ldquo;complete&rdquo; is a bet and not a fact, and that almost every technical decision here is, deep down, a choice between latency and correctness.</p>
<p>Writing this forced me to admit three or four things I thought I knew and did not. It was worth the effort.</p>
<p>It remains to say why I went after this, because it is not Flink for Flink&rsquo;s sake. What I want is to process OTLP, the format the OpenTelemetry Collector speaks, and in practice that means the four signals: logs, spans, metrics and profiles.</p>
<p>The Collector receives, transforms and forwards very well, but it is built for data to pass through. The moment the question needs memory between events, event-time windows or correlating signals that arrive separately and out of order, it falls exactly into what this text describes: <code>keyBy</code>, keyed state and watermarks. That fit is what I want to test in the next round.</p>
<p>Next week on the schedule: reviewing the Data Engineer roadmap. I keep writing while I learn.</p>
]]></content:encoded><category>apache-flink</category><category>data-engineering</category><category>learning-in-public</category></item><item><title>The Myth of Er</title><link>https://apolzek.github.io/en/2026/08/04/the-myth-of-er/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/08/04/the-myth-of-er/</guid><pubDate>Tue, 04 Aug 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;This is a text about the Myth of Er, from Plato&amp;rsquo;s Republic. It is a book I read without having, at the time, the foundations needed to fully understand it. Even so, reading it became necessary for the stage of life I was going through. Before reading it, I researched the work, I already knew Plato&amp;rsquo;s biography and my interest in philosophy had been awakened.&lt;/p&gt;
&lt;p&gt;What is the &lt;em&gt;Republic&lt;/em&gt;? It is one of Plato&amp;rsquo;s most important and best-known works, probably written in the 4th century BC, around 375 BC. The work is built mainly in the form of dialogues, with Socrates as the central character.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This is a text about the Myth of Er, from Plato&rsquo;s Republic. It is a book I read without having, at the time, the foundations needed to fully understand it. Even so, reading it became necessary for the stage of life I was going through. Before reading it, I researched the work, I already knew Plato&rsquo;s biography and my interest in philosophy had been awakened.</p>
<p>What is the <em>Republic</em>? It is one of Plato&rsquo;s most important and best-known works, probably written in the 4th century BC, around 375 BC. The work is built mainly in the form of dialogues, with Socrates as the central character.</p>
<p>One of the great problems guiding the discussion is an apparently simple yet quite uncomfortable question: <em>is it worth being just?</em> What is justice? Is it better to be just for its own sake, or only for the advantages and rewards it may bring? In other words: is it worth acting rightly when injustice could bring more benefits?</p>
<p>From these questions, Plato develops a long inquiry into justice, education, the organization of the city, the nature of the soul and the way we ought to live.</p>
<p>It is in this context that the <em>Myth of Er</em> appears, closing the work. The myth tells the experience of Er, a man who supposedly died in battle and returned to life to tell what he saw after death. His account deals with the fate of souls, the consequences of their choices and the possibility of choosing a new life.</p>
<p>Being at the end of the <em>Republic</em>, the <em>Myth of Er</em> works as a symbolic conclusion of the work and takes up one of its fundamental questions: what are the consequences of our choices, and what kind of life should we choose? It should not be understood simply as Plato&rsquo;s &ldquo;final answer&rdquo;, but as a narrative that closes and expands the philosophical inquiry developed throughout the dialogue.</p>
<p><strong>Who tells the story?</strong> Socrates, who was Plato&rsquo;s teacher and is the main character in almost all his books. It is through his mouth that the myth is narrated.</p>
<h2>The setting<span class="hx:absolute hx:-mt-20" id="the-setting"></span>
    <a href="#the-setting" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Er is a warrior who dies in battle. His body lies for days on the field without rotting and, when it is about to be cremated, he wakes up and tells everything he saw on the other side.</p>
<p>Basically: the first near-death experience report in the history of philosophy.</p>
<p>What Er describes is the process by which souls return to the sensible world. Translating the technical term: the &ldquo;sensible world&rdquo; is simply the world we see, touch, smell, the material world, THE ONE HERE. Plato believed the soul exists before and after the body, and that it goes and comes back. What Er saw was precisely the boarding desk of that return. And there are people working there. The Moirai, three figures from Greek mythology who control each person&rsquo;s fate, represented as spinners (women who spin, that is, produce thread). The idea is that your life is a thread, and they take care of that thread:</p>
<ul>
<li><strong>Clotho</strong>: the one who <em>spins</em>, that is, produces the thread of your life.</li>
<li><strong>Lachesis</strong>: the one who <em>measures</em> and distributes, draws lots and organizes destinies.</li>
<li><strong>Atropos</strong>: the one who <em>cuts</em> the thread. She is death. Her name literally means &ldquo;the inflexible&rdquo;, the one who does not turn back.</li>
</ul>
<p>Keep that detail, because it matters: the Moirai are there, fate exists in the story. Plato is not denying fate. He is going to do something smarter than that.</p>
<h2>The rules of the game<span class="hx:absolute hx:-mt-20" id="the-rules-of-the-game"></span>
    <a href="#the-rules-of-the-game" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now comes the good part. The rebirth of souls follows very specific rules.. and each one carries a message.</p>
<h3>1. There is a lottery<span class="hx:absolute hx:-mt-20" id="1-there-is-a-lottery"></span>
    <a href="#1-there-is-a-lottery" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>When the souls are ready to be reborn, they take part in a lottery. But the lottery does not define the life you will have. It only defines the order in which each soul will choose. It is a queue ticket, not a prize. You do not draw what you will take; you draw when you will get to pick.</p>
<h3>2. There is enough life for everyone<span class="hx:absolute hx:-mt-20" id="2-there-is-enough-life-for-everyone"></span>
    <a href="#2-there-is-enough-life-for-everyone" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>And here is the central point of the myth: there are more life options than souls. Chance takes part in the process. The lottery exists and determines the order in which each soul will choose, but the choice of the life each one will lead is, ultimately, its own. And, along with the choice, comes the responsibility for it. That idea hit me in an unexpected way. It was the kind of passage that made me stop reading and stare at the ceiling for a good while.</p>
<h3>3. The gods are not to blame<span class="hx:absolute hx:-mt-20" id="3-the-gods-are-not-to-blame"></span>
    <a href="#3-the-gods-are-not-to-blame" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Plato hammers on this with full force. The sentence that appears in the myth is practically a verdict: the blame belongs to the one who chooses, and the divinity is blameless. Nobody shoved anything down anybody&rsquo;s throat. Each soul chooses its path according to the wisdom it has, or according to the lack of it. In the Greek context, that was quite a provocation. The culture of the time was full of stories in which the gods ruined people&rsquo;s lives out of whim, jealousy or revenge. Plato is cutting that excuse at the root.</p>
<h2>The menu of lives<span class="hx:absolute hx:-mt-20" id="the-menu-of-lives"></span>
    <a href="#the-menu-of-lives" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And what was available to choose from? Everything. Rich lives, poor lives, lives of tyrants, lives of famous people, lives of nobodies, simple lives, even the lives of animals. A full buffet of possible existences. But that is where things get really complicated. Because the life that looks good is not always good. The life of a tyrant shines from afar: power, money, everyone obeying you. Up close, it is a machine for producing injustice and, according to Plato, whoever produces injustice ends up destroying themselves from within. There is a great scene in the myth: a soul throws itself at the life of a tyrant, grabs it with everything it has, and only afterwards realizes what came bundled in the package. Then it leaves lamenting and blaming the gods. But it chose for itself. It just did not look properly. So the real challenge was never choosing the flashiest life. The challenge was having enough self-knowledge to choose a life that led to virtue and not to injustice.</p>
<p>Quickly on those two terms, because they are the heart of the text:</p>
<ul>
<li><strong>Virtue</strong>, for Plato, is not being &ldquo;nice&rdquo; or following rules of moral etiquette. It is the soul working well, in order, with reason in charge, like a tuned instrument.</li>
<li><strong>Injustice</strong> is the opposite: the soul in disarray, ungoverned, with the impulses in command. For Plato, this is a kind of disease of the soul. And that is why being unjust is bad for you, even if nobody finds out and even if you profit from it.</li>
</ul>
<h3>And there is a cruel detail: character<span class="hx:absolute hx:-mt-20" id="and-there-is-a-cruel-detail-character"></span>
    <a href="#and-there-is-a-cruel-detail-character" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Character</strong>, one&rsquo;s way of being, the moral structure of a person, is very hard to change after the choice is made. In other words: you do not only choose the circumstances of your life. You also choose, bundled and built in, the kind of person you will be within it. It all comes in the same package. That is why the myth insists so much that the most important skill in the world is being able to foresee the good and the evil a life can bring. Knowing how to look at an option and see not the shine on the surface, but what it will do to you on the inside, in the long run. Without that, you are betting in the dark with your own destiny.</p>
<h2>And why don&rsquo;t we remember any of this?<span class="hx:absolute hx:-mt-20" id="and-why-dont-we-remember-any-of-this"></span>
    <a href="#and-why-dont-we-remember-any-of-this" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Good question, and the myth answers it. Before reincarnating, the souls cross an arid plain and drink the water of the <strong>River of Forgetfulness</strong> (in Greek, <em>Lethe</em>, a word that literally means &ldquo;forgetting&rdquo;). One sip and done: everything is erased. That is why nobody arrives here remembering their own choice. We are born without the manual, without the receipt, without the memory of having signed at the bottom. Which is a bit mean, I admit. But it is also exactly what makes life a real life, and not the execution of a script you have already read before.</p>
<h2>What Plato really meant by all this<span class="hx:absolute hx:-mt-20" id="what-plato-really-meant-by-all-this"></span>
    <a href="#what-plato-really-meant-by-all-this" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>1. It is a frontal attack on fatalism<span class="hx:absolute hx:-mt-20" id="1-it-is-a-frontal-attack-on-fatalism"></span>
    <a href="#1-it-is-a-frontal-attack-on-fatalism" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Fatalism</strong> is the belief that everything is already written and nothing you do matters, &ldquo;if it is meant to be, it will be&rdquo;. Its cousin, <strong>determinism</strong>, is the idea that everything you do was already caused by things outside your control: genetics, upbringing, society, chance. Notice what Plato does: he <strong>does not deny</strong> that fate exists. The Moirai are there, weaving. The lottery exists. Chance exists and really interferes. He could simply have said &ldquo;fate does not exist&rdquo;, but he did not. What he does is more sophisticated: he shows that, <strong>even within fate, freedom remains</strong>. You do not choose your position in the queue. You choose what you do with it. And since there is always a good option left on the table, the excuse &ldquo;I had no alternative&rdquo; simply does not hold.</p>
<p>That is the central thesis: <strong>the human being is free and, being free, is responsible for the path they walk</strong>.</p>
<h3>2. It is a message about whose fault it is<span class="hx:absolute hx:-mt-20" id="2-it-is-a-message-about-whose-fault-it-is"></span>
    <a href="#2-it-is-a-message-about-whose-fault-it-is" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The entire structure of the myth serves to close the emergency exits we use to escape responsibility.</p>
<p>Blame the gods?? Denied.. the divinity is innocent. Blame the lottery?? Denied.. there was good life left over. Blame the lack of options?? Denied.. the menu was huge. Blame whoever chose before you?? Denied.. ask Odysseus.</p>
<p>What is left?? You =)))))</p>
<h3>3. The &ldquo;choice&rdquo; did not happen in an afterlife, it is happening now<span class="hx:absolute hx:-mt-20" id="3-the-choice-did-not-happen-in-an-afterlife-it-is-happening-now"></span>
    <a href="#3-the-choice-did-not-happen-in-an-afterlife-it-is-happening-now" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is the most important point, and it is where the myth stops being a story about reincarnation and becomes a story about <strong>today</strong>. Remember that character comes built into the choice and is almost impossible to change afterwards? Remember that the soul that grabbed the tyrant&rsquo;s life did so on impulse, without examining? Well: that is an image of what we do all the time.</p>
<p>With every decision, what to study, who to hang out with, what to accept, what to let slide, what kind of person to allow yourself to be in an argument, you are back in that field, choosing a life package without properly seeing what comes inside. And, just as in the myth, after a while it becomes very hard to undo. The River of Forgetfulness is the perfect image of this: we forget that we are choosing. We act on autopilot, blame the circumstances, and only notice the package when it is already too late.</p>
<h3>4. That is why philosophy matters.. and that is the real conclusion of the book<span class="hx:absolute hx:-mt-20" id="4-that-is-why-philosophy-matters-and-that-is-the-real-conclusion-of-the-book"></span>
    <a href="#4-that-is-why-philosophy-matters-and-that-is-the-real-conclusion-of-the-book" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Here everything fits together. If the only thing separating a good choice from a disastrous one is the <strong>ability to examine the options before grabbing the first one that shines</strong>, then the most valuable skill there is consists in learning to examine. That is what Plato calls philosophy. Not memorizing Greek names: it is the training of looking at things calmly and seeing what they really are, not what they appear to be. It is exactly what Odysseus did while everyone else was trampling each other.</p>
<p>And so the <em>Republic</em> closes the circle. The initial question was &ldquo;is it worth being just?&rdquo;. The myth&rsquo;s answer is: <strong>it is</strong>, because justice is not a rule imposed from outside that gets in your way, it is what keeps your soul in order, and a soul in order is the only thing capable of choosing well. Being unjust does not give you an advantage; it leaves you blind at the moment when seeing matters most.</p>
<hr>
<p>&ldquo;Keep thy heart with all diligence; for out of it are the issues of life.&rdquo; — Proverbs 4:23</p>
]]></content:encoded><category>philosophy</category><category>reading</category></item><item><title>Dialogue on the Master and Practice</title><link>https://apolzek.github.io/en/2026/07/15/dialogue-on-the-master-and-practice/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/07/15/dialogue-on-the-master-and-practice/</guid><pubDate>Wed, 15 Jul 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;&lt;em&gt;Setting: Socrates is walking near the palaestra when Glaucon hurries out of it, dust still on his shoulders and visibly upset.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SOCRATES:&lt;/strong&gt; Glaucon, you leave the palaestra like a man fleeing a fire. What happened?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GLAUCON:&lt;/strong&gt; An argument, Socrates. The young men mocked old Ikkos, who teaches the wrestlers there. They said: &amp;ldquo;how can a man who never won at Olympia teach anyone to win?&amp;rdquo;. And I confess I laughed along with them, but now something bothers me.&lt;/p&gt;</description><content:encoded><![CDATA[<p><em>Setting: Socrates is walking near the palaestra when Glaucon hurries out of it, dust still on his shoulders and visibly upset.</em></p>
<p><strong>SOCRATES:</strong> Glaucon, you leave the palaestra like a man fleeing a fire. What happened?</p>
<p><strong>GLAUCON:</strong> An argument, Socrates. The young men mocked old Ikkos, who teaches the wrestlers there. They said: &ldquo;how can a man who never won at Olympia teach anyone to win?&rdquo;. And I confess I laughed along with them, but now something bothers me.</p>
<p><strong>SOCRATES:</strong> What bothers you?</p>
<p><strong>GLAUCON:</strong> Ikkos trained three champions. None of his disciples ever asked about his crowns, and all of them left crowned. How can that be?</p>
<p><strong>SOCRATES:</strong> Tell me: does the charioteer who wins the race win by his own eyes, or by the eyes of the horse?</p>
<p><strong>GLAUCON:</strong> By his own, I suppose.</p>
<p><strong>SOCRATES:</strong> And the man in the stands who notices that the charioteer takes the turn too early and loses half a length, does he himself need to know how to drive the chariot?</p>
<p><strong>GLAUCON:</strong> Perhaps not. Seeing the error and performing the right move seem to be different things.</p>
<p><strong>SOCRATES:</strong> Then you have already found two arts where you thought there was one. There is the art of doing, which lives in the hands and the legs, and there is the art of seeing and explaining, which lives in the eyes and in speech. Could the fastest runner in Athens tell you <em>why</em> he is fast?</p>
<p><strong>GLAUCON:</strong> I doubt it. I once asked one of them and he only answered: &ldquo;I have been running since I was a boy&rdquo;.</p>
<p><strong>SOCRATES:</strong> There is a man who knows and does not know that he knows. Put him to teaching and he will tell his pupils &ldquo;run since you are boys&rdquo;, and he will have taught nothing. And Ikkos, who may never have thrown an opponent, has watched ten thousand men fall and can name the cause of every fall. Which of the two will form the champion?</p>
<p><strong>GLAUCON:</strong> Ikkos, it seems. Then it is settled: whoever teaches need not excel at what he teaches.</p>
<p><strong>SOCRATES:</strong> Do not run so fast, Glaucon, or you will stumble like the charioteer at the turn. Answer me: would you agree to learn to swim from a man who has never entered the water?</p>
<p><strong>GLAUCON:</strong> By no means!</p>
<p><strong>SOCRATES:</strong> And why not, if a moment ago we agreed that teaching and doing are distinct arts?</p>
<p><strong>GLAUCON:</strong> Because&hellip; that man would not know the water. He would speak of the sea as one speaks of a country never visited.</p>
<p><strong>SOCRATES:</strong> Very well. Then distinguish with me: it is one thing to demand that the master be the <em>best</em> of practitioners; it is another to demand that he <em>know from the inside</em> what he teaches. Ikkos wrestled, fell, tasted the sand in his mouth. He did not gather crowns, but he gathered the knowledge of wrestling. What he lacks is glory; what he has is the science.</p>
<p><strong>GLAUCON:</strong> But then why do we demand the crowns, Socrates? Why is the first question we ask a master &ldquo;what have you won?&rdquo;, and not &ldquo;what do you understand?&rdquo;.</p>
<p><strong>SOCRATES:</strong> Because crowns can be counted on the fingers, and understanding cannot be counted. The hurried man prefers the proof the eyes verify in an instant to the proof that demands acquaintance and time. The crown is a label hung on the chest, and we have talked before, on another occasion, about how much labels spare us the work of thinking.</p>
<p><strong>GLAUCON:</strong> Then are we wrong to demand practical reputation from those who teach?</p>
<p><strong>SOCRATES:</strong> We are wrong and we are right, Glaucon, and that is what I ask you not to simplify. We are right, because practical reputation is a clue: whoever has done it probably knows it. We are wrong, because we take the clue for proof: whoever has done it may not know how to explain it, and whoever explains it may have known the thing without ever shining at it. The crown proves the man won; it does not prove he knows why he won, and even less that he knows how to make <em>another</em> win.</p>
<p><strong>GLAUCON:</strong> And does the reverse also hold? Does the absence of a crown not prove ignorance?</p>
<p><strong>SOCRATES:</strong> Just as the absence of wealth does not prove laziness, nor the absence of a beard prove youth. But note: it does not prove wisdom either. There are masters without crowns who are like Ikkos, and there are masters without crowns who are like the dry man teaching swimming. The absence of the label only gives back to you the work the label promised to spare: that of examining.</p>
<p><strong>GLAUCON:</strong> And how do I examine, Socrates? If not by the master&rsquo;s victories, then by what?</p>
<p><strong>SOCRATES:</strong> By the fruits that are properly his own. The fruits of the wrestler are the falls he causes; the fruits of the master are the wrestlers he forms. Ask the master not &ldquo;what have you won?&rdquo;, but &ldquo;who has learned from you, and what did they become?&rdquo;. And ask yourself, as you listen to him: &ldquo;do I understand more now than before?&rdquo;. If the answer is yes, you have a master before you, whether he has a hundred crowns or none.</p>
<p><strong>GLAUCON:</strong> Still, something in me resists. When the master also performs with excellence, is he not better?</p>
<p><strong>SOCRATES:</strong> Perhaps he is rarer, Glaucon, as rare as a horse that is both beautiful and fast. When you find a man who does with excellence and explains with clarity, honor him, for nature has been generous twice. But do not make that double generosity the measure of all: you would demand of every horse that it be the most beautiful <em>and</em> the fastest, and you would be left on foot.</p>
<p><strong>GLAUCON:</strong> So, in the end, is it right or wrong to demand of the one who teaches the reputation of the one who performs?</p>
<p><strong>SOCRATES:</strong> That scale, Glaucon, is not mine to carry; it is carried by each person who chooses whom to trust with their learning. I only show you the weights: on one side, the honest clue that practice gives; on the other, the injustice of measuring one art with the ruler of another. <strong>Weigh them yourself, with every master you meet, and distrust whoever sells you the ready answer, including me, who never won a single bout and spend my days teaching in the square</strong>.</p>
<p>..then Glaucon fell silent, and Socrates smiled at his own example..</p>
<h2>Glossary<span class="hx:absolute hx:-mt-20" id="glossary"></span>
    <a href="#glossary" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><em>palaestra</em>: a school of wrestling and physical exercise in ancient Greece</li>
<li><em>charioteer</em>: the driver of a horse-drawn racing chariot</li>
<li><em>Olympia</em>: the sanctuary where the ancient Olympic Games were held</li>
<li><em>Ikkos</em>: inspired by Ikkos of Tarentum, athlete and later a celebrated Greek trainer</li>
</ul>
<h2>Note<span class="hx:absolute hx:-mt-20" id="note"></span>
    <a href="#note" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Does the one who teaches need to master, in practice, what they teach? The answer seems obvious, but maybe it is not. We tend to demand from those who teach the résumé of those who perform, as if the ability to explain were an inevitable consequence of experience. Yet there are those who never reached the greatest feats and still teach with clarity and depth. Likewise, there are extraordinary practitioners unable to transmit what they do.</p>
<p>Perhaps practical experience is a strong indication of competence, but not definitive proof of the ability to teach. And the absence of that experience is not enough, by itself, to invalidate a good master. Between practice and teaching there is a relationship, but not an identity. The question remains open: what, after all, qualifies someone to teach? Maybe the only certainty is the Socratic lesson that clues are not enough and that examining each case remains work without shortcuts.</p>
]]></content:encoded><category>philosophy</category><category>dialogues</category></item><item><title>Old Traps, New Victims</title><link>https://apolzek.github.io/en/2026/07/04/old-traps-new-victims/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/07/04/old-traps-new-victims/</guid><pubDate>Sat, 04 Jul 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;In a world where there is an excess of information, the biggest challenge is no longer finding content, but &lt;strong&gt;separating what really adds value from what merely holds your attention&lt;/strong&gt;. In this article I share practical strategies to avoid losing hours on videos and materials that look educational but deliver little actual knowledge, a very common situation on YouTube, which today is one of my main sources of study.&lt;/p&gt;
&lt;h2&gt;No padding&lt;span class="hx:absolute hx:-mt-20" id="no-padding"&gt;&lt;/span&gt;
&lt;a href="#no-padding" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Watch out for an increasingly common trap in the videos you watch: calculated stalling. It even has technical names. Padding is inflating a video with unnecessary content just to increase its length. Burying the lede is hiding the main information until the end, making you consume a lot of content before getting to what really matters. Watch time baiting is holding your attention for as long as possible to boost retention metrics. Pay attention to the signs: promises constantly postponed, unnecessary recaps and the feeling that the answer is always &amp;ldquo;just ahead&amp;rdquo;. When you notice this, remember: the video was not made to inform you, but to maximize the time you spend watching. &lt;strong&gt;Your time is valuable&lt;/strong&gt;. Decide quickly whether it is worth continuing. If the content shows value, consume it in full. If you notice you are merely being retained, leave without guilt.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In a world where there is an excess of information, the biggest challenge is no longer finding content, but <strong>separating what really adds value from what merely holds your attention</strong>. In this article I share practical strategies to avoid losing hours on videos and materials that look educational but deliver little actual knowledge, a very common situation on YouTube, which today is one of my main sources of study.</p>
<h2>No padding<span class="hx:absolute hx:-mt-20" id="no-padding"></span>
    <a href="#no-padding" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Watch out for an increasingly common trap in the videos you watch: calculated stalling. It even has technical names. Padding is inflating a video with unnecessary content just to increase its length. Burying the lede is hiding the main information until the end, making you consume a lot of content before getting to what really matters. Watch time baiting is holding your attention for as long as possible to boost retention metrics. Pay attention to the signs: promises constantly postponed, unnecessary recaps and the feeling that the answer is always &ldquo;just ahead&rdquo;. When you notice this, remember: the video was not made to inform you, but to maximize the time you spend watching. <strong>Your time is valuable</strong>. Decide quickly whether it is worth continuing. If the content shows value, consume it in full. If you notice you are merely being retained, leave without guilt.</p>
<h2>Clickbait<span class="hx:absolute hx:-mt-20" id="clickbait"></span>
    <a href="#clickbait" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Be careful with titles made to exploit your curiosity and your fear of missing out. Phrases like &ldquo;you need to know this&rdquo;, &ldquo;without this you will never make it&rdquo;, &ldquo;the mistake 99% of people make&rdquo; or &ldquo;nobody is telling you this&rdquo; usually appeal more to emotion than to content. Good content clearly explains what you will learn. Clickbait makes vague, exaggerated or urgent promises to win your click before winning your trust. Before opening a video, ask yourself: does the title describe concrete information, or does it only stir anxiety and curiosity? If the promise is too generic, treat it with skepticism. Your click and your attention have value. Spend them only on content that delivers what it promises.</p>
<h2>I think..<span class="hx:absolute hx:-mt-20" id="i-think"></span>
    <a href="#i-think" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Be careful with content that presents personal opinions as if they were facts. Not every creator is transmitting knowledge; often they are sharing their own worldview, experiences and beliefs without making that explicit. Knowledge can be verified, questioned and compared against evidence. Opinions reflect individual interpretations and may vary from person to person. When that difference is not clear, it is easy to absorb a particular perspective as if it were a universal truth. Ask yourself: is this claim supported by data, evidence or reliable sources, or is it just the author&rsquo;s conclusion? Developing this habit helps you learn more critically and build your own conclusions, instead of merely adopting someone else&rsquo;s.</p>
<h2>Perfect product (:<span class="hx:absolute hx:-mt-20" id="perfect-product-"></span>
    <a href="#perfect-product-" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Remember that presentation is not content. Flawless production, animations, flashy slides, fast-paced editing and grandiose promises can make a message more convincing, but they do not make it truer or more useful. Evaluate first what is being said, not how it is being presented. Form may catch your attention; content is what should justify your time and your trust.</p>
<h2>YouTube sophists<span class="hx:absolute hx:-mt-20" id="youtube-sophists"></span>
    <a href="#youtube-sophists" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>use flashy titles and thumbnails to look like experts;</li>
<li>speak with great confidence, even when the content is shallow;</li>
<li>prioritize audience retention and engagement over depth;</li>
<li>sell the feeling of learning, but deliver little practical knowledge.</li>
</ul>
]]></content:encoded><category>study</category><category>productivity</category></item><item><title>The 20 Most Valuable Companies in the World (June 2026)</title><link>https://apolzek.github.io/en/2026/06/06/most-valuable-companies-top20/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/06/06/most-valuable-companies-top20/</guid><pubDate>Sat, 06 Jun 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;A snapshot in time..&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Category (what it does)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;NVIDIA&lt;/td&gt;
&lt;td&gt;~US$ 5,0 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#chips&lt;/code&gt; &lt;code&gt;#AI&lt;/code&gt; &lt;code&gt;#GPU&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Alphabet (Google)&lt;/td&gt;
&lt;td&gt;~US$ 4,4 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#search&lt;/code&gt; &lt;code&gt;#advertising&lt;/code&gt; &lt;code&gt;#AI&lt;/code&gt; &lt;code&gt;#cloud&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Apple&lt;/td&gt;
&lt;td&gt;~US$ 4,3 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#electronics&lt;/code&gt; &lt;code&gt;#iPhone&lt;/code&gt; &lt;code&gt;#services&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;~US$ 2,9 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#software&lt;/code&gt; &lt;code&gt;#cloud&lt;/code&gt; &lt;code&gt;#AI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;SpaceX&lt;/td&gt;
&lt;td&gt;~US$ 2,65 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#aerospace&lt;/code&gt; &lt;code&gt;#satellites&lt;/code&gt; &lt;code&gt;#AI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;~US$ 2,59 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ecommerce&lt;/code&gt; &lt;code&gt;#cloud&lt;/code&gt; &lt;code&gt;#logistics&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;TSMC&lt;/td&gt;
&lt;td&gt;~US$ 2,2 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#chips&lt;/code&gt; &lt;code&gt;#semiconductors&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Broadcom&lt;/td&gt;
&lt;td&gt;~US$ 1,86 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#chips&lt;/code&gt; &lt;code&gt;#AI&lt;/code&gt; &lt;code&gt;#semiconductors&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Saudi Aramco&lt;/td&gt;
&lt;td&gt;~US$ 1,7 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#oil&lt;/code&gt; &lt;code&gt;#energy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Meta&lt;/td&gt;
&lt;td&gt;~US$ 1,45 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#socialmedia&lt;/code&gt; &lt;code&gt;#advertising&lt;/code&gt; &lt;code&gt;#AI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Tesla&lt;/td&gt;
&lt;td&gt;~US$ 1,44 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#electricvehicles&lt;/code&gt; &lt;code&gt;#energy&lt;/code&gt; &lt;code&gt;#AI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Micron&lt;/td&gt;
&lt;td&gt;~US$ 1,12 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#chips&lt;/code&gt; &lt;code&gt;#memory&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Berkshire Hathaway&lt;/td&gt;
&lt;td&gt;~US$ 1,06 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#investing&lt;/code&gt; &lt;code&gt;#insurance&lt;/code&gt; &lt;code&gt;#holding&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Eli Lilly&lt;/td&gt;
&lt;td&gt;~US$ 1,02 tri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#pharma&lt;/code&gt; &lt;code&gt;#healthcare&lt;/code&gt; &lt;code&gt;#weightloss&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Walmart&lt;/td&gt;
&lt;td&gt;~US$ 968 bi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#retail&lt;/code&gt; &lt;code&gt;#grocery&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;JPMorgan Chase&lt;/td&gt;
&lt;td&gt;~US$ 896 bi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#banking&lt;/code&gt; &lt;code&gt;#finance&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;AMD&lt;/td&gt;
&lt;td&gt;~US$ 844 bi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#chips&lt;/code&gt; &lt;code&gt;#processors&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;ASML&lt;/td&gt;
&lt;td&gt;~US$ 719 bi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#lithography&lt;/code&gt; &lt;code&gt;#equipment&lt;/code&gt; &lt;code&gt;#chips&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;Intel&lt;/td&gt;
&lt;td&gt;~US$ 633 bi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#chips&lt;/code&gt; &lt;code&gt;#processors&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Visa&lt;/td&gt;
&lt;td&gt;~US$ 623 bi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#payments&lt;/code&gt; &lt;code&gt;#finance&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;A company&amp;rsquo;s market value is less an absolute truth and more a snapshot of the market&amp;rsquo;s perception at a given moment.&lt;/p&gt;</description><content:encoded><![CDATA[<p>A snapshot in time..</p>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>Company</th>
          <th>Value</th>
          <th>Category (what it does)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>NVIDIA</td>
          <td>~US$ 5,0 tri</td>
          <td><code>#chips</code> <code>#AI</code> <code>#GPU</code></td>
      </tr>
      <tr>
          <td>2</td>
          <td>Alphabet (Google)</td>
          <td>~US$ 4,4 tri</td>
          <td><code>#search</code> <code>#advertising</code> <code>#AI</code> <code>#cloud</code></td>
      </tr>
      <tr>
          <td>3</td>
          <td>Apple</td>
          <td>~US$ 4,3 tri</td>
          <td><code>#electronics</code> <code>#iPhone</code> <code>#services</code></td>
      </tr>
      <tr>
          <td>4</td>
          <td>Microsoft</td>
          <td>~US$ 2,9 tri</td>
          <td><code>#software</code> <code>#cloud</code> <code>#AI</code></td>
      </tr>
      <tr>
          <td>5</td>
          <td>SpaceX</td>
          <td>~US$ 2,65 tri</td>
          <td><code>#aerospace</code> <code>#satellites</code> <code>#AI</code></td>
      </tr>
      <tr>
          <td>6</td>
          <td>Amazon</td>
          <td>~US$ 2,59 tri</td>
          <td><code>#ecommerce</code> <code>#cloud</code> <code>#logistics</code></td>
      </tr>
      <tr>
          <td>7</td>
          <td>TSMC</td>
          <td>~US$ 2,2 tri</td>
          <td><code>#chips</code> <code>#semiconductors</code></td>
      </tr>
      <tr>
          <td>8</td>
          <td>Broadcom</td>
          <td>~US$ 1,86 tri</td>
          <td><code>#chips</code> <code>#AI</code> <code>#semiconductors</code></td>
      </tr>
      <tr>
          <td>9</td>
          <td>Saudi Aramco</td>
          <td>~US$ 1,7 tri</td>
          <td><code>#oil</code> <code>#energy</code></td>
      </tr>
      <tr>
          <td>10</td>
          <td>Meta</td>
          <td>~US$ 1,45 tri</td>
          <td><code>#socialmedia</code> <code>#advertising</code> <code>#AI</code></td>
      </tr>
      <tr>
          <td>11</td>
          <td>Tesla</td>
          <td>~US$ 1,44 tri</td>
          <td><code>#electricvehicles</code> <code>#energy</code> <code>#AI</code></td>
      </tr>
      <tr>
          <td>12</td>
          <td>Micron</td>
          <td>~US$ 1,12 tri</td>
          <td><code>#chips</code> <code>#memory</code></td>
      </tr>
      <tr>
          <td>13</td>
          <td>Berkshire Hathaway</td>
          <td>~US$ 1,06 tri</td>
          <td><code>#investing</code> <code>#insurance</code> <code>#holding</code></td>
      </tr>
      <tr>
          <td>14</td>
          <td>Eli Lilly</td>
          <td>~US$ 1,02 tri</td>
          <td><code>#pharma</code> <code>#healthcare</code> <code>#weightloss</code></td>
      </tr>
      <tr>
          <td>15</td>
          <td>Walmart</td>
          <td>~US$ 968 bi</td>
          <td><code>#retail</code> <code>#grocery</code></td>
      </tr>
      <tr>
          <td>16</td>
          <td>JPMorgan Chase</td>
          <td>~US$ 896 bi</td>
          <td><code>#banking</code> <code>#finance</code></td>
      </tr>
      <tr>
          <td>17</td>
          <td>AMD</td>
          <td>~US$ 844 bi</td>
          <td><code>#chips</code> <code>#processors</code></td>
      </tr>
      <tr>
          <td>18</td>
          <td>ASML</td>
          <td>~US$ 719 bi</td>
          <td><code>#lithography</code> <code>#equipment</code> <code>#chips</code></td>
      </tr>
      <tr>
          <td>19</td>
          <td>Intel</td>
          <td>~US$ 633 bi</td>
          <td><code>#chips</code> <code>#processors</code></td>
      </tr>
      <tr>
          <td>20</td>
          <td>Visa</td>
          <td>~US$ 623 bi</td>
          <td><code>#payments</code> <code>#finance</code></td>
      </tr>
  </tbody>
</table>
<blockquote>
  <p>A company&rsquo;s market value is less an absolute truth and more a snapshot of the market&rsquo;s perception at a given moment.</p>

</blockquote>
]]></content:encoded><category>business</category><category>economics</category></item><item><title>Study as a Path to Liberation</title><link>https://apolzek.github.io/en/2026/05/09/study-as-a-path-to-liberation/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/05/09/study-as-a-path-to-liberation/</guid><pubDate>Sat, 09 May 2026 12:00:00 GMT</pubDate><description>&lt;h2&gt;Reflections of a former terrible student&lt;span class="hx:absolute hx:-mt-20" id="reflections-of-a-former-terrible-student"&gt;&lt;/span&gt;
&lt;a href="#reflections-of-a-former-terrible-student" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;When someone decides to seriously study a new subject, they almost always experience a kind of intellectual anxiety. It is an understandable phenomenon. As the desire to know grows, so does the awareness of one&amp;rsquo;s own ignorance. The more you see, the more you notice what still remains hidden. &amp;ldquo;I know that I know nothing&amp;rdquo; — Socrates.&lt;/p&gt;
&lt;p&gt;At that moment a problem appears that did not exist before: the limitation of time. It is no longer enough to want to learn; it becomes necessary to discern what deserves depth and what can remain in the realm of general information. Intellectual maturity consists precisely in this: knowing how far one needs to go and where it is wise to stop.&lt;/p&gt;</description><content:encoded><![CDATA[<h2>Reflections of a former terrible student<span class="hx:absolute hx:-mt-20" id="reflections-of-a-former-terrible-student"></span>
    <a href="#reflections-of-a-former-terrible-student" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When someone decides to seriously study a new subject, they almost always experience a kind of intellectual anxiety. It is an understandable phenomenon. As the desire to know grows, so does the awareness of one&rsquo;s own ignorance. The more you see, the more you notice what still remains hidden. &ldquo;I know that I know nothing&rdquo; — Socrates.</p>
<p>At that moment a problem appears that did not exist before: the limitation of time. It is no longer enough to want to learn; it becomes necessary to discern what deserves depth and what can remain in the realm of general information. Intellectual maturity consists precisely in this: knowing how far one needs to go and where it is wise to stop.</p>
<p>Looking at reality, one notices that there are, broadly speaking, three kinds of scholars. The first is the shallow generalist: knows a little about many things, but rarely reaches the essence of any of them. The second is the narrow specialist: masters a few subjects deeply, sacrificing breadth in favor of depth. The third combines both qualities: possesses vast culture and, at the same time, deep knowledge of the subjects studied. It is the rarest kind, because it demands not only intelligence, but discipline, time and an extraordinary capacity to order one&rsquo;s own thinking.</p>
<p>The problem is that this third ideal, admirable as it is, becomes practically unattainable within the average lifespan of a human being, above all for those who have genuine curiosity about many areas of knowledge. An honest look at reality is enough to see that there is not enough time to absorb everything humanity has already produced, let alone keep up with what it keeps producing.</p>
<p>This is where the first great intellectual challenge appears: delimitation. Drawing a line is not an act of resignation, but of prudence. Without that delimitation, the student risks living permanently haunted by a sense of insufficiency, as if always behind in the face of the immensity of available knowledge. Clearly defined goals do not impoverish the intellectual search; on the contrary, they give it direction and meaning.</p>
<p>There is, however, a second problem just as important. Humanity has produced and keeps producing a colossal amount of content on practically every imaginable subject. Yet quantity has never been a synonym for quality. A large part of that production is born of naivety, lack of rigor, insufficient information or, worse still, the replacement of facts by opinions dressed up as knowledge. Not every author deserves trust. Not every work deserves attention.</p>
<p>That is why two virtues become indispensable to the student: the capacity to delimit one&rsquo;s own field of investigation and the capacity to carefully select one&rsquo;s sources. Before starting a journey of study, set aside some time to determine what you really need to learn and what level of mastery is necessary to reach your goal. When you reach it, consider the goal met. Otherwise, study turns into an endless search, incapable of producing any legitimate feeling of completion.</p>
<p>Likewise, set aside time to identify authors, teachers and thinkers who have withstood the judgment of time and whose reputation was built on intellectual seriousness. Prefer those who show prudence when evaluating their predecessors, who know the tradition they speak from and who have the honesty to clearly distinguish between what they know, what they believe and what they merely suppose. <strong>In matters of study, choosing the right guides is often more important than choosing the right subjects.</strong></p>
<p>Everyone talks about the importance of having a solid base of knowledge, but few really understand what that means. When you study a subject and feel you are not understanding anything, or when you open a book and within the first pages find it excessively difficult or tedious, maybe the problem is neither the subject nor the book. Often, some earlier foundation is missing that would make the experience intelligible.</p>
<p>Try studying Differential and Integral Calculus without first mastering basic algebra. I lived that experience and I can say with confidence: it does not work. <strong>The intellect does not advance by arbitrary leaps.</strong> Knowledge has a structure, and each step presupposes the previous ones.</p>
<p>It took me a long time to understand the importance of logic, language, basic mathematics, epistemology, the scientific method, statistics and the history of ideas. For years I thought: &ldquo;What am I ever going to need this for?&rdquo;. And, in a certain sense, I was right. In the job market and in conventional everyday life, much of that knowledge seems dispensable. In most professions, it is perfectly possible to live without it.</p>
<p>But there is a difference between what is useful for earning a living and what is necessary for understanding life. For an existence guided by self-knowledge and the comprehension of reality, those studies are indispensable foundations. They are instruments of intellectual liberation.</p>
<p>It is worth asking yourself: does what I am studying have a relationship with who I am, or only with where I work?? Is it tied to my formation as a human being, or only to the next professional goal I want to reach?</p>
<p>That is a hard decision. What will you learn to develop as a person? And what will you learn to acquire practical skills, increase your economic usefulness and become more valued in the job market? In the end, both dimensions matter. I believe the mistake lies in completely sacrificing one of them in favor of the other.</p>
<p>I would like to close with a reflection..</p>
<p>How many people in your circle still carry the informal title of &ldquo;healers&rdquo;, keepers of knowledge passed down from generation to generation? How many will exist a hundred years from now? After answering in your head, leave the house, walk two blocks and look at the reality around you. You will most likely find a drugstore. They are everywhere: on corners, avenues, shopping centers and residential neighborhoods, a few minutes away for most of the population.</p>
<p>This is not about rejecting the advances of medicine nor idealizing the past. The question is another: we are becoming more and more dependent on systems we understand less and less. We have lost touch with traditions, local knowledge and forms of understanding that for centuries were part of human experience.</p>
<p>I have the impression that we are forming individuals ever more specialized in operating mechanisms and less capable of understanding themselves. Knowledge grows in volume, but wisdom seems to become rarer.</p>
<p>I know.. this text started out talking about knowledge and ended with my indignation :<strong>/</strong> . Maybe because, deep down, knowledge is not just accumulating information, but understanding the place we occupy in the world and the direction we are walking in.</p>
<p>Finally, I remember a sentence I once heard from a cardiologist who has since passed away:</p>
<p>&ldquo;Only knowledge frees man.&rdquo; — E.C.</p>
<hr>
<h2>Knowledge vs. Intelligence vs. Wisdom<span class="hx:absolute hx:-mt-20" id="knowledge-vs-intelligence-vs-wisdom"></span>
    <a href="#knowledge-vs-intelligence-vs-wisdom" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>Knowledge</strong> is the set of information, concepts and facts a person has acquired through study, observation or experience. It is what someone knows. A doctor knows anatomy, physiology and pharmacology because they studied those disciplines; a mechanic knows how engines work because they learned their structure and operation. Knowledge mainly answers the question: &ldquo;what is it?&rdquo;.</p>
<p><strong>Intelligence</strong> is the capacity to understand, relate, analyze and use knowledge to solve problems or deal with new situations. Two people may have the same amount of knowledge, yet show different levels of intelligence when applying it. A student who learns the formulas of physics has knowledge; one who can use them to solve a problem never seen before shows intelligence. Intelligence answers the question: &ldquo;how do I use what I know?&rdquo;.</p>
<p><strong>Wisdom</strong> is the capacity to correctly judge the value, the limits and the consequences of knowledge and intelligence in real life. It is a virtue tied to prudence, discernment and the understanding of human nature. An investor may have knowledge of economics and the intelligence to identify financial opportunities, but only wisdom will let them notice when they already have enough, when the risk is excessive or when the pursuit of money is sacrificing more important aspects of life. Wisdom answers the question: &ldquo;is it worth doing, and what will the consequences be?&rdquo;.</p>
<p>In simple terms, knowledge is knowing that fire burns; intelligence is understanding how to produce, control or use fire; wisdom is knowing when to light it, when to put it out and when to keep away from it. Knowledge accumulates information, intelligence turns information into solutions, and wisdom steers those solutions toward truly good and fitting ends.</p>
<p>This distinction appears, in different formulations, in authors such as Aristotle, who differentiated science, craft and prudence, and also in modern thinkers of epistemology and cognitive psychology. In every serious tradition of thought, the conclusion is similar: <em>it is possible to have a lot of knowledge without being intelligent, to be intelligent without being wise, but it is impossible to be truly wise without some degree of knowledge and intelligence. Wisdom does not replace the two; it orders them.</em></p>
<hr>
<h2>Being a polymath<span class="hx:absolute hx:-mt-20" id="being-a-polymath"></span>
    <a href="#being-a-polymath" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A <strong>polymath</strong> is someone whose mind transcends the artificial borders between fields of knowledge, identifying the principles, patterns and structures that connect distinct areas into a unified understanding of reality. Rather than merely accumulating information, the polymath synthesizes, reconstructs and integrates knowledge, turning learning into systemic perception. Their cognition does not operate linearly or in compartments, but as a dynamic network of connections, in which science, philosophy, psychology, politics, finance and human experience reveal different expressions of the same fundamental truths.</p>
]]></content:encoded><category>philosophy</category><category>study</category></item><item><title>How to set up a Kubernetes cluster to host your static website</title><link>https://apolzek.github.io/en/2026/05/02/kubernetes-static-website/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/05/02/kubernetes-static-website/</guid><pubDate>Sat, 02 May 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;&lt;strong&gt;don&amp;rsquo;t do it&lt;/strong&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>don&rsquo;t do it</strong></p>
]]></content:encoded><category>kubernetes</category><category>humor</category></item><item><title>Dialogue on Labels and Truth</title><link>https://apolzek.github.io/en/2026/04/11/dialogue-on-labels-and-truth/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/04/11/dialogue-on-labels-and-truth/</guid><pubDate>Sat, 11 Apr 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;&lt;em&gt;Setting: Socrates meets Glaucon under the portico of a square, where the latter is examining, irritated, a tablet covered in inscriptions.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SOCRATES:&lt;/strong&gt; Glaucon, I see you frowning. What troubles you?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GLAUCON:&lt;/strong&gt; I have discovered, Socrates, that a poet whose work I dearly love holds political opinions I find repugnant. And another, whose ideas I share as a brother would, writes dreadful verses. I feel I must choose one and renounce the other.&lt;/p&gt;</description><content:encoded><![CDATA[<p><em>Setting: Socrates meets Glaucon under the portico of a square, where the latter is examining, irritated, a tablet covered in inscriptions.</em></p>
<p><strong>SOCRATES:</strong> Glaucon, I see you frowning. What troubles you?</p>
<p><strong>GLAUCON:</strong> I have discovered, Socrates, that a poet whose work I dearly love holds political opinions I find repugnant. And another, whose ideas I share as a brother would, writes dreadful verses. I feel I must choose one and renounce the other.</p>
<p><strong>SOCRATES:</strong> Tell me: when you eat from a fig tree, do you judge the fruit by its taste, or by the opinions of the farmer?</p>
<p><strong>GLAUCON:</strong> By its taste.</p>
<p><strong>SOCRATES:</strong> And if the farmer is a wicked man, but his figs are sweet, will you stop eating them?</p>
<p><strong>GLAUCON:</strong> No, for the fig has nothing to do with the man&rsquo;s wickedness.</p>
<p><strong>SOCRATES:</strong> Then does the work, once made, live on its own, or does it depend at every instant on whoever made it?</p>
<p><strong>GLAUCON:</strong> It lives on its own.</p>
<p><strong>SOCRATES:</strong> So we may judge the fig by what it is, without being obliged to praise the farmer in everything. And conversely: the fact that a man is virtuous will not make a bitter fig sweet. Mix these things up, Glaucon, and you lose twice: you lose the taste of good figs, and you lose the clarity to judge the man.</p>
<p><strong>GLAUCON:</strong> I understand. But there is yet another thing on this tablet that bothers me. It is written about me: &ldquo;Glaucon is a fool.&rdquo;</p>
<p><strong>SOCRATES:</strong> Just that?</p>
<p><strong>GLAUCON:</strong> Just that.</p>
<p><strong>SOCRATES:</strong> And if I said to you: &ldquo;Glaucon, you are a fool because, calculating the price of three amphorae of oil, you multiplied by two and lost a drachma,&rdquo; what would you do?</p>
<p><strong>GLAUCON:</strong> I would check whether it is true, and, if it were, I would correct the error.</p>
<p><strong>SOCRATES:</strong> And if I merely said &ldquo;you are a fool,&rdquo; and nothing more?</p>
<p><strong>GLAUCON:</strong> I would not know what to correct, nor whether there is anything to correct.</p>
<p><strong>SOCRATES:</strong> So one statement hands you a reason, a path. The other hands you nothing but a thrown stone. Tell me: would you entrust your horse to a man who holds out his hand for the reins without saying who he is?</p>
<p><strong>GLAUCON:</strong> Never.</p>
<p><strong>SOCRATES:</strong> And your judgment of yourself, a thing far more valuable than a horse, will you hand it to whoever labels you without saying why?</p>
<p><strong>GLAUCON:</strong> I would be more of a fool than he accuses me of being.</p>
<p><strong>SOCRATES:</strong> Then keep this rule: the accusation without a reason is chaff in the wind, ignore it. The accusation with a reason is a seed that may sprout in you, examine it. But mark this well, Glaucon: no one can examine for you. Not I, not the author of the tablet, not the crowd in the square.</p>
<p><strong>GLAUCON:</strong> How so, Socrates?</p>
<p><strong>SOCRATES:</strong> I mean that the work of weighing reasons, accepting what holds and discarding what does not, is yours and yours alone. And this is not only true of men, Glaucon. It is true of everything you encounter: a law imposed on you, advice you receive, a decision you must make in your house or in your city. In all of them, someone will want to think for you.</p>
<p><strong>GLAUCON:</strong> And if I err by deciding alone?</p>
<p><strong>SOCRATES:</strong> You will err. And that is what distinguishes the free man from the slave: the free man carries the consequences of what he concludes. If you decide well, you will reap. If you decide badly, you will lose. But in either case you will have learned something no one can learn for you.</p>
<p><strong>GLAUCON:</strong> And the one who prefers that another decide in his place?</p>
<p><strong>SOCRATES:</strong> He lives a borrowed life. When something goes right, the merit will not be his. When something goes wrong, he will blame the other, and learn nothing, for the error was not properly his own. It is the worst of conditions, Glaucon: that of the man who never fails because he never dared to judge.</p>
<p><strong>GLAUCON:</strong> Then freedom comes at the price of responsibility.</p>
<p><strong>SOCRATES:</strong> That is its price, and also its dignity. Examine every work, every accusation, every situation. Always ask for the reasons. Decide. And when the consequence comes, sweet or bitter, receive it as your own. <strong>It is the only road to truly becoming someone</strong>.</p>
<p>..then Socrates straightened his cloak, Glaucon turned the tablet to face the wall, and the two went for a beer.</p>
<h2>Glossary<span class="hx:absolute hx:-mt-20" id="glossary"></span>
    <a href="#glossary" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><em>amphorae</em>: ancient vessels used to store liquids such as wine and oil</li>
<li><em>drachma</em>: a coin used in ancient Greece</li>
<li><em>portico</em>: a columned entrance at the front of a building</li>
</ul>
<h2>Note<span class="hx:absolute hx:-mt-20" id="note"></span>
    <a href="#note" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Reading the Greek texts means not treating them as mere discursive constructions, but as records of an effective perception of reality, guided by the attempt to grasp the essence and inner workings of the objects of nature.</p>
<p>When I read <strong>Plato&rsquo;s Republic</strong>, I knew I did not have, and perhaps still do not have, enough grounding to make the most of it. Even so, I felt it was a reading worth doing. I want to reread it soon, precisely to see how the experience of a work changes as we mature as readers.</p>
]]></content:encoded><category>philosophy</category><category>dialogues</category></item><item><title>starting here</title><link>https://apolzek.github.io/en/2026/04/04/apolzek/</link><guid isPermaLink="true">https://apolzek.github.io/en/2026/04/04/apolzek/</guid><pubDate>Sat, 04 Apr 2026 12:00:00 GMT</pubDate><description>&lt;p&gt;Blog reset, May 18, 2026&lt;/p&gt;</description><content:encoded>&lt;p>Blog reset, May 18, 2026&lt;/p>
</content:encoded><category>blog</category></item><item><title>Thoughts</title><link>https://apolzek.github.io/en/thoughts/</link><guid isPermaLink="true">https://apolzek.github.io/en/thoughts/</guid><pubDate>Mon, 01 Jan 0001 00:00:00 GMT</pubDate><description>&lt;p&gt;Sharing quotes from others and a few thoughts of my own&lt;/p&gt;</description><content:encoded>&lt;p>Sharing quotes from others and a few thoughts of my own&lt;/p>
</content:encoded></item><item><title>Moments</title><link>https://apolzek.github.io/en/moments/</link><guid isPermaLink="true">https://apolzek.github.io/en/moments/</guid><pubDate>Mon, 01 Jan 0001 00:00:00 GMT</pubDate><description>&lt;p&gt;Photos and captions, my internal Instagram&lt;/p&gt;</description><content:encoded>&lt;p>Photos and captions, my internal Instagram&lt;/p>
</content:encoded></item><item><title>Cheatsheet</title><link>https://apolzek.github.io/en/cheatsheet/</link><guid isPermaLink="true">https://apolzek.github.io/en/cheatsheet/</guid><pubDate>Mon, 01 Jan 0001 00:00:00 GMT</pubDate><description>&lt;p&gt;commands and tricks for day to day&lt;/p&gt;</description><content:encoded>&lt;p>commands and tricks for day to day&lt;/p>
</content:encoded></item><item><title>About</title><link>https://apolzek.github.io/en/about/</link><guid isPermaLink="true">https://apolzek.github.io/en/about/</guid><pubDate>Mon, 01 Jan 0001 00:00:00 GMT</pubDate><description>&lt;p&gt;My name is &lt;strong&gt;Vinícius Gomes Batista&lt;/strong&gt;. I started my career as a .NET backend developer, already on the modern versions of the platform. Since then I have had a strong interest in infrastructure and security, and I naturally ended up moving into reliability engineering, where I spent most of my time.&lt;/p&gt;
&lt;p&gt;What motivated me most was the desire for a holistic view, of both the product and the company. I liked understanding the parts and seeing how they connect and interact. That happened naturally and, in the same way, I developed the urge to express the health of systems, their dependencies and their integrations through dashboards, monitoring them and following their evolution. That led me to a second move, into platform engineering, specifically observability platforms, where I spent my last 12 months.&lt;/p&gt;</description><content:encoded><![CDATA[<p>My name is <strong>Vinícius Gomes Batista</strong>. I started my career as a .NET backend developer, already on the modern versions of the platform. Since then I have had a strong interest in infrastructure and security, and I naturally ended up moving into reliability engineering, where I spent most of my time.</p>
<p>What motivated me most was the desire for a holistic view, of both the product and the company. I liked understanding the parts and seeing how they connect and interact. That happened naturally and, in the same way, I developed the urge to express the health of systems, their dependencies and their integrations through dashboards, monitoring them and following their evolution. That led me to a second move, into platform engineering, specifically observability platforms, where I spent my last 12 months.</p>
<p>What you will see on this blog is a mix of what I lived through in that time: mostly lessons that cost me dearly and tips about technical work inside large organizations, which often have rigid processes and an anti-productive culture in the name of stability. But you will also see many philosophical texts, inspired by the works I have been reading and by the thoughts and reflections I have been having. That is part of me, and I would like it to live in the same place.</p>
<p>Along the way, if you read my texts, you will end up getting to know me better.</p>
<p>Every post is available in Portuguese and English; use the PT | EN switch at the top of the page. Besides the posts, there are two more things around here:</p>
<ul>
<li><a href="/en/thoughts/">Thoughts</a>: quotes from others and a few thoughts of my own, with dates.</li>
<li><a href="/en/cheatsheet/">Cheatsheet</a>: day-to-day commands and tricks, with search and tag filter.</li>
</ul>
<p>Find me on <a href="https://github.com/apolzek"target="_blank" rel="noopener">GitHub</a> and <a href="https://x.com/apolzek"target="_blank" rel="noopener">X</a>.</p>
<blockquote>
  <p>&ldquo;The most important fact that sets the human being apart from all its brothers and cousins on the phylogenetic scale is knowledge; only knowledge frees man, only through knowledge is man free and, being free, he can aspire to a better condition of life for himself and all his fellow beings. I can only understand a society in which knowledge is the foremost reason the government gives for the formation of the citizen. My message is a positive one: that man must know, must understand, and in understanding, he is free.&rdquo;</p>
<p>— Dr. Enéas Ferreira Carneiro (Rio Branco, November 5, 1938 — Rio de Janeiro, May 6, 2007)</p>

</blockquote>
]]></content:encoded></item></channel></rss>