<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dev.karakun.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dev.karakun.com/" rel="alternate" type="text/html" /><updated>2026-06-18T08:12:58+00:00</updated><id>https://dev.karakun.com/feed.xml</id><title type="html">Karakun Developer Hub</title><subtitle>The Developer Hub of Karakun AG</subtitle><entry><title type="html">Shipping marimo WASM Notebooks as Browser-Based Engineering Tools with Spring Boot</title><link href="https://dev.karakun.com/2026/06/17/marimo-wasm-exoknox.html" rel="alternate" type="text/html" title="Shipping marimo WASM Notebooks as Browser-Based Engineering Tools with Spring Boot" /><published>2026-06-17T00:00:00+00:00</published><updated>2026-06-17T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/06/17/Marimo-wasm-exoknox</id><content type="html" xml:base="https://dev.karakun.com/2026/06/17/marimo-wasm-exoknox.html"><![CDATA[<p>Interactive engineering workflows often need more than static forms or fully automated pipelines. 
In <a href="https://exoknox.com" target="_blank">EXOKNOX</a>, curve fitting and extrapolation require engineers to compare algorithms, tune parameters, and inspect results before simulation.</p>

<p>This article explains how we integrated marimo WASM notebooks as browser-based Python tools using Pyodide, Spring Boot security, shared Python wheels, and REST API integration.</p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#the-engineering-data-problem">The Engineering Data Problem</a></li>
  <li><a href="#why-marimo-for-browser-based-python-tools">Why marimo for Browser-Based Python Tools</a></li>
  <li><a href="#what-we-built-marimo-wasm-apps-in-spring-boot">What We Built: marimo WASM Apps in Spring Boot</a></li>
  <li><a href="#how-we-built-the-marimo-wasm-deployment">How We Built the marimo WASM Deployment</a></li>
  <li><a href="#trade-offs-and-constraints-of-marimo-wasm">Trade-offs and Constraints of marimo WASM</a></li>
  <li><a href="#benefits-of-browser-based-python-engineering-tools">Benefits of Browser-Based Python Engineering Tools</a></li>
  <li><a href="#conclusion-when-marimo-wasm-fits">Conclusion: When marimo WASM Fits</a></li>
  <li><a href="#cta">Let’s Discuss</a></li>
</ul>

<hr />

<h2 id="-the-engineering-data-problem"><a name="the-engineering-data-problem"></a> The Engineering Data Problem</h2>

<p><a href="https://exoknox.com" target="_blank">EXOKNOX</a> is a platform for managing functional engineering data.
Before simulation, engineers start with measured <a href="https://en.wikipedia.org/wiki/Hysteresis" target="_blank">hysteresis</a> data: repeated loading and unloading curves from physical tests. 
For downstream simulation, these data have to be reduced to a representative single curve and extrapolated beyond the measured force range.</p>

<p>This step cannot be fully automated. 
The correct result depends on engineering judgment: different smoothing, fitting, and extrapolation strategies can produce curves that are mathematically plausible but physically wrong.
Engineers need to compare different algorithms, tune parameters, inspect the result, and repeat until the curve is suitable for simulation.</p>

<p>At the same time, we wanted to move away from EXOKNOX’s Java-based Eclipse RCP frontend.</p>

<p>So the requirement was clear: we needed a lightweight, browser-based Python tool with an interactive UI that could read and write EXOKNOX data.</p>

<h2 id="-why-marimo-for-browser-based-python-tools"><a name="why-marimo-for-browser-based-python-tools"></a> Why marimo for Browser-Based Python Tools</h2>

<p><a href="https://marimo.io" target="_blank">Marimo</a> is a reactive Python notebook framework. 
Unlike Jupyter, marimo notebooks are pure Python files — no JSON, no hidden state. 
Cells are reactive: when a value changes, all dependent cells re-execute automatically. 
It ships a clean web UI and can be deployed either as a running server application or as a <strong>WebAssembly (WASM) application</strong> that executes entirely in the browser via <a href="https://pyodide.org" target="_blank">Pyodide</a>.</p>

<p>The important requirements for us were: notebooks had to be versionable as normal source files, UI state had to be reproducible, and the same notebook had to support fast local development as well as browser-only deployment. 
Marimo fit that better than a traditional Jupyter workflow because the notebook is ordinary Python source and the dependency graph is explicit.
A custom Vue or React UI would have offered more control, but at a much higher implementation cost for exploratory engineering workflows.</p>

<h2 id="-what-we-built-marimo-wasm-apps-in-spring-boot"><a name="what-we-built-marimo-wasm-apps-in-spring-boot"></a> What We Built: marimo WASM Apps in Spring Boot</h2>

<p>Before diving into the challenges, here’s what the final system looks like. The <code class="language-plaintext highlighter-rouge">frontend/scripting</code> module delivers two interactive data-analysis tools — <strong>Curve Editor</strong> and <strong>Load Fitting</strong> — as self-contained Python applications that run entirely in the browser. No Python server is needed at runtime.</p>

<p>The module is organized around two layers:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>frontend/scripting/
├── build.gradle.kts          ← Orchestrates the entire Python + WASM build
├── common/                   ← Shared Python library (wheel)
│   ├── pyproject.toml
│   └── src/common/
│       ├── api/              ← HTTP client to access the backend REST API
│       └── curveprocessing/  ← Curve processing functions
└── marimoapps/
    ├── curveeditor/          ← marimo notebook app
    └── curvefitting/          ← marimo notebook app
</code></pre></div></div>
<p><br />
<strong><code class="language-plaintext highlighter-rouge">common</code></strong> is a plain Python package built as a wheel (<code class="language-plaintext highlighter-rouge">.whl</code>). 
It contains all business logic and is shared across both apps — as an editable <a href="https://github.com/astral-sh/uv" target="_blank">uv</a> workspace dependency during local development and as a pre-built wheel loaded at runtime inside the browser.</p>

<p><strong><code class="language-plaintext highlighter-rouge">marimoapps</code></strong> contains the marimo notebooks. Each app declares <code class="language-plaintext highlighter-rouge">common</code> as a <code class="language-plaintext highlighter-rouge">uv</code> workspace dependency so that during development they share a single source tree. For WASM export, the common wheel is bundled alongside the app and loaded at runtime via <code class="language-plaintext highlighter-rouge">micropip</code>.</p>

<p>The high-level architecture is straightforward:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Browser
  └── marimo WASM app (Python running in Pyodide)
        ├── Fetches functional data via EXOKNOX REST API
        └── Writes results back via EXOKNOX REST API

Spring Boot Server
  ├── Serves marimo notebooks as static resources
  ├── Enforces OIDC authentication
  └── Provides the EXOKNOX REST API
</code></pre></div></div>
<p><br />
There is no marimo server process and no Python runtime on the backend. Just static files, served securely, executing in the client’s browser.</p>

<p>The resulting tools are embedded as web pages in the browser:</p>

<h5 id="curve-editor">Curve Editor</h5>
<p><img src="/assets/posts/2026-06-17-Marimo-wasm-exoknox/curveeditor.png" alt="Curve Editor" /></p>
<h5 id="curve-fitting">Curve Fitting</h5>
<p><img src="/assets/posts/2026-06-17-Marimo-wasm-exoknox/curve-fitting.png" alt="Curve Fitting" /></p>

<p>Getting to this architecture required solving three concrete challenges.</p>

<hr />
<h2 id="-how-we-built-the-marimo-wasm-deployment"><a name="how-we-built-the-marimo-wasm-deployment"></a> How We Built the marimo WASM Deployment</h2>

<p>Moving from proof of concept to production meant solving deployment, API, and development workflow constraints one by one.</p>

<h3 id="challenge-1--secure-static-notebook-deployment-without-a-marimo-server">Challenge 1 — Secure Static Notebook Deployment Without a marimo Server</h3>

<p>The obvious deployment for marimo is as a server: you run <code class="language-plaintext highlighter-rouge">marimo run notebook.py</code> and marimo starts a WebSocket-backed application server that executes Python on the backend. 
We evaluated this and rejected it for two reasons.</p>

<p><strong>Security surface.</strong> 
A running marimo server executes arbitrary Python code from notebooks that are essentially customer property. 
Even sandboxed, this is an attack vector we preferred not to manage.</p>

<p><strong>Infrastructure complexity.</strong> 
For on-premise installations — which many EXOKNOX customers require — spinning up and managing a persistent marimo server (or per-session containers in Kubernetes) places requirements on the customer’s infrastructure that we cannot guarantee.</p>

<p>The WASM approach removes the need to execute notebook Python on the backend. 
That significantly reduces the server-side attack surface, although the browser-side notebook still has to be treated like any authenticated frontend code.
The Python runtime lives in the browser, execution is sandboxed by the browser’s security model, and the server is stateless. 
Backend access is secured by OIDC authentication and managed entirely by the browser.</p>

<h4 id="from-notebook-to-static-webassembly-assets">From Notebook to Static WebAssembly Assets</h4>

<p>marimo can export a notebook as a self-contained WASM application:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>marimo <span class="nb">export </span>html-wasm notebook.py <span class="nt">-o</span> dist/notebook.html <span class="nt">--mode</span> run
</code></pre></div></div>
<p><br />
The output is a static directory with an <code class="language-plaintext highlighter-rouge">index.html</code>, the notebook code, and the assets needed by the marimo WASM runtime. 
We serve this from Spring Boot as static content, protected behind Spring Security’s OAuth2 login flow.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">SecurityFilterChain</span> <span class="nf">securityFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">,</span> 
                                        <span class="nc">GrantedAuthoritiesMapper</span> <span class="n">grantedAuthoritiesMapper</span><span class="o">,</span> 
                                        <span class="nc">OpaqueTokenIntrospector</span> <span class="n">opaqueTokenIntrospector</span><span class="o">,</span> 
                                        <span class="nc">JwtAuthenticationConverter</span> <span class="n">jwtAuthenticationConverter</span><span class="o">,</span> 
                                        <span class="nc">SecurityFilter</span> <span class="n">securityFilter</span><span class="o">)</span> <span class="o">{</span>

    <span class="c1">// takes care of HTTP authorization - authorizationCustomizer secures the protected paths</span>
    <span class="n">http</span><span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">authorizationCustomizer</span><span class="o">);</span>

    <span class="c1">// takes care of login (authentication flow)</span>
    <span class="n">http</span><span class="o">.</span><span class="na">oauth2Login</span><span class="o">(</span><span class="n">oauth2</span> <span class="o">-&gt;</span> <span class="n">oauth2</span><span class="o">.</span><span class="na">userInfoEndpoint</span><span class="o">(</span><span class="n">userInfo</span> <span class="o">-&gt;</span> <span class="n">userInfo</span><span class="o">.</span><span class="na">userAuthoritiesMapper</span><span class="o">(</span><span class="n">grantedAuthoritiesMapper</span><span class="o">)));</span>

    <span class="c1">// takes care of bearer tokens in the HTTP header - resourceServerCustomizer handles opaque and jwt tokens</span>
    <span class="n">http</span><span class="o">.</span><span class="na">oauth2ResourceServer</span><span class="o">(</span><span class="n">oauth2</span> <span class="o">-&gt;</span> <span class="n">resourceServerCustomizer</span><span class="o">(</span><span class="n">oauth2</span><span class="o">,</span> <span class="n">opaqueTokenIntrospector</span><span class="o">,</span> <span class="n">jwtAuthenticationConverter</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<p><br />
The notebook is never accessible without authentication.</p>

<h4 id="wiring-the-build-with-gradle-and-uv">Wiring the Build with Gradle and uv</h4>

<p>We needed the WASM export to happen automatically as part of the standard Gradle build — not as a manual step. 
The <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> uses the community plugin <code class="language-plaintext highlighter-rouge">com.pswidersk.python-uv-plugin</code> to drive <code class="language-plaintext highlighter-rouge">uv</code> commands from Gradle tasks, plus <code class="language-plaintext highlighter-rouge">org.openapi.generator</code> to generate <code class="language-plaintext highlighter-rouge">Pydantic</code> model classes from the backend’s <code class="language-plaintext highlighter-rouge">OpenAPI</code> specs.</p>

<p>The pipeline runs in four phases on every build:</p>

<p><strong>Phase 1 — OpenAPI Code Generation.</strong> 
The backend service modules expose REST APIs defined by <code class="language-plaintext highlighter-rouge">OpenAPI</code> YAML files. 
Gradle scans those specs and runs OpenAPI Generator to produce <code class="language-plaintext highlighter-rouge">Pydantic</code> model classes.
We generate only the model layer, not the transport layer, because the generated clients assume a normal CPython HTTP stack, while the WASM runtime needs browser-based fetch through <code class="language-plaintext highlighter-rouge">pyodide.http</code>.
The generated models are synced into <code class="language-plaintext highlighter-rouge">common/src/exoknox_&lt;name&gt;_client/models/</code>, giving the shared library strongly typed data structures for every backend API response.</p>

<p><strong>Phase 2 — Common Library Build.</strong> 
<code class="language-plaintext highlighter-rouge">uv build --managed-python</code> produces <code class="language-plaintext highlighter-rouge">common/dist/common-0.1.0-py3-none-any.whl</code>. 
This is a pure-Python, platform-neutral artifact that the browser will later fetch and install.</p>

<p><strong>Phase 3 — Per-App WASM Export.</strong> 
For each app discovered by scanning <code class="language-plaintext highlighter-rouge">marimoapps/*/pyproject.toml</code>, Gradle creates a task chain:</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">uvBuild&lt;App&gt;</code></strong> — builds the app package with <code class="language-plaintext highlighter-rouge">uv</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">uvWasm&lt;App&gt;</code></strong> — runs <code class="language-plaintext highlighter-rouge">marimo export html-wasm</code>, which bundles the notebook with the Pyodide Python runtime and produces a self-contained <code class="language-plaintext highlighter-rouge">dist/wasm/</code> directory.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">uvWheelCommon&lt;App&gt;</code></strong> — copies the common wheel into <code class="language-plaintext highlighter-rouge">dist/wasm/public/</code>, making it fetchable by the browser at a relative URL.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">copyWasmApplication&lt;App&gt;</code></strong> and <strong><code class="language-plaintext highlighter-rouge">buildWasm&lt;App&gt;</code></strong> — copy the output to <code class="language-plaintext highlighter-rouge">build/wasm/&lt;app&gt;/</code>.</li>
</ol>

<p>The top-level <code class="language-plaintext highlighter-rouge">buildWasm</code> task aggregates all per-app tasks. 
The standard Gradle <code class="language-plaintext highlighter-rouge">build</code> task depends on <code class="language-plaintext highlighter-rouge">buildWasm</code>, so the full pipeline runs on every build with no extra steps.</p>

<p><strong>Phase 4 — JAR Packaging.</strong> <code class="language-plaintext highlighter-rouge">processResources</code> includes the WASM output in the Spring Boot JAR:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">from</span><span class="p">(</span><span class="s">"marimoapps/$appName/dist/wasm"</span><span class="p">)</span> <span class="nf">into</span><span class="p">(</span><span class="s">"wasm/$appName"</span><span class="p">)</span>
</code></pre></div></div>

<p>Spring Boot then serves these static resources, making the apps accessible at:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/exoknox/marimo/curveeditor/index.html
/exoknox/marimo/loadfitting/index.html
</code></pre></div></div>

<h3 id="challenge-2--rest-api-integration-from-the-browser">Challenge 2 — REST API Integration from the Browser</h3>

<p>A marimo WASM notebook runs entirely in the browser. 
It has no direct access to databases or backend services — it can only make HTTP requests. 
The data access model is exactly the same as any other frontend application. 
In the module we have a directory with Python modules that are bundled as a wheel into the WASM, providing access to the EXOKNOX REST API.</p>

<h4 id="browser-based-http-requests-with-pyodide">Browser-Based HTTP Requests with Pyodide</h4>

<p>We built a Python module that uses Pyodide’s HTTP module to send HTTP requests to the backend (<code class="language-plaintext highlighter-rouge">http_client.py</code>).
The notebook fetches data from the EXOKNOX REST API using the user’s existing browser session. 
In WASM mode, <code class="language-plaintext highlighter-rouge">credentials="include"</code> lets the browser attach the same authenticated session cookies it would use for the rest of the application.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pyodide.http</span> <span class="k">as</span> <span class="n">http</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">get_request</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="n">request</span> <span class="o">=</span> <span class="k">await</span> <span class="n">http</span><span class="p">.</span><span class="n">pyfetch</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">method</span><span class="o">=</span><span class="s">"GET"</span><span class="p">,</span> <span class="n">credentials</span><span class="o">=</span><span class="s">"include"</span><span class="p">)</span>
    <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">request</span><span class="p">.</span><span class="n">string</span><span class="p">()</span>
    <span class="n">status_code</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s">"status"</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>
    <span class="k">if</span> <span class="mi">200</span> <span class="o">&lt;=</span> <span class="n">status_code</span> <span class="o">&lt;</span> <span class="mi">300</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">response</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="sa">f</span><span class="s">"Error fetching </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s"> : </span><span class="si">{</span><span class="n">status_code</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>
<p><br />
The write path mirrors the read path:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">post_request</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">body</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="n">request</span> <span class="o">=</span> <span class="k">await</span> <span class="n">http</span><span class="p">.</span><span class="n">pyfetch</span><span class="p">(</span>
        <span class="n">url</span><span class="p">,</span>
        <span class="n">method</span><span class="o">=</span><span class="s">"POST"</span><span class="p">,</span>
        <span class="n">credentials</span><span class="o">=</span><span class="s">"include"</span><span class="p">,</span>
        <span class="n">body</span><span class="o">=</span><span class="n">body</span><span class="p">,</span>
        <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"Content-Type"</span><span class="p">:</span> <span class="s">"application/json"</span><span class="p">}</span>
    <span class="p">)</span>
    <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">request</span><span class="p">.</span><span class="n">string</span><span class="p">()</span>
    <span class="n">status_code</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s">"status"</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>
    <span class="k">if</span> <span class="mi">200</span> <span class="o">&lt;=</span> <span class="n">status_code</span> <span class="o">&lt;</span> <span class="mi">300</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">response</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="sa">f</span><span class="s">"Error posting to </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s"> : </span><span class="si">{</span><span class="n">status_code</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>
<p><br /></p>
<h4 id="a-typed-rest-api-layer">A Typed REST API Layer</h4>

<p>On top of the raw HTTP calls, <code class="language-plaintext highlighter-rouge">exoknox_api.py</code> provides functions that encapsulate the REST API endpoints, giving notebooks clean, typed access to backend data.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">common.api.http_client</span> <span class="kn">import</span> <span class="n">get_request</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">read_dataset</span><span class="p">(</span><span class="n">dataset_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">base_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ChannelsDTO</span><span class="p">:</span>
    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">base_url</span><span class="si">}</span><span class="s">/channels?dataSetId=</span><span class="si">{</span><span class="n">dataset_id</span><span class="si">}</span><span class="s">"</span>
    <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">get_request</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">ChannelsDTO</span><span class="p">.</span><span class="n">from_json</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</code></pre></div></div>
<p><br />
When the user has completed their analysis — fitted a curve, computed new values, reviewed the result in an interactive chart — a save action posts the result:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">save_dataset</span><span class="p">(</span><span class="n">scripting_request</span><span class="p">:</span> <span class="n">ScriptingResultRequestDTO</span><span class="p">,</span> <span class="n">base_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ScriptingResultResponseDTO</span><span class="p">:</span>
    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">base_url</span><span class="si">}</span><span class="s">/scripting-result"</span>
    <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">post_request</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">scripting_request</span><span class="p">.</span><span class="n">to_json</span><span class="p">())</span>
    <span class="k">return</span> <span class="n">ScriptingResultResponseDTO</span><span class="p">.</span><span class="n">from_json</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</code></pre></div></div>
<p><br /></p>
<h4 id="notebook-integration">Notebook Integration</h4>

<p>Each marimo notebook contains a dedicated cell to load data on startup. 
It reads the dataset ID from URL query parameters, derives the base URL from the notebook’s current location, and hands back either the loaded channels or an error message:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">cell</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">_</span><span class="p">(</span><span class="n">mo</span><span class="p">,</span> <span class="n">exoknox_api</span><span class="p">):</span>
    <span class="n">qp</span> <span class="o">=</span> <span class="n">mo</span><span class="p">.</span><span class="n">query_params</span><span class="p">()</span>
    <span class="n">datasetid</span> <span class="o">=</span> <span class="n">qp</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"datasetid"</span><span class="p">,</span> <span class="s">""</span><span class="p">)</span>

    <span class="n">nb</span> <span class="o">=</span> <span class="n">mo</span><span class="p">.</span><span class="n">notebook_location</span><span class="p">()</span>
    <span class="kn">from</span> <span class="nn">urllib.parse</span> <span class="kn">import</span> <span class="n">urlparse</span>
    <span class="n">parsed</span> <span class="o">=</span> <span class="n">urlparse</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">nb</span><span class="p">))</span>
    <span class="n">base_url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">parsed</span><span class="p">.</span><span class="n">scheme</span><span class="si">}</span><span class="s">://</span><span class="si">{</span><span class="n">parsed</span><span class="p">.</span><span class="n">netloc</span><span class="si">}</span><span class="s">"</span>

    <span class="k">try</span><span class="p">:</span>
        <span class="n">channels</span> <span class="o">=</span> <span class="k">await</span> <span class="n">exoknox_api</span><span class="p">.</span><span class="n">read_dataset</span><span class="p">(</span><span class="n">datasetid</span><span class="p">,</span> <span class="n">base_url</span><span class="p">)</span>
        <span class="n">loading_error_message</span> <span class="o">=</span> <span class="s">""</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">error</span><span class="p">:</span>
        <span class="n">loading_error_message</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"Error loading data: </span><span class="si">{</span><span class="n">error</span><span class="si">}</span><span class="s">"</span>
        <span class="n">channels</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">base_url</span><span class="p">,</span> <span class="n">channels</span><span class="p">,</span> <span class="n">loading_error_message</span><span class="p">)</span>
</code></pre></div></div>
<p><br />
Saving is equally straightforward. 
A save button triggers a cell that posts results back only when the button is actually pressed:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">cell</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">_</span><span class="p">(</span><span class="n">mo</span><span class="p">,</span> <span class="n">exoknox_api</span><span class="p">,</span> <span class="n">base_url</span><span class="p">,</span> <span class="n">save_button</span><span class="p">,</span> <span class="n">datasetid</span><span class="p">,</span> <span class="n">x_fitted</span><span class="p">,</span> <span class="n">y_fitted</span><span class="p">):</span>
    <span class="n">mo</span><span class="p">.</span><span class="n">stop</span><span class="p">(</span><span class="ow">not</span> <span class="n">save_button</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>  <span class="c1"># if button was not pressed, return
</span>    <span class="k">with</span> <span class="n">mo</span><span class="p">.</span><span class="n">status</span><span class="p">.</span><span class="n">spinner</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"Saving Data Set..."</span><span class="p">)</span> <span class="k">as</span> <span class="n">_spinner</span><span class="p">:</span>
        <span class="kn">from</span> <span class="nn">exoknox_scripting_result_client.models</span> <span class="kn">import</span> <span class="n">ScriptingResultRequestDTO</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">exoknox_api</span><span class="p">.</span><span class="n">save_dataset</span><span class="p">(</span>
                <span class="n">base_url</span><span class="o">=</span><span class="n">base_url</span><span class="p">,</span>
                <span class="n">scripting_request</span><span class="o">=</span><span class="n">ScriptingResultRequestDTO</span><span class="p">(</span><span class="n">dataSetId</span><span class="o">=</span><span class="n">datasetid</span><span class="p">,</span> <span class="n">x</span><span class="o">=</span><span class="n">x_fitted</span><span class="p">,</span> <span class="n">y</span><span class="o">=</span><span class="n">y_fitted</span><span class="p">)</span>
            <span class="p">)</span>
            <span class="n">saving_error_message</span> <span class="o">=</span> <span class="s">""</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="n">result</span> <span class="o">=</span> <span class="bp">None</span>
            <span class="n">saving_error_message</span> <span class="o">=</span> <span class="s">"Error saving data"</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">,</span> <span class="n">saving_error_message</span>
</code></pre></div></div>

<h3 id="challenge-3--development-mode-vs-production-wasm-mode">Challenge 3 — Development Mode vs. Production WASM Mode</h3>

<p>This was the most practically fiddly challenge. 
During development, a running marimo server is the right environment: fast feedback, full Python library support, no Pyodide compilation step. 
In production, the notebook runs in WASM under Pyodide.</p>

<p>Start marimo in edit mode with <code class="language-plaintext highlighter-rouge">uv run marimo edit curve_fitting.py</code>. 
In this mode, you build your board interactively: add and edit cells, add UI elements, and see results update immediately. 
Changes propagate automatically to dependent cells, so there’s no manual rerun flow. 
Everything you do is saved instantly to the underlying Python file, making the board both live and persistent at the same time.</p>

<p>But this is not the same environment as the production setup that uses Pyodide. 
These two environments differ in two important ways:</p>

<p><strong>Import availability.</strong> 
Pyodide supports a substantial subset of the scientific Python ecosystem (NumPy, SciPy, Pandas, Matplotlib), but not every library. 
Anything with C extensions that Pyodide has not pre-compiled is unavailable. 
The Python version in each <code class="language-plaintext highlighter-rouge">pyproject.toml</code> must match the Python version provided by the Pyodide runtime used by marimo’s WASM export. 
In our setup that means pinning Python to <code class="language-plaintext highlighter-rouge">==3.12.*</code>, because the prebuilt Pyodide wheels we rely on are built for that runtime.</p>

<p><strong>Available APIs.</strong> 
Browser-based async execution has different constraints from server-side CPython. 
In particular, HTTP calls need to go through browser fetch APIs exposed by Pyodide (<code class="language-plaintext highlighter-rouge">pyodide.http</code>), rather than <code class="language-plaintext highlighter-rouge">requests</code>, <code class="language-plaintext highlighter-rouge">httpx</code> or a normal socket-based client.</p>

<p>Our solution was to isolate the environment-specific code behind a thin detection layer defined at the top of each notebook:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">cell</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">_</span><span class="p">(</span><span class="n">mo</span><span class="p">):</span>
    <span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
    <span class="n">nb</span> <span class="o">=</span> <span class="n">mo</span><span class="p">.</span><span class="n">notebook_location</span><span class="p">()</span>    <span class="c1"># In WASM, this is the URL of the webpage, in non-WASM, this is the directory of the notebook 
</span>    <span class="n">wasm_marimo</span> <span class="o">=</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">nb</span><span class="p">,</span> <span class="n">Path</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">wasm_marimo</span>
</code></pre></div></div>
<p><br />
We then thread <code class="language-plaintext highlighter-rouge">wasm_marimo</code> through to the HTTP client functions. 
In <code class="language-plaintext highlighter-rouge">http_client.py</code>, the flag drives two completely different transport implementations:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">fetch_data</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">wasm_marimo</span><span class="p">:</span> <span class="nb">bool</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="k">if</span> <span class="n">wasm_marimo</span><span class="p">:</span>
        <span class="c1"># In WASM: the browser provides the session cookie to access the server
</span>        <span class="n">request</span> <span class="o">=</span> <span class="k">await</span> <span class="n">http</span><span class="p">.</span><span class="n">pyfetch</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">method</span><span class="o">=</span><span class="s">"GET"</span><span class="p">,</span> <span class="n">credentials</span><span class="o">=</span><span class="s">"include"</span><span class="p">)</span>
        <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">request</span><span class="p">.</span><span class="n">string</span><span class="p">()</span>
        <span class="n">status_code</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s">"status"</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">response</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="sa">f</span><span class="s">"Error fetching </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s"> : </span><span class="si">{</span><span class="n">status_code</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="c1"># Deployed locally: calls need a bearer token to access the backend
</span>        <span class="n">token</span> <span class="o">=</span> <span class="n">_get_access_token</span><span class="p">()</span>  <span class="c1"># login if necessary
</span>
        <span class="kn">import</span> <span class="nn">urllib.error</span>
        <span class="kn">import</span> <span class="nn">urllib.request</span>
        <span class="n">request</span> <span class="o">=</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">Request</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"Authorization"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"Bearer </span><span class="si">{</span><span class="n">token</span><span class="si">}</span><span class="s">"</span><span class="p">})</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">with</span> <span class="n">urllib</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="k">as</span> <span class="n">request</span><span class="p">:</span>
                <span class="n">response</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">read</span><span class="p">().</span><span class="n">decode</span><span class="p">(</span><span class="n">UTF_8</span><span class="p">)</span>
        <span class="k">except</span> <span class="n">urllib</span><span class="p">.</span><span class="n">error</span><span class="p">.</span><span class="n">HTTPError</span> <span class="k">as</span> <span class="n">error</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="sa">f</span><span class="s">"Error fetching </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s"> : </span><span class="si">{</span><span class="n">error</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="k">except</span> <span class="n">urllib</span><span class="p">.</span><span class="n">error</span><span class="p">.</span><span class="n">URLError</span> <span class="k">as</span> <span class="n">error</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="sa">f</span><span class="s">"Error fetching </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s"> : </span><span class="si">{</span><span class="n">error</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">response</span>
</code></pre></div></div>
<p><br />
This pattern adds a modest amount of boilerplate per notebook. 
We accepted it as the cost of a comfortable development experience. 
The alternative — always developing against a local WASM build — would have meant a slow compile cycle on every change.</p>

<h4 id="browser-bootstrap-with-pyodide-and-micropip">Browser Bootstrap with Pyodide and micropip</h4>

<p>When a user opens the app, the browser downloads and instantiates the Pyodide WASM binary. 
The notebook then detects its environment via the <code class="language-plaintext highlighter-rouge">wasm_marimo</code> flag described above. 
If running in WASM, it uses <code class="language-plaintext highlighter-rouge">micropip</code> — Pyodide’s in-browser package manager — to install the common library wheel from the same origin, together with other libraries used by the notebook:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">wasm_marimo</span><span class="p">:</span>
    <span class="n">base_url</span> <span class="o">=</span> <span class="n">mo</span><span class="p">.</span><span class="n">notebook_location</span><span class="p">()</span> 
    <span class="kn">import</span> <span class="nn">micropip</span>
    <span class="n">common_url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">base_url</span><span class="si">}</span><span class="s">/public/common-0.1.0-py3-none-any.whl"</span>
    <span class="k">await</span> <span class="n">micropip</span><span class="p">.</span><span class="n">install</span><span class="p">([</span><span class="n">common_url</span><span class="p">,</span> <span class="s">"plotly"</span><span class="p">,</span> <span class="s">"anywidget"</span><span class="p">])</span>
</code></pre></div></div>
<p><br />
This is the key to the whole architecture — what we call the <strong>wheel-in-public</strong> pattern. 
The shared <code class="language-plaintext highlighter-rouge">common</code> library is built as a platform-neutral wheel and placed in the <code class="language-plaintext highlighter-rouge">public/</code> subdirectory of the WASM output during the Gradle build. 
The browser fetches it at startup via a relative URL and installs it with <code class="language-plaintext highlighter-rouge">micropip</code>, achieving code reuse across both apps without any server-side Python. 
After that, the app runs fully client-side, calling the backend REST API from the browser using the OAuth-aware HTTP client in <code class="language-plaintext highlighter-rouge">common/api/</code>.</p>

<h2 id="-trade-offs-and-constraints-of-marimo-wasm"><a name="trade-offs-and-constraints-of-marimo-wasm"></a> Trade-offs and Constraints of marimo WASM</h2>

<p>No architectural decision is free. 
These are the constraints we accepted:</p>

<p><strong>Pyodide’s library limitations.</strong> 
If a script requires a library that Pyodide has not compiled, it cannot run in WASM. 
So far this has not been a problem — NumPy, SciPy, and Pandas cover our use cases. 
This also prevented us from generating the complete client with <code class="language-plaintext highlighter-rouge">OpenAPI</code> as this is not using <code class="language-plaintext highlighter-rouge">pyodide.http</code>.</p>

<p><strong>Startup latency.</strong> 
Starting up the marimo app takes some time because the browser first has to initialize Pyodide and load notebook dependencies.</p>

<p><strong>Performance.</strong> 
WASM Python is slower than native Python. 
For the data sizes we work with (thousands to low tens of thousands of data points), this is unnoticeable. 
For genuinely large datasets, data should be downsampled on the server.</p>

<p><strong>Notebook source is embedded in the HTML.</strong> 
The WASM export includes the Python source. 
Spring Security protects access, but anyone authenticated can view source. 
This is an accepted trade-off; the notebooks contain customer-specific logic that customers themselves should be able to see.</p>

<p><strong>Notebook architecture limits app complexity.</strong> 
With marimo notebooks, it is not easy to build larger, more complex applications. 
We therefore use this approach for focused, interactive analysis tools rather than full-featured application surfaces.</p>

<p><strong>Dual-mode boilerplate.</strong> 
The <code class="language-plaintext highlighter-rouge">wasm_marimo</code> flag is a small but real maintenance surface. 
We mitigated this by keeping it minimal and consistent across notebooks.</p>

<hr />

<h2 id="-benefits-of-browser-based-python-engineering-tools"><a name="benefits-of-browser-based-python-engineering-tools"></a> Benefits of Browser-Based Python Engineering Tools</h2>

<ul>
  <li><strong>Fast time to value.</strong> 
We can build new interactive analysis tools as Python notebooks instead of full frontend features.</li>
  <li><strong>Easy customer-specific extensions.</strong> 
It is straightforward to adapt notebooks to individual requirements.</li>
  <li><strong>Strong plotting capabilities.</strong> 
Rich, interactive visualizations are available out of the box.</li>
  <li><strong>Practical engineering UI components.</strong> 
marimo includes useful prebuilt elements for technical workflows.</li>
  <li><strong>No backend Python execution.</strong> 
With marimo WASM, code runs in a fully browser-sandboxed environment.</li>
  <li><strong>Simple deployment model.</strong> 
The production artifact is static content packaged into the existing Spring Boot application.</li>
</ul>

<hr />

<h2 id="-conclusion-when-marimo-wasm-fits"><a name="conclusion-when-marimo-wasm-fits"></a> Conclusion: When marimo WASM Fits</h2>

<p>Marimo’s WASM deployment mode gave us something we could not easily get elsewhere: 
a fully interactive Python data environment that runs in the browser, requires no server-side Python runtime, and integrates naturally with an existing Spring Boot security model.</p>

<p>The combination of reactive notebooks, Pyodide’s scientific Python stack, and standard REST-based data access covers the vast majority of customer-specific scripting use cases we encounter — at a fraction of the implementation cost of our previous approach. 
The full stack looks like this:</p>

<table>
  <thead>
    <tr>
      <th>Concern</th>
      <th>Technology</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Notebook authoring</td>
      <td>marimo</td>
    </tr>
    <tr>
      <td>Python runtime in browser</td>
      <td>Pyodide (via marimo <code class="language-plaintext highlighter-rouge">html-wasm</code> export)</td>
    </tr>
    <tr>
      <td>In-browser package loading</td>
      <td><code class="language-plaintext highlighter-rouge">micropip</code></td>
    </tr>
    <tr>
      <td>Python package management</td>
      <td><code class="language-plaintext highlighter-rouge">uv</code></td>
    </tr>
    <tr>
      <td>Shared logic distribution</td>
      <td>Pure-Python wheel (<code class="language-plaintext highlighter-rouge">common-0.1.0-py3-none-any.whl</code>)</td>
    </tr>
    <tr>
      <td>API type safety</td>
      <td><code class="language-plaintext highlighter-rouge">OpenAPI</code> Generator → <code class="language-plaintext highlighter-rouge">Pydantic</code> models</td>
    </tr>
    <tr>
      <td>Build orchestration</td>
      <td>Gradle 9 with <code class="language-plaintext highlighter-rouge">python-uv-plugin</code></td>
    </tr>
    <tr>
      <td>Deployment</td>
      <td>Spring Boot static resource serving</td>
    </tr>
  </tbody>
</table>

<p>For teams considering a similar architecture, the deciding questions are simple: 
do your dependencies run in Pyodide, and are your data volumes suitable for browser-side execution? 
If yes, marimo WASM offers a compelling deployment model: 
interactive Python tools shipped as static assets, protected by the same authentication and API layer as the rest of the application.</p>

<h2 id="-lets-discuss"><a name="cta"></a> Let’s discuss!</h2>

<p>Do you have questions about browser-based Python tools, marimo WASM, Pyodide, or Spring Boot integration?
<a href="/people/marcel">Feel free to reach out</a>.
I’m always happy to exchange knowledge, ideas, and experiences.</p>]]></content><author><name>marcel</name></author><category term="Spring Boot" /><category term="Python" /><category term="REST API" /><category term="WASM" /><summary type="html"><![CDATA[Deploy marimo WASM notebooks as browser-based Python tools with Pyodide, Spring Boot security, shared wheels, and REST API integration.]]></summary></entry><entry><title type="html">Open Source Doesn’t Need Another Pull Request. It Needs Triage.</title><link href="https://dev.karakun.com/2026/06/09/open-source-doesnt-need-another-pull-request-it-needs-triage.html" rel="alternate" type="text/html" title="Open Source Doesn’t Need Another Pull Request. It Needs Triage." /><published>2026-06-09T00:00:00+00:00</published><updated>2026-06-09T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/06/09/open-source-triage</id><content type="html" xml:base="https://dev.karakun.com/2026/06/09/open-source-doesnt-need-another-pull-request-it-needs-triage.html"><![CDATA[<p>Most engineers think contributing to open source starts when you write code.</p>

<p>But on busy open source projects, the most valuable contribution is often not another pull request. 
It is triage: clarifying issues, connecting related work, identifying incomplete fixes, and helping maintainers decide what should happen next.</p>

<p>Large issue trackers are not just backlogs. 
They are the project’s shared memory. 
When that memory is vague, outdated, or misleading, contributors duplicate work, maintainers merge partial fixes, and users keep running into problems the project may already have half-solved elsewhere.</p>

<p>This article explains why open source triage is engineering work, how it helps maintainers, how to distinguish related issues from true duplicates, and how AI coding agents can support triage without replacing human judgment.</p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#Triage-is-debugging-the-issue-tracker">Triage is debugging the issue tracker</a></li>
  <li><a href="#Why-this-matters-more-than-most-people-think">Why this matters more than most people think</a></li>
  <li><a href="#Related-isn't-the-same-as-duplicate">Related isn’t the same as duplicate</a></li>
  <li><a href="#The-5-minute-workflow-I-wish-more-people-used">The 5-minute workflow I wish more people used</a></li>
  <li><a href="#What-good-triage-comments-sound-like">What good triage comments sound like</a></li>
  <li><a href="#The-fastest-ways-to-make-triage-worse">The fastest ways to make triage worse</a></li>
  <li><a href="#AI-makes-human-triage-more-important">AI makes human triage more important, not less</a></li>
  <li><a href="#Final-Thoughts">Final thoughts</a></li>
</ul>

<hr />

<p>Before getting into the workflow, here is the kind of situation where triage matters.</p>

<p>Imagine a successful open source project with thousands of open issues. 
Somewhere in that backlog are three related reports:</p>

<ul>
  <li>one says the Web UI only accepts images</li>
  <li>another asks for document uploads, such as PDF and Word files</li>
  <li>a third asks for support for uploading any file type</li>
</ul>

<p>In the pull request queue, two related fixes already exist:</p>

<ul>
  <li>one changes only the file picker in the frontend</li>
  <li>another changes both the file picker and the backend</li>
</ul>

<p>Now one engineer finds the first issue and starts writing a third pull request because the bug looks easy to fix. 
At the same time, a maintainer sees the frontend-only PR, assumes it solves the whole problem, and merges it.
The UI now looks fixed, but the backend still drops the files. 
Multiple people have spent their evening on the same problem, and the issue tracker is now misleading everyone.</p>

<p>The most expensive open source bug is often not the hardest one. 
It is the one that gets fixed three times.</p>

<p>You might think this is just a communication problem. 
In a company, people might notice each other’s work in a stand-up or Slack channel. 
Open source does not work like that.</p>

<p>On large open source projects, contributors work across time zones and personal schedules.
Maintainers cannot manually connect every duplicate issue, related pull request, partial fix, and stale report. 
If they did, they would have no time left to review the work that actually needs to be merged.</p>

<p>That is why missing links between issues and pull requests are not a minor inconvenience. 
If one engineer claims an issue but another finds a duplicate elsewhere, they may never realize that someone is already halfway through the fix, or that two pull requests already address the same problem.</p>

<p>I almost did exactly that.
While using <a href="https://openclaw.ai" target="_blank">OpenClaw</a> on my Android phone, I noticed that tapping the paperclip in the Web UI only let me choose images, while Telegram let me upload any file.
Since OpenClaw is an AI coding assistant, I asked it to investigate whether this was a bug. 
It found the technical cause quickly and immediately asked whether it should prepare a pull request.</p>

<p>Instead, I asked it to check the issue tracker and pull requests first.
That changed everything.</p>

<p>Several related issues and two pull requests already existed. 
One PR changed only the frontend. 
The other changed both frontend and backend.
The key detail was that we already knew the backend dropped these files, so a UI-only fix would create a feature that looked complete but still failed.</p>

<p>At that point, writing another fix was the least useful thing I could do.
The useful contribution was mapping the existing work so maintainers could see the overlap, close duplicates, and focus on the pull request that actually solved the whole problem.
That is triage.</p>

<h2 id="-triage-is-debugging-the-issue-tracker"><a name="Triage-is-debugging-the-issue-tracker"></a> Triage is debugging the issue tracker</h2>

<p>At first glance, triage sounds boring.</p>

<p>It sounds like paperwork.
It sounds like process.
It sounds like the thing you do when you can’t contribute code.
I think that’s backwards.</p>

<p>On a busy open source project, triage is one of the most important contributions you can make, because it changes what everyone else does next.</p>

<p>The best short definition I’ve come up with is this:</p>

<p><strong>Triage is debugging the issue tracker until the next action becomes obvious.</strong></p>

<p>That next action might be:</p>

<ol>
  <li>Is this reproducible?</li>
  <li>Is there already a better issue for it?</li>
  <li>Is there already a PR for it?</li>
  <li>Does that PR solve the whole problem, or only part of it?</li>
  <li>Is this still relevant, or has later work already changed the behavior?</li>
</ol>

<p>A good triage comment usually doesn’t try to do everything.</p>

<p>It does one job: it reduces uncertainty.</p>

<p>If you only remember one thing from this article, remember this:</p>

<p><strong>A short, accurate comment is better than a long, uncertain one.</strong></p>

<h2 id="-why-this-matters-more-than-most-people-think"><a name="Why-this-matters-more-than-most-people-think"></a> Why this matters more than most people think</h2>

<h3 id="duplicate-work-is-easier-than-people-realize">Duplicate work is easier than people realize</h3>

<p>As we saw with the OpenClaw example, the asynchronous nature of open source makes it incredibly easy for two people to spend their evening on the exact same problem without realizing it.</p>

<p>That sounds small until it happens again and again.</p>

<p>Then it becomes a tax on everyone involved.</p>

<h3 id="a-merged-pr-isnt-the-same-as-a-solved-issue">A merged PR isn’t the same as a solved issue</h3>

<p>A PR title can sound complete. 
The green “Merged” badge feels like a finish line. 
But a merged PR doesn’t automatically mean the whole problem is gone.</p>

<p>Recently, a severe issue was reported in OpenClaw: sending a binary file via Telegram caused the bot to dump raw, unsanitized bytes into the context. 
A single file could blow up the prompt to <a href="https://github.com/openclaw/openclaw/pull/66663" target="_blank">around 460,000 tokens</a>. 
This wasn’t just a bug; it posed a massive risk of resource exhaustion and cost amplification.</p>

<p>Shortly after the issue was reported, an OpenClaw contributor opened a PR to fix it.
Because the issue affected prompt handling and could dramatically increase token usage, a maintainer merged the change quickly.
When I looked at the diff, the fix seemed surprisingly small for the scope of the problem. 
I deployed the updated OpenClaw branch locally and tried to reproduce the issue myself.
Given the severity of the problem and the volume of incoming work, I completely understood why the maintainer had merged it so quickly.</p>

<p>Normally, your time is better spent triaging open issues than rechecking merged PRs.
But in this case, the diff left me unsure whether the entire problem had actually been solved.</p>

<p>Uploading a 100 KB EPUB file immediately blew up my local prompt to <a href="https://github.com/openclaw/openclaw/pull/66877" target="_blank">231,000 tokens</a>.</p>

<p>The PR author had fixed part of the issue, but not all of it.
The PR also skipped the repository’s verification checklist, so nobody had explicitly confirmed the fix worked outside the code review itself.</p>

<p>If the missing verification had been obvious earlier, someone else could have tested the change before it was merged. 
Whether a human ignores a template or an AI omits it entirely, maintainers lose important context for judging how much trust to place in a fix.</p>

<p>After I patched the remaining upload leak, I kept digging. 
Experience tells me that where there is one bug, there are usually neighbors. 
Instead of stopping at the narrowest interpretation of the bug, I tried <em>replying</em> to a message of a previously sent binary file on Telegram. 
Sure enough, it pulled the raw bytes into the context again. 
It was a different path, but the same broader problem.</p>

<p>I packaged both fixes into a new <a href="https://github.com/openclaw/openclaw/pull/66877" target="_blank">PR (#66877)</a>, which the maintainers merged an hour later.</p>

<p>The real lesson here is about what code review looks like under pressure. 
In a perfect world, every PR would be tested locally before it is merged. 
In reality, maintainers often have to rely on diffs, contributor claims, and community feedback to decide whether a fix is ready.</p>

<p>This is exactly where triage steps in. 
You don’t have to write the code to save the day. 
If you take an open PR, test it locally, and leave a comment saying, <em>“I deployed this branch and followed the reproduction steps, but the issue is still present,”</em> you just saved the project from shipping a broken feature. 
Catching an incomplete fix before it gets merged makes you a hero to the maintainers.</p>

<p>In my case, the broken PR was merged within minutes because it was an urgent security fix, leaving no window for the community to verify the code before it landed. 
But normally, PRs sit in the review queue for days or weeks. 
That gives you plenty of time to pull the branch, test the fix yourself, and raise that exact flag.</p>

<p>However, if the code actually works, commenting, <em>“I deployed this locally and confirmed: on main the issue happens, but on this branch it is completely resolved,”</em> is extremely valuable for a maintainer. 
Doing the manual verification that maintainers don’t have time for is one of the most valuable triage contributions you can make.</p>

<h3 id="a-messy-issue-tracker-lies-to-people">A messy issue tracker lies to people</h3>

<p>An unclear issue tracker doesn’t just look untidy.
It actively changes what people decide to do.</p>

<p>Someone spends an evening reproducing a bug that already has a PR.
A maintainer assumes the problem is solved because the title sounds right.
A contributor opens a duplicate issue for a “PDF upload error” because the original report was vaguely titled “mobile attachment bug” and nobody ever added the specific keywords or error codes that would have made it show up in a search.</p>

<p>Bridging these gaps doesn’t require you to be the lead architect. 
It just means applying a <strong>technical perspective</strong> to look past the surface-level description. 
In my case, it was easy to assume that because Telegram already allowed all file types, the backend was fine - making a frontend-only fix look like the complete answer. 
Triage is that “Wait a minute” moment where you pause to verify if that assumption is actually true.</p>

<p>Whether you’re identifying a shared root cause between two different-looking bugs, or noticing that a PR only masks a symptom instead of fixing the logic, you’re using engineering judgment. That’s why keeping the issue tracker trustworthy isn’t admin work; it’s <strong>engineering work</strong>.</p>

<h3 id="its-one-of-the-best-ways-to-start-contributing">It’s one of the best ways to start contributing</h3>

<p>Many engineers assume they need deep knowledge of the codebase before they can contribute to open source.</p>

<p>That’s understandable, but it’s often wrong.</p>

<p>You don’t need to know every service, build step, and deployment detail to notice that:</p>

<ul>
  <li>the reproduction steps are missing</li>
  <li>the version is missing</li>
  <li>the PR description doesn’t match the changed files</li>
  <li>two PRs address the same issue but aren’t linked to the issue yet</li>
  <li>a PR fixes an issue that hasn’t been linked</li>
  <li>two issues describe the exact same problem from slightly different angles</li>
  <li>a PR only touches the frontend because it <strong>looks</strong> as though the backend is already “done” (as I initially assumed). You don’t need to know the code to ask: 
“Since Telegram <strong>already allows all file types</strong>, are we sure the Web UI uses the exact same API path, or are we just <strong>hoping</strong> it does?”</li>
</ul>

<p>That level of careful reading is an immediate, high-value contribution. 
On a project with a large review queue, the last thing maintainers need is another item added to it. 
Even a one-line “quick fix” adds to the noise. 
Connecting the dots is more valuable because it helps clear the backlog instead of adding to it.</p>

<h2 id="-related-isnt-the-same-as-duplicate"><a name="Related-isn't-the-same-as-duplicate"></a> Related isn’t the same as duplicate</h2>

<p>This is one of the easiest mistakes to make if you’re new to open source. 
Several things can exist in the same part or functionality of the software without being the same issue.</p>

<p>In my OpenClaw example, all of these were about file uploads:</p>

<ul>
  <li>the web UI only accepts images</li>
  <li>one issue asks for support for document files</li>
  <li>a broader issue wants support for any kind of file</li>
  <li>one PR changes only the frontend</li>
  <li>another changes frontend and backend</li>
  <li>there was also a question about whether uploading files actually worked at all</li>
</ul>

<p>Those items are clearly connected, but they aren’t identical. 
If you treat them as a single “bucket” and start closing them simply based on which one arrived first, you create a chain reaction of waste:</p>

<p><strong>1. The “Incomplete Fix” Trap</strong></p>

<p>It’s easy to think, <em>“Obviously, I would keep the PR that fixes both the frontend and the backend.”</em> 
But in reality, triagers and maintainers rarely have the time to deeply compare the code of every duplicate. 
Usually, they just see two PRs with similar titles that claim to fix the same problem.</p>

<p>Ideally, the PR author would leave a note saying, <em>“I’m opening this because PR #123 is an incomplete fix.”</em> 
But in practice, most contributors don’t realize they should search for existing, unlinked PRs before writing code. 
They usually have no idea the older PR even exists.</p>

<p>If you just assume the two PRs are identical and blindly close the newer one as a duplicate, you might accidentally bury the actual solution. 
If the older, partial fix gets merged, the feature will still be broken. 
But since the “right” PR was already closed, nobody will think to look for it. 
Weeks from now, a third contributor will end up spending hours just to rewrite the exact same backend code that was already sitting in the PR you closed.</p>

<p><strong>2. The Scope Trap</strong></p>

<p>If you close the request for “any file type” in favor of the narrower “documents support,” you have unintentionally limited the project’s potential. 
It creates a confusing experience where users can send images through the Telegram bot, but not through the Web UI. 
This mismatch happens when a triager takes an issue author’s request too literally. 
Issue authors often think about their immediate needs, like uploading a PDF, but a good triager has to translate that narrow complaint into a broader system requirement. 
If you just assume the issue author’s specific example is the whole story, you guarantee that developers will have to rewrite the exact same code the moment someone else tries to upload a code file.</p>

<p><strong>3. The “Cannot Reproduce” Trap</strong></p>

<p>The most dangerous mistake in triage is assuming a bug doesn’t exist just because you can’t reproduce it yourself. 
It’s incredibly common to read an issue report, try it out, see it working perfectly, and immediately close the issue as non-reproducible. 
But unless the issue author is an LLM, they probably aren’t hallucinating. 
If you can’t trigger the bug, it’s almost always because you don’t fully understand the issue author’s environment, or because they left out a specific detail that felt too “obvious” to mention. 
When you experience this, the best thing you can ask yourself is: <em>“What am I missing?”</em></p>

<p>You wouldn’t believe how many issues get closed this way, leaving real, systemic bugs hiding in the codebase to frustrate users for years. 
Maintainers don’t do this because they don’t care. 
With thousands of issues to fix, they just don’t have the time to chase down missing details for every vague report.</p>

<p>This is exactly where you can step in.
By asking clarifying questions, recreating the issue author’s environment as closely as possible, or testing different configurations, you provide the exact help maintainers often don’t have time to provide themselves.
When you can’t reproduce a bug, don’t ask, “Should I close this?” Ask, “What am I missing?”</p>

<p>Good triage means looking for the missing variable instead of closing the issue at the first obstacle.</p>

<p>There’s one more trap that causes the same kind of waste, even when no duplicate is involved.</p>

<p><strong>4. The “Confident Diagnosis” Trap</strong></p>

<p>Some issue reports do more than describe a problem. 
They also explain what the issue author believes caused it.
That’s helpful but it can also hide the next thing you should verify.</p>

<p>Imagine an issue that says:</p>

<blockquote>
  <p>The database doesn’t save profile changes.</p>

  <p>I changed my display name, clicked Save, saw a success message, refreshed the page, and the old name was back.</p>

  <p>I checked the user record in the database and found that it still contains the old display name, so I suspect there’s an issue with writing the change to the database.</p>
</blockquote>

<p>The issue author may be right. 
Maybe the backend really doesn’t persist the change to the database.</p>

<p>But the problem could also be somewhere else: the frontend never sent the changed field, the backend rejected the change but the frontend still showed a success message, the backend wrote to a different record, or the write was attempted but rolled back.</p>

<p>Good faith means assuming the issue author is trying to help. It doesn’t mean assuming their diagnosis is automatically correct.</p>

<p>A useful triage comment could say:</p>

<blockquote>
  <p>Thanks for the clear steps and for checking the database.</p>

  <p>You mentioned that after clicking Save, you see a success message, but the user record in the database still contains the old display name.</p>

  <p>I’d like to confirm where in the flow the change gets lost. Could you check the browser network tab when clicking Save and see whether the request contains the changed display name and whether the response is successful?</p>

  <p>That would help narrow this down: either the frontend doesn’t send the change, the backend rejects it but the frontend still shows success, the backend writes to a different record, or the write is attempted but rolled back.</p>
</blockquote>

<p>This kind of comment takes the issue seriously without accepting the first diagnosis too quickly. 
It tests the simple explanations first, but still leaves room for the issue author to have information you don’t have yet.</p>

<p>That balance matters. 
If you assume the issue author is wrong, your comment can sound dismissive and make them defensive. 
If you assume the issue author’s diagnosis is right, you may skip the simplest explanation and turn a misunderstanding into a misclassified bug, a misplaced feature request, or a much larger investigation than necessary.</p>

<p><strong>Don’t waste a misunderstanding</strong></p>

<p>And if the issue author comes back and says, “You were right, I misunderstood how this works,” don’t treat that as the end of the story.</p>

<p>That misunderstanding may still be useful.</p>

<p>This isn’t limited to configuration misunderstandings. 
Whenever an issue ends without any change to code, docs, examples, error messages, or repository guidance, pause for a moment: is there a small change that would have helped the issue author find the answer before needing to open an issue?</p>

<p>You can ask:</p>

<blockquote>
  <p>Thanks for confirming, glad it works now.</p>

  <p>One more thought: this sounds like something the docs could make clearer. 
What should the docs say so the next person can find the answer before needing to open an issue?</p>
</blockquote>

<p>If the issue author suggests a clearer wording, don’t let that disappear in the thread. 
If you have time, turn it into a small docs PR and link the PR back to the original issue. 
You don’t need to be a maintainer to do that.</p>

<p>If you don’t have time to make the docs change yourself, create a follow-up issue instead. 
Link the original discussion and write down what was confusing, so someone else can pick it up later.</p>

<p>Even if the original issue has already been closed, this can still be valuable. 
If comments are still open, you can ask the question there. 
If not, you can still open a docs issue that points back to the original discussion.</p>

<p>A misunderstanding isn’t always just user error. 
Sometimes it’s evidence that the project is teaching the right thing in the wrong way.</p>

<p>That’s still triage. 
You took one confusing issue and turned it into something the project can learn from.</p>

<p>In the end, good triage sits in the middle. 
It keeps the differences that matter and removes the duplication that doesn’t. 
Sometimes the real question isn’t just “what links to what?” It’s “what kind of problem is this, actually?” 
In my case, the Web UI behavior looked like a bug because Telegram allowed arbitrary file types. 
But after reading more closely, it also looked plausible that the Web UI had simply been implemented as an image-only flow on purpose. 
Good triage makes that kind of distinction visible instead of pretending it’s obvious.</p>

<h2 id="-the-5-minute-workflow-i-wish-more-people-used"><a name="The-5-minute-workflow-I-wish-more-people-used"></a> The 5-minute workflow I wish more people used</h2>

<p>You don’t need a large, complicated process.
You need one simple enough that you’ll actually use it. 
If a workflow feels like a chore, you’ll skip it the second you get busy. 
If it’s natural and intuitive, it actually gets followed.</p>

<h3 id="1-read-the-whole-thing">1. Read the whole thing</h3>

<p>Don’t triage from the title.
Read the body, screenshots, reproduction steps, version information, linked issues, recent comments, and, for PRs, the changed files.</p>

<p>A surprising amount of poor triage comes from people reacting to names instead of content.
As you read, slow down whenever the issue author moves from “this happened” to “therefore this is the cause.”</p>

<p>This describes what happened:</p>

<blockquote>
  <p>I changed my display name, clicked Save, saw a success message, refreshed the page, and the old name was back.</p>
</blockquote>

<p>This is a possible cause:</p>

<blockquote>
  <p>There must be an issue with writing to the database.</p>
</blockquote>

<p>The issue author may be right. 
But the cause could also be the frontend request, backend validation, a different database record, a rollback, stale cached data, or a draft state.</p>

<p>That doesn’t mean the issue author is wrong. 
It just tells you what to check next.</p>

<h3 id="2-ask-whether-there-is-enough-information-to-act">2. Ask whether there is enough information to act</h3>

<p>Before doing detective work, ask a simpler question:
Is there even enough detail here to classify the problem?</p>

<p>For an issue, that usually means:</p>

<ul>
  <li>exact version</li>
  <li>reproduction steps</li>
  <li>expected behavior</li>
  <li>actual behavior</li>
  <li>environment</li>
  <li>logs or screenshots, when relevant</li>
</ul>

<p>For a PR, it can also mean:</p>

<ul>
  <li>scope</li>
  <li>linked issues</li>
  <li>tests</li>
  <li>migration notes</li>
  <li>whether the changed files actually match the claim</li>
</ul>

<p>If the basics are missing, asking for them may already be the most useful thing you can do.</p>

<h3 id="3-search-for-existing-context">3. Search for existing context</h3>

<p>Before you comment, build a tiny map in your head by searching for:</p>

<ul>
  <li>the same symptom</li>
  <li>a broader issue in the same area</li>
  <li>a deeper issue behind the symptom</li>
  <li>an existing PR that may already cover it</li>
  <li>a PR that may only cover part of it</li>
  <li>newer releases or merged PRs that may already have changed the behavior</li>
</ul>

<p>That small map is often enough to stop you from commenting too early or opening something that never needed to exist.</p>

<h3 id="4-decide-the-one-job-of-your-comment">4. Decide the one job of your comment</h3>

<p>Before writing anything, finish this sentence:</p>

<p><strong>The job of this comment is to…</strong></p>

<p>For example:</p>

<ul>
  <li>ask for missing details</li>
  <li>link this issue to a broader one</li>
  <li>point out that the PR is partial</li>
  <li>tell readers where the real implementation work is happening</li>
  <li>explain that two related issues aren’t duplicates</li>
</ul>

<p>If your comment tries to do five jobs, it will usually do none of them well.</p>

<h3 id="5-only-state-what-you-have-verified">5. Only state what you have verified</h3>

<p>This is the rule I trust most: <strong>Don’t guess. Only state exactly what you’ve personally checked.</strong></p>

<p>Not the longest explanation.
Not the most confident-sounding assumption.
Just the verified facts.</p>

<p>Comment on the <strong>issue</strong> when the main point is that the problem needs clarification, is narrower or broader than another issue, or already has a relevant PR.</p>

<p>Comment on the <strong>PR</strong> when the main point is that the proposed fix is partial, broader than the linked issue, or overlapping with other work.</p>

<p>Only comment on both the issue and the PR if the two audiences (the issue authors reporting the bug and the developers reviewing the code) genuinely need different information.</p>

<p>And whatever you do, match your certainty to what you actually verified.</p>

<p>Don’t write:</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Fixed by #123.
</code></pre></div></div>

<p>because the title sounds right.
Write it only if you checked the diff and are confident it really solves the issue.</p>

<p>If you’re not there yet, softer wording is better:</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code>This may be addressed by #123.
</code></pre></div></div>

<p>That sounds like a small difference.
In triage, it isn’t.</p>

<p>On GitHub, if you write <a href="https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue" target="_blank"><code class="language-plaintext highlighter-rouge">Fixes #123</code></a> in a PR description, the linked issue will usually get closed automatically once the PR is merged. 
If you’re wrong, the bug stays in production, users get frustrated, and someone has to open a new issue weeks later. 
That false confidence is expensive.</p>

<h2 id="-what-good-triage-comments-sound-like"><a name="What-good-triage-comments-sound-like"></a> What good triage comments sound like</h2>

<p>The best triage comments are usually short, concrete, and slightly boring in the best possible way.</p>

<p>They don’t try to sound clever.
They don’t try to sound authoritative.
They remove confusion.</p>

<p>A useful triage comment usually does three things:</p>

<ol>
  <li>Lead with the conclusion.</li>
  <li>Explain why.</li>
  <li>Then stop.</li>
</ol>

<p>That doesn’t mean the comment has to be tiny. 
It means the comment should contain the information needed to make the next decision without requiring everyone else to reconstruct your reasoning.</p>

<p>Here are four real patterns from the OpenClaw issues and PRs that inspired this post.</p>

<h3 id="linking-a-narrower-issue-to-a-broader-one">Linking a narrower issue to a broader one</h3>

<p>From <a href="https://github.com/openclaw/openclaw/issues/50337#issuecomment-4184430216" target="_blank">Issue #50337</a>:</p>

<blockquote>
  <p>This issue is similar to <a href="https://github.com/openclaw/openclaw/issues/56344" target="_blank">#56344</a>.</p>

  <p>This issue is about allowing documents to be uploaded in addition to images through the Web UI, while <a href="https://github.com/openclaw/openclaw/issues/56344" target="_blank">#56344</a> is about allowing all file types. 
I prefer the approach of <a href="https://github.com/openclaw/openclaw/issues/56344" target="_blank">#56344</a>, because it’s consistent with what channels like Telegram allow and would also cover other useful file types like <code class="language-plaintext highlighter-rouge">.patch</code>, <code class="language-plaintext highlighter-rouge">.md</code>, <code class="language-plaintext highlighter-rouge">.adoc</code>, etc.</p>

  <p>Because of that, I think this issue could be closed in favor of <a href="https://github.com/openclaw/openclaw/issues/56344" target="_blank">#56344</a>. 
The PR for that broader change is <a href="https://github.com/openclaw/openclaw/pull/57707" target="_blank">#57707</a>.</p>
</blockquote>

<p>This works because it doesn’t just say “duplicate” or “related”. 
It explains the relationship between the issues: one is narrower, the other is broader, and the broader one already has an implementation path.</p>

<p>The useful pattern is:</p>

<ol>
  <li>Name the related issue.</li>
  <li>Explain how it’s related.</li>
  <li>Explain which one should stay open and why.</li>
  <li>Point to the PR, if one exists.</li>
</ol>

<h3 id="explaining-that-a-pr-is-only-partial">Explaining that a PR is only partial</h3>

<p>From <a href="https://github.com/openclaw/openclaw/pull/54248#issuecomment-4184430558" target="_blank">PR #54248</a>:</p>

<blockquote>
  <p>This PR is incomplete, because it only covers the UI side of the upload flow.</p>

  <p>Files other than images would still not be handled properly by the backend, so this would not fully solve the problem. 
I think this PR could be closed in favor of <a href="https://github.com/openclaw/openclaw/pull/57707" target="_blank">#57707</a>, because that one covers both the frontend and backend parts of the same issue.</p>
</blockquote>

<p>This works because it leads with the conclusion but still includes the reason that matters. 
The problem isn’t that the PR is bad. 
The problem is that it only fixes one side of the flow.</p>

<p>The useful pattern is:</p>

<ol>
  <li>State that the PR is incomplete.</li>
  <li>Say exactly which part it covers.</li>
  <li>Say exactly which part is still missing.</li>
  <li>Link to the more complete PR.</li>
</ol>

<h3 id="pointing-issue-readers-to-the-implementation">Pointing issue readers to the implementation</h3>

<p>From <a href="https://github.com/openclaw/openclaw/pull/57707#issuecomment-4184430466" target="_blank">PR #57707</a>:</p>

<blockquote>
  <p>Implements <a href="https://github.com/openclaw/openclaw/issues/56344">#56344</a> and <a href="https://github.com/openclaw/openclaw/issues/58423" target="_blank">#58423</a>.</p>

  <p>It also includes the smaller change requested in <a href="https://github.com/openclaw/openclaw/issues/50337" target="_blank">#50337</a>, since allowing all file types also covers allowing documents in addition to images through the Web UI.</p>
</blockquote>

<p>This works because it makes the scope of the PR explicit. 
Someone reading one of the issues can understand that this implementation covers more than one request, and why the smaller request is included in the broader one.</p>

<p>The useful pattern is:</p>

<ol>
  <li>List the issues the PR implements.</li>
  <li>Mention smaller related requests that are also covered.</li>
  <li>Explain why they are covered, instead of assuming that the link is obvious.</li>
</ol>

<h3 id="asking-for-the-one-detail-that-matters-next">Asking for the one detail that matters next</h3>

<p>Adapted from <a href="https://github.com/openclaw/openclaw/issues/56375#issuecomment-4184430620" target="_blank">Issue #56375</a>:</p>

<blockquote>
  <p>The upload button isn’t just decorative. Uploading image files through it works for me.</p>

  <p>You’re on <code class="language-plaintext highlighter-rouge">2026.3.24</code>. Can you check whether this still happens on <code class="language-plaintext highlighter-rouge">2026.4.2</code>?</p>

  <p>It would help if you could share the file type you are trying to upload, the actual file if you can attach it, whether this only affects one file or different file types, whether it also happens in another browser, what OS/environment OpenClaw runs on, and whether you use an ad blocker, VPN, router-level blocking, or something similar.</p>

  <p>Since the screenshot shows a custom API setup, it would also help to know whether this happens with other providers/models and to see the relevant part of your <code class="language-plaintext highlighter-rouge">openclaw.json</code>, with secrets redacted.</p>

  <p>One important detail: the Web UI currently only supports image uploads. So if your file picker lets you choose a non-image file, that could explain what you are seeing. This may also change with <a href="https://github.com/openclaw/openclaw/pull/57707" target="_blank">#57707</a>, which adds support for all file types in the Web UI.</p>
</blockquote>

<p>This works because it doesn’t just ask for “more information”. 
It asks for the specific information that would help separate several possible causes: an old version, an unsupported file type, a browser issue, an environment issue, a blocking extension, or a provider/model configuration problem.</p>

<p>The useful pattern is:</p>

<ol>
  <li>State what you could verify yourself.</li>
  <li>Ask the issue author to test the newest relevant version, if they aren’t already using it.</li>
  <li>Ask for the smallest useful set of missing details.</li>
  <li>Explain any limitations that might already explain the issue report.</li>
  <li>Link to the PR that may change that behavior.</li>
</ol>

<p>The pattern is the same in all four cases: say what you think should happen, give enough context to make it actionable, and avoid turning the comment into another discussion thread.</p>

<p>Good triage comments aren’t short because information is missing. 
They’re short because everything unrelated to the next decision has been removed.</p>

<h2 id="-the-fastest-ways-to-make-triage-worse"><a name="The-fastest-ways-to-make-triage-worse"></a> The fastest ways to make triage worse</h2>

<p>Bad triage is worse than no triage because it adds noise and false confidence.</p>

<p>The fastest ways to make a busy issue tracker worse are usually these:</p>

<ul>
  <li>opening a new issue or PR before checking what already exists</li>
  <li>posting bare links like <code class="language-plaintext highlighter-rouge">Related: #123</code> or no links at all without saying why the link matters</li>
  <li>posting comments like “I have this issue too” without providing any relevant context or reproduction steps</li>
  <li>guessing from titles instead of reading the diff</li>
  <li>assuming that just because two issues touch the same part of the UI, they must be the exact same bug</li>
  <li>asking for information that’s already in the issue body or screenshot</li>
  <li>sounding more certain than you really are</li>
  <li>treating “it was my mistake” as the end of the story, instead of turning the misunderstanding into a docs improvement or follow-up issue</li>
  <li>pasting AI-generated comments without reviewing them first</li>
</ul>

<p>Remember: triage is supposed to reduce work, not increase it!</p>

<h2 id="-ai-makes-human-triage-more-important-not-less"><a name="AI-makes-human-triage-more-important"></a> AI makes human triage more important, not less</h2>

<p>AI is genuinely useful for triage.
It can help with things like:</p>

<ul>
  <li>finding related issues and PRs</li>
  <li>checking whether an issue or PR follows the repository template</li>
  <li>suggesting better search terms</li>
  <li>summarizing the overlap between two issues</li>
  <li>mapping the surrounding repository context</li>
</ul>

<p>But AI is also very good at sounding certain when it shouldn’t be.
That makes it useful as an assistant and dangerous as a substitute for judgment.
A simple way I think about it is this:</p>

<p><strong>AI is a speed multiplier. It multiplies good process and bad process.</strong></p>

<p>In my OpenClaw case, the assistant quickly understood the code and was ready to fix it. 
What it didn’t naturally do was the human part: slow down, inspect the issue tracker carefully, and figure out whether a new PR would actually help.</p>

<p>Instead of letting AI blindly post comments for you, the best way to use it for triage is to map the territory first.</p>

<p>You can use AI to scan the repository, identify similar issues, check recent PRs, read contribution guidelines, and inspect issue or PR templates before you ever write a line of code.</p>

<p>One tedious part of that work is checking whether an existing issue or PR actually follows the repository’s own template. 
To make that easier, I wrote a <a href="https://gist.github.com/martinfrancois/b38b3d14098ec585f431299a61c3f7c9" target="_blank">reusable prompt</a> for checking whether an issue or PR follows the repository’s own template. 
You paste in the issue or PR URL, and it asks the agent to find the relevant template, compare the body against it, classify the result, flag possible inconsistencies, and draft a concise comment for you to review, if one is needed.</p>

<p>I contributed that prompt to the <a href="https://github.com/tesslio/good-oss-citizen" target="_blank">Good OSS Citizen</a> skills, so if you use an AI coding agent, Good OSS Citizen is the more convenient version: same idea, less copy-pasting, and more structure.</p>

<p>From the cloned fork of the open source project you plan to work from, install it with one of these commands (requires <a href="https://nodejs.org/en/download" target="_blank">Node.js</a> or <a href="https://bun.com/docs/installation" target="_blank">Bun</a>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># npm</span>
npx tessl i tessl-labs/good-oss-citizen

<span class="c"># Yarn</span>
yarn dlx tessl i tessl-labs/good-oss-citizen

<span class="c"># pnpm</span>
pnpm dlx tessl i tessl-labs/good-oss-citizen

<span class="c"># Bun</span>
bunx tessl i tessl-labs/good-oss-citizen
</code></pre></div></div>

<p>If your coding agent has internet access and can run shell commands, you can also point it to the Good OSS Citizen repository and ask it to install the tool in your fork. 
Review the command before running it.</p>

<p>Then ask your agent:</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Triage this issue:
https://github.com/example/project/issues/123
</code></pre></div></div>

<p>That’s it.</p>

<p>The triage skill in Good OSS Citizen does a bit more than the raw prompt. 
It can fetch the already-open issue or PR body, fetch the matching templates, apply a reusable rubric, write a <code class="language-plaintext highlighter-rouge">triage_comment.md</code> handoff, and explicitly tell the agent not to post to GitHub. 
It drafts; you decide whether to post.</p>

<p>Good OSS Citizen also includes broader open source contribution checks through its rules, skills, and scripts: contribution guidelines, AI policies, prior rejected PRs, claimed issues, DCO requirements, and changelog expectations. 
For triage, the important part is that the agent does the boring checks first and leaves the judgment to you.</p>

<p>Use AI for the heavy lifting.
Let it search.
Let it summarize.
Let it prepare a draft.</p>

<p>But don’t outsource the judgment.</p>

<h2 id="-final-thoughts"><a name="Final-Thoughts"></a> Final thoughts</h2>

<p>Triage isn’t glamorous.
It doesn’t give you the same dopamine hit as opening a PR, seeing green CI checks, and getting something merged.
But on busy open source projects, it’s often the most impactful contribution you can make.</p>

<p>Issue trackers don’t usually get messy because of a single big mistake. 
They become messy the same way a kitchen junk drawer does. 
One day, you toss a vague bug report in. 
The next day, an unlinked PR. 
Then an overconfident comment. 
Nobody cleans it out, and six months later, no one can find the batteries.</p>

<p>Good triage works in the opposite direction.
It makes the issue tracker easier to trust.
It makes the next decision easier.
It helps maintainers spend more time reviewing the right work and less time reconstructing context that should already be there.</p>

<p>And if you’re not sure what kind of help a project needs, ask.</p>

<p>Most projects link to their community from the <code class="language-plaintext highlighter-rouge">README.md</code>, <code class="language-plaintext highlighter-rouge">CONTRIBUTING.md</code>, or documentation. 
Look for words like “Community”, “Contributing”, “Support”, “Chat”, or “Getting help”. 
That might lead you to Discord, Slack, Matrix, Zulip, a forum, a mailing list, or GitHub Discussions.</p>

<p>Once you find the most relevant place, ask a simple question:</p>

<blockquote>
  <p>I like this project and would like to contribute in a way that actually helps. Is this the right place to ask what would be most useful right now?</p>
</blockquote>

<p>Even if it isn’t the perfect place, this makes it easy for someone to point you in the right direction.</p>

<p>So the next time you want to contribute to open source, don’t start by asking:</p>

<p><strong>“What can I code?”</strong></p>

<p>Also ask:</p>

<p><strong>“What can I clarify?”</strong></p>

<p>On busy projects, that’s triage.
And very often, that’s exactly the contribution maintainers need most.</p>

<p>If you’d like to share your own experiences with triage, want a second opinion on a messy issue tracker, or need specific advice, feel free to reach out via <a href="mailto:francois.martin@karakun.com">email</a> or connect with me on <a href="https://linkedin.com/in/françoismartin" target="_blank">LinkedIn</a>.</p>]]></content><author><name>francois</name></author><category term="Development" /><category term="Open source" /><category term="OpenClaw" /><category term="Community" /><summary type="html"><![CDATA[On busy open source projects, triage can be more valuable than another pull request. Learn how to clarify issues, connect related work, and help maintainers make better decisions.]]></summary></entry><entry><title type="html">Jfokus 2026: 20 Years of Java, Community, and Innovation</title><link href="https://dev.karakun.com/2026/04/22/Jfokus2026.html" rel="alternate" type="text/html" title="Jfokus 2026: 20 Years of Java, Community, and Innovation" /><published>2026-04-22T00:00:00+00:00</published><updated>2026-04-22T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/04/22/Jfokus</id><content type="html" xml:base="https://dev.karakun.com/2026/04/22/Jfokus2026.html"><![CDATA[<p>At Karakun, we closely follow trends in Java and modern software engineering. 
Jfokus 2026 marked 20 years of one of Europe’s leading developer conferences, covering topics from core Java to AI and cloud technologies. 
This article summarizes key insights, themes, and observations from the event.</p>

<hr />

<h2 id="table-of-contents">Table Of Contents</h2>

<ul>
  <li><a href="#jfokus-2026-java-community-milestone">A Milestone for the Global Java Community</a></li>
  <li><a href="#jfokus-2026-java-to-multitrack-developer-conference">From Java Conference to Multi-Track Developer Conference</a></li>
  <li><a href="#jfokus-2026-20-years-java-community-growth">Two Decades of Growth in the Java Community</a></li>
  <li><a href="#jfokus-2026-nordic-atmosphere-developer-conference">A Unique Atmosphere: Where Tech Meets Nordic Mythology</a></li>
  <li><a href="#jfokus-2026-java-ai-software-engineering-trends">Key Topics: Java, AI, and Modern Software Engineering Trends</a></li>
  <li><a href="#jfokus-2026-developer-conference-expo-networking">Expo and Networking at a Leading Developer Conference</a></li>
  <li><a href="#jfokus-2026-mentoring-hub-developer-networking">Personal Highlight: The Mentoring Hub</a></li>
  <li><a href="#jfokus-2026-stockholm-nordic-culture-experience">Beyond the Conference: Exploring Nordic Culture</a></li>
  <li><a href="#jfokus-2026-modern-developer-conference-standard">Conclusion: Setting the Standard for Modern Developer Conferences</a></li>
  <li><a href="#cta">Let’s Discuss</a></li>
</ul>

<hr />

<h2 id="-a-milestone-for-the-global-java-community"><a name="jfokus-2026-java-community-milestone"></a> A Milestone for the Global Java Community</h2>

<p><a href="https://jfokus.se/" target="_blank">Jfokus</a> 2026 at the Stockholm Waterfront Congress Centre offered a unique experience for anyone with a professional interest in Java. 
The event marked a milestone for the Swedish developer community, as Jfokus celebrated its 20th anniversary.</p>

<p>Since its humble beginnings in January 2007, when just over 450 Java enthusiasts gathered in Stockholm for the very first edition, the conference has grown into one of Europe’s premier developer events, drawing around 2,000 attendees annually from across the globe. 
The 2026 edition, held from February 2–4, was not just another conference – it was a celebration of twenty years of community, code, and continuous learning.</p>

<h2 id="-from-java-conference-to-multi-track-developer-conference"><a name="jfokus-2026-java-to-multitrack-developer-conference"></a> From Java Conference to Multi-Track Developer Conference</h2>

<p>Founded by <a href="https://www.linkedin.com/in/mattiask/" target="_blank">Mattias Karlsson</a> and organised in partnership with Javaforum Stockholm, Jfokus was driven by a passion to create an unparalleled experience for the global developer community.</p>

<p>Over the past two decades, it has remained at the forefront of software development – evolving from a tightly Java-focused gathering into a broad, multi-track conference covering AI/ML, DevOps, Cloud, and emerging technologies, all while staying true to its developer-first roots.</p>

<h2 id="-two-decades-of-growth-in-the-java-community"><a name="jfokus-2026-20-years-java-community-growth"></a> Two Decades of Growth in the Java Community</h2>

<p>The first Jfokus was held in January 2007 and was an immediate success. 
With more than 450 participants, it became the biggest meeting place in Sweden for Java professionals. 
This marked the beginning of a format that has since gained recognition within the professional developer community.</p>

<p>Attending in 2026 offered a rare opportunity to meet the people who have shaped Jfokus over the years, like Johan Rhedin, who has been instrumental in crafting the conference’s branding and digital identity, or <a href="https://www.linkedin.com/in/jeannegothberg/" target="_blank">Jeanne Göthberg</a>, who has been managing bringing people, ideas, and projects together to move Jfokus forward. 
Nowhere else have I encountered such a high concentration of Java Champions in one room as at the Jfokus Speaker Dinner.</p>

<p>Since its launch in 2007, the conference has developed into an established fixture in the global developer events calendar. 
For two decades, Jfokus has played a significant role in the evolution of Java conferences. 
Over the years, the conference has covered a wide range of Java-related topics and consistently engaged its audience.</p>

<h2 id="-a-unique-atmosphere-where-tech-meets-nordic-mythology"><a name="jfokus-2026-nordic-atmosphere-developer-conference"></a> A Unique Atmosphere: Where Tech Meets Nordic Mythology</h2>

<p>The 20th anniversary highlighted this unique blend – where else do a Viking-inspired atmosphere and modern Java innovation come together so naturally?</p>

<p>This year, world-leading Java experts took the stage, contributing to an atmosphere that felt more like a celebration than a typical conference. 
The opening talk marked the beginning of a saga of fire and ice, creating a distinctive Nordic winter atmosphere with elements inspired by Nordic mythology – including visual effects and fire shows that complemented the theme.</p>

<p><img src="/assets/posts/2026-04-22-Jfokus2026/Jfokus26_opening.png" alt="Opening Jfokus 2026" title="Nordic winter atmosphere at Jfokus 2026" /></p>

<p>The combination of cutting-edge software engineering innovations, imaginative conference design, and top-class performances has attracted an ever-growing number of visitors, including locals, international software developers, and world-class speakers.</p>

<p>The 20th anniversary of Jfokus was a significant milestone, enriched by its Nordic theme. Despite the wintery setting in Stockholm, the atmosphere was warm, driven by engaging conversations and a strong sense of community. 
The organization was smooth, the attendees were amazing, and the Viking spirit of the conference made the event stand out.</p>

<h2 id="-key-topics-java-ai-and-modern-software-engineering-trends"><a name="jfokus-2026-java-ai-software-engineering-trends"></a> Key Topics: Java, AI, and Modern Software Engineering Trends</h2>

<p>Over the years, the conference broadened its topics beyond core Java to include Frontend &amp; Web development, Android &amp; Mobile, Continuous Delivery &amp; DevOps, Cloud &amp; Big Data, Security, and alternative JVM languages.</p>

<p>One key takeaway was how rapidly AI is evolving and increasingly shaping the work of software engineers – as well as the pace at which software technologies themselves are advancing.</p>

<p>The talks and booths at Jfokus were both inspiring and insightful: attendees could learn about the latest features and innovations in Java and AI and how leading industry experts apply them in practice – across all areas of modern software development.</p>

<h2 id="-expo-and-networking-opportunities-at-a-leading-developer-conference"><a name="jfokus-2026-developer-conference-expo-networking"></a> Expo and Networking Opportunities at a leading Developer Conference</h2>

<p>One of the key advantages of attending in person was the exhibition floor, where some of the biggest names in the tech industry had set up booths. 
Companies from the Java ecosystem were represented, making it easy to walk up, start a conversation, and get direct insights from the engineers and advocates who actually build the tools developers use every day.</p>

<p>For many attendees, those informal booth chats turned out to be just as valuable as the sessions themselves.</p>

<h2 id="-personal-highlight-the-mentoring-hub"><a name="jfokus-2026-mentoring-hub-developer-networking"></a> Personal Highlight: The Mentoring Hub</h2>

<p>My personal highlight was the Mentoring Hub: there were so many opportunities to exchange ideas with leading experts – either one-on-one or in small groups – on a wide range of professional development topics.</p>

<p>The mentors were eager to share their experience and knowledge. 
It proved to be an excellent way to gain meaningful advice for career development.</p>

<h2 id="-beyond-the-conference-exploring-nordic-culture"><a name="jfokus-2026-stockholm-nordic-culture-experience"></a> Beyond the Conference: Exploring Nordic Culture</h2>

<p>Attending the conference also offered the opportunity to explore aspects of Viking history and culture, their way of life, and to visit, for example, the Viking Museum in Stockholm – a true Nordic highlight.</p>

<p>Setting the Standard for Modern Developer Conferences</p>

<p>The Jfokus conference series combines emerging technology topics, a distinctive Nordic-inspired atmosphere, excellent speakers, and a strong community focus, and it continues to set a high standard for developer conferences.</p>

<h2 id="-lets-discuss"><a name="cta"></a> Let’s discuss!</h2>

<p>Do you have questions about Jfokus, other developer conferences, or specific developer topics or AI?
<a href="/people/iryna">Feel free to reach out</a>.
I’m always happy to exchange knowledge, ideas, and experiences.</p>]]></content><author><name>iryna</name></author><category term="Conferences" /><category term="Jfokus" /><category term="Java" /><summary type="html"><![CDATA[Explore Jfokus 2026, a leading Java conference covering AI, DevOps, and software engineering trends, bringing together the global developer community.]]></summary></entry><entry><title type="html">Swiss Testing Day 2026 – Reflections on Testing AI and Non-Deterministic Systems</title><link href="https://dev.karakun.com/2026/04/02/Swiss-Testing-Day.html" rel="alternate" type="text/html" title="Swiss Testing Day 2026 – Reflections on Testing AI and Non-Deterministic Systems" /><published>2026-04-01T00:00:00+00:00</published><updated>2026-04-01T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/04/02/SwissTestingDay</id><content type="html" xml:base="https://dev.karakun.com/2026/04/02/Swiss-Testing-Day.html"><![CDATA[<p>At Karakun, we are closely following how software engineering evolves in the age of AI – especially when it comes to testing and reliability.
The Swiss Testing Day 2026 brought together a range of perspectives on exactly this topic: from classical software verification to emerging approaches for testing non-deterministic systems.
I, <a href="/people/Mike">Mike Mannion</a>, attended the conference and captured a set of reflections and observations from selected talks.</p>

<hr />

<h2 id="table-of-contents">Table Of Contents</h2>
<ul>
  <li><a href="#software-verification">Opening Keynote: Software Verification in the Age of AI</a></li>
  <li><a href="#good-bad-ai-testing-strategy">Good AI Testing Strategy / Bad AI Testing Strategy. The difference and why it matters</a></li>
  <li><a href="#agentic-testing-in-banking">Agentic testing in banking: From hype to governed practice</a></li>
  <li><a href="#why-ai-is-useless-for-compliance">Why AI Is Useless for Compliance</a></li>
  <li><a href="#takeaways">Key Takeaways</a></li>
  <li><a href="#Karakun">Karakun Perspective</a></li>
  <li><a href="#cta">Let’s Discuss</a></li>
</ul>

<hr />

<h2 id="-opening-keynote-software-verification-in-the-age-of-ai"><a name="software-verification"></a> Opening Keynote: Software Verification in the Age of AI</h2>

<p><strong>Bertrand Meyer – OO and Software Correctness Pioneer</strong></p>

<p>Bertrand is a personal hero of mine.
His work on software correctness shaped my profile as a software developer the moment I came in contact with it.
In this keynote he covers a wide range of issues, but comes back to the central idea: proving that the software does what it promises; a challenge which LLMs, with their always-present non-determinism, have only made more difficult.</p>

<p><img src="/assets/posts/2026-04-01-Swiss-Testing-Day/1-keynote.jpeg" alt="Betrand Meyer during his opening keynote at Swiss Testing Day 2026" title="Betrand Meyer during his opening keynote at Swiss Testing Day 2026" /></p>

<p>The second line of the following slide is absolutely crucial and a key aspect of the probabilistic testing framework <a href="https://javai.org" target="_blank">PUnit</a>.</p>

<p><img src="/assets/posts/2026-04-01-Swiss-Testing-Day/2-Bullshit-Avoidance-Discipline.jpeg" alt="B.A.D. - Bullshit Avoidance Discipline" title="B.A.D. - Bullshit Avoidance Discipline" /></p>

<p>The performance of stochastic features – which especially includes LLMs – must be measured, because a simple correct/not correct is not sufficient to gauge performance.</p>

<p>But despite this observation, Bertrand does not go into detail about how to measure such systems.
Instead he reiterates the message, which he has been saying for decades: program correctness must be built into the software.</p>

<p>This was, in fact, the genius of his Eiffel language, which unfortunately was not adopted by any mainstream language that followed.
But the principle stands – even if it does not yet fully answer the question of performance of stochastic systems.</p>

<p>He ends on an optimistic note, stressing that the need for good software engineers will not disappear any time soon.
Generated code must still be verified, and human understanding of the code remains key.</p>

<h2 id="-good-ai-testing-strategy--bad-ai-testing-strategy-the-difference-and-why-it-matters"><a name="good-bad-ai-testing-strategy"></a> Good AI Testing Strategy / Bad AI Testing Strategy. The difference and why it matters</h2>

<p><strong>Iosif Itkin</strong></p>

<p>A very philosophical but worthwhile talk. Iosif asks: What is strategy? What is it not?</p>

<p>He challenges us to think about this carefully, and reminds us not to confuse strategy with a list of goals.
It is also not QA; QA requires a different mindset than testing – and that mindset is critical.</p>

<p>He frequently references the book <em>Good Strategy/Bad Strategy</em>.</p>

<p>A useful distinction: QA is not testing.</p>

<p>Iosif is adamant that testing is about identifying bugs.
He does not explicitly include other outputs such as usability feedback, responsiveness data, or success rates for stochastic services.</p>

<p>When I asked him about this, he suggested that these aspects could also be interpreted as “bugs”.
I’m not sure I agree – a suggestion for improvement is not necessarily a bug, but a claim that needs to be evaluated and may evolve into a requirement.</p>

<p>Despite this difference of opinion, the talk is an important reminder: organisations need to think carefully about their testing strategy – not just their tooling.</p>

<h2 id="-agentic-testing-in-banking-from-hype-to-governed-practice"><a name="agentic-testing-in-banking"></a> Agentic testing in banking: From hype to governed practice</h2>

<p><strong>J. Reitermayer, S. Baumberger, M. Hause</strong></p>

<p>Reitermayer presents an agent called “Avalon”, which generates synthetic test data.</p>

<p>He quickly dives into product details, which can be difficult to follow without prior context.
However, one thing is clear: this is a sophisticated agent-based system that delivers measurable productivity gains.</p>

<p>What stands out is the transparency of the system.
The execution is visualised in real time in the UI, exposing LLM interactions, tool calls, and decision steps.</p>

<h2 id="-why-ai-is-useless-for-compliance"><a name="why-ai-is-useless-for-compliance"></a> Why AI Is Useless for Compliance</h2>

<p><strong>Nick Gushchin – AI Transformation Manager</strong></p>

<p><img src="/assets/posts/2026-04-01-Swiss-Testing-Day/3-why-ai-is-useless.jpeg" alt="Opening slide of the talk &quot;Why AI is useless for compliance&quot;" title="Opening Slide &quot;Why AI is useless for Compliance&quot;" /></p>

<p>Nick structures risk into different levels, each requiring different types of tooling.</p>

<p><img src="/assets/posts/2026-04-01-Swiss-Testing-Day/4-roles-and-responsibilities.jpeg" alt="Defining Roles and Responsibilities for AI in Organisation" title="Defining Roles and Responsibilities for AI in Organisation" /></p>

<p>He presents a matrix using likelihood and impact to systematically assess risks associated with AI systems.
This is not a new concept – but its importance applies just as much in the context of AI.</p>

<p><img src="/assets/posts/2026-04-01-Swiss-Testing-Day/5-matrix-ai-agents.jpeg" alt="Cross-Industry Insight: AI Governance Through a Banking Risk Lens" title="Cross-Industry Insight: AI Governance Through a Banking Risk Lens" /></p>

<p>One open question remains: how do we quantify “likelihood” in non-deterministic systems?
Frameworks like <a href="https://javai.org" target="_blank">PUnit</a> (view repository on <a href="https://github.com/javai-org/punit" target="_blank">GitHub</a>) may offer part of the answer here.</p>

<p>This was an outstanding talk – highly practical, with no hype, and extremely relevant for anyone working on testing and reliability in AI-driven systems.</p>

<h2 id="-key-takeaways"><a name="takeaways"></a> Key Takeaways</h2>

<p>Across the talks, a few recurring themes emerged:</p>

<ul>
  <li>
    <p><strong>Non-deterministic systems require new testing approaches</strong>:
Traditional binary correctness is not sufficient for AI-based systems.</p>
  </li>
  <li>
    <p><strong>Measurement becomes critical</strong>:
Observability, probabilities, and performance metrics are central to evaluating stochastic behaviour.</p>
  </li>
  <li>
    <p><strong>Testing is strategic, not just operational</strong>:
Organisations need to actively design how they approach testing – not just execute it.</p>
  </li>
</ul>

<h2 id="-karakun-perspective"><a name="Karakun"></a> Karakun Perspective</h2>

<p>For us at Karakun, these discussions reinforce a key observation:
As AI systems become part of real-world engineering systems, testing can no longer rely on deterministic assumptions.</p>

<p>Instead, we need:</p>
<ul>
  <li>new models for evaluating system behaviour</li>
  <li>transparent and explainable execution</li>
  <li>and engineering practices that integrate correctness and probabilistic performance</li>
</ul>

<p>This is particularly relevant in domains such as automotive, aerospace, and other safety-critical environments – where reliability is non-negotiable.</p>

<h2 id="-lets-discuss"><a name="cta"></a> Let’s discuss!</h2>

<p>The Swiss Testing Day 2026 made one thing very clear:
AI does not eliminate the need for engineering discipline – it increases it.</p>

<p>What are your thoughts on AI and engineering discipline?
<a href="/people/Mike">Feel free to reach out</a>.
I’m always happy to exchange knowledge, ideas, and experiences.</p>]]></content><author><name>mike</name></author><category term="Testing" /><category term="AI" /><summary type="html"><![CDATA[Insights from Swiss Testing Day 2026 on AI testing, non-deterministic systems, and strategies for ensuring reliability in modern software engineering.]]></summary></entry><entry><title type="html">Migrating from Elasticsearch 7.17 to 8.19: A Practical Guide</title><link href="https://dev.karakun.com/2026/03/26/elasticsearch-7-to-8-migration-guide.html" rel="alternate" type="text/html" title="Migrating from Elasticsearch 7.17 to 8.19: A Practical Guide" /><published>2026-03-26T00:00:00+00:00</published><updated>2026-03-26T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/03/26/elasticsearch-migration</id><content type="html" xml:base="https://dev.karakun.com/2026/03/26/elasticsearch-7-to-8-migration-guide.html"><![CDATA[<p>Migrating from Elasticsearch 7.17 to 8.x introduces significant changes in client APIs, security defaults, and index management. 
This article provides a practical migration guide, covering the transition from HLRC to the Java API Client, structured error handling, composable index templates, and production-ready testing strategies.</p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#why-upgrade-elasticsearch-8">Why Upgrade to Elasticsearch 8.x Now?</a></li>
  <li><a href="#migration-overview">Elasticsearch Migration Overview</a></li>
  <li><a href="#replacing-hlrc">Replacing HLRC with the Elasticsearch Java API Client</a></li>
  <li><a href="#structured-error-handling">Structured Error Handling in Elasticsearch Java Client</a></li>
  <li><a href="#elasticsearch-8-index-templates-mapping-changes">Elasticsearch 8 Index Templates and Mappings Changes</a></li>
  <li><a href="#bulk-operations-response-handling">Bulk Operations and Response Handling in Elasticsearch 8 Java Client</a></li>
  <li><a href="#elasticsearch-8-testing-testcontainers">Testing Elasticsearch 8 with Testcontainers</a></li>
  <li><a href="#spring-boot-health-indicator">Spring Boot Elasticsearch Health Indicator Migration</a></li>
  <li><a href="#administrative-operations">Administrative Operations</a></li>
  <li><a href="#elasticsearch-migration-checklist">Elasticsearch Migration Checklist (7.17 to 8.x)</a></li>
  <li><a href="#lessons-learned-from-migration">Key Lessons from the Migration</a></li>
  <li><a href="#elasticsearch-migration-resources">Elasticsearch Migration Resources</a></li>
  <li><a href="#cta">Let’s connect</a></li>
</ul>

<hr />

<p>Elasticsearch 7.x has reached end-of-life, with maintenance ending in April 2025 and support ending in January 2026, prompting many teams to migrate to version 8.x. 
This migration is more than a simple version bump—it requires rethinking how your Java application interacts with Elasticsearch. 
The Java High Level REST Client (HLRC), the primary client library for ES 7.x, is now deprecated in favor of a completely redesigned Java API Client that embraces modern patterns such as builders, functional composition, and strong typing.</p>

<p>This article documents our journey migrating a production Spring Boot application from Elasticsearch 7.17 to 8.19.3, covering the key technical challenges, code transformations, and lessons learned along the way.</p>

<h2 id="-why-upgrade-now"><a name="why-upgrade-elasticsearch-8"></a> Why Upgrade Now?</h2>

<p>Beyond the requirement to remain on a supported version, Elasticsearch 8.19 brings:</p>

<ul>
  <li><strong>Security by default:</strong> TLS and basic authentication are now enabled out of the box</li>
  <li><strong>Performance improvements:</strong> Leveraging Lucene 9.12.2 with numerous bug fixes and optimizations</li>
  <li><strong>Modern API design:</strong> The new Java client offers type-safe requests and responses, reducing runtime errors</li>
  <li><strong>Future-proofing:</strong> Access to vector search, inference APIs, and other 8.x-exclusive features</li>
</ul>

<p>Most importantly, continuing with HLRC means living with a frozen, unmaintained codebase while the ecosystem moves forward.</p>

<h2 id="-the-migration-landscape"><a name="migration-overview"></a> The Migration Landscape</h2>

<p>Our migration touched four major areas:</p>

<ol>
  <li><strong>Client library replacement:</strong> Swapping HLRC for the new typed Java API Client</li>
  <li><strong>Security configuration:</strong> Adapting to Elasticsearch’s security-first defaults</li>
  <li><strong>Index templates and mappings:</strong> Updating to composable templates and changed analyzer semantics</li>
  <li><strong>Error handling:</strong> Reworking exception handling for the new client’s error model</li>
</ol>

<h2 id="-part-1-replacing-the-java-client"><a name="replacing-hlrc"></a> Part 1: Replacing the Java Client</h2>

<h3 id="dependency-updates">Dependency Updates</h3>

<p>The first step was updating our Gradle dependencies:</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Old (ES 7.17)</span>
<span class="n">implementation</span> <span class="s2">"org.elasticsearch.client:elasticsearch-rest-high-level-client:7.17.0"</span>

<span class="c1">// New (ES 8.19)</span>
<span class="n">implementation</span> <span class="s2">"org.elasticsearch.client:elasticsearch-rest-client:8.19.3"</span>
<span class="n">implementation</span> <span class="s2">"co.elastic.clients:elasticsearch-java:8.19.3"</span>
<span class="n">implementation</span> <span class="s2">"jakarta.json:jakarta.json-api:2.1.1"</span>
</code></pre></div></div>

<p>Note that the new client requires a JSON-P implementation. 
We chose Jackson’s JSON-P mapper for seamless integration with our existing Jackson setup.</p>

<h3 id="client-initialization">Client Initialization</h3>

<p>The old HLRC used a simple builder pattern:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ES 7.17 approach</span>
<span class="nc">RestHighLevelClient</span> <span class="n">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RestHighLevelClient</span><span class="o">(</span>
    <span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span>
        <span class="k">new</span> <span class="nf">HttpHost</span><span class="o">(</span><span class="s">"localhost"</span><span class="o">,</span> <span class="mi">9200</span><span class="o">,</span> <span class="s">"http"</span><span class="o">)</span>
    <span class="o">)</span>
<span class="o">);</span>
</code></pre></div></div>

<p>The new client separates concerns between transport and the client itself:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ES 8.19 approach</span>
<span class="nc">RestClient</span> <span class="n">restClient</span> <span class="o">=</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span>
    <span class="k">new</span> <span class="nf">HttpHost</span><span class="o">(</span><span class="s">"localhost"</span><span class="o">,</span> <span class="mi">9200</span><span class="o">,</span> <span class="s">"http"</span><span class="o">)</span>
<span class="o">).</span><span class="na">build</span><span class="o">();</span>

<span class="nc">ElasticsearchTransport</span> <span class="n">transport</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RestClientTransport</span><span class="o">(</span>
    <span class="n">restClient</span><span class="o">,</span> 
    <span class="k">new</span> <span class="nf">JacksonJsonpMapper</span><span class="o">()</span>
<span class="o">);</span>

<span class="nc">ElasticsearchClient</span> <span class="n">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ElasticsearchClient</span><span class="o">(</span><span class="n">transport</span><span class="o">);</span>
</code></pre></div></div>

<h3 id="authentication-and-security">Authentication and Security</h3>

<p>Elasticsearch 8.x enables security by default. 
For production environments, we added basic authentication:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RestClientBuilder</span> <span class="n">builder</span> <span class="o">=</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="n">httpHosts</span><span class="o">)</span>
    <span class="o">.</span><span class="na">setRequestConfigCallback</span><span class="o">(</span><span class="n">cfg</span> <span class="o">-&gt;</span> 
        <span class="n">cfg</span><span class="o">.</span><span class="na">setSocketTimeout</span><span class="o">(</span><span class="n">timeoutInSeconds</span> <span class="o">*</span> <span class="mi">1000</span><span class="o">)</span>
    <span class="o">);</span>

<span class="k">if</span> <span class="o">(</span><span class="n">authEnabled</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">CredentialsProvider</span> <span class="n">credentials</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BasicCredentialsProvider</span><span class="o">();</span>
    <span class="n">credentials</span><span class="o">.</span><span class="na">setCredentials</span><span class="o">(</span>
        <span class="nc">AuthScope</span><span class="o">.</span><span class="na">ANY</span><span class="o">,</span>
        <span class="k">new</span> <span class="nf">UsernamePasswordCredentials</span><span class="o">(</span><span class="n">username</span><span class="o">,</span> <span class="n">password</span><span class="o">)</span>
    <span class="o">);</span>
    <span class="n">builder</span><span class="o">.</span><span class="na">setHttpClientConfigCallback</span><span class="o">(</span><span class="n">cb</span> <span class="o">-&gt;</span> 
        <span class="n">cb</span><span class="o">.</span><span class="na">setDefaultCredentialsProvider</span><span class="o">(</span><span class="n">credentials</span><span class="o">)</span>
    <span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>For local development and temporary flexibility, we have the option to disable the security in <code class="language-plaintext highlighter-rouge">docker-compose.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">elasticsearch</span><span class="pi">:</span>
  <span class="na">image</span><span class="pi">:</span> <span class="s">docker.elastic.co/elasticsearch/elasticsearch:8.19.3</span>
  <span class="na">environment</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">xpack.security.enabled=false</span>
    <span class="pi">-</span> <span class="s">discovery.type=single-node</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s2">"</span><span class="s">9200:9200"</span>
</code></pre></div></div>

<h3 id="requestresponse-pattern-changes">Request/Response Pattern Changes</h3>

<p>The new client’s biggest shift is from generic maps to strongly-typed builders and response objects.</p>

<p><strong>Old search operation (ES 7.17):</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">SearchRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SearchRequest</span><span class="o">(</span><span class="n">indexName</span><span class="o">);</span>
<span class="nc">SearchSourceBuilder</span> <span class="n">sourceBuilder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SearchSourceBuilder</span><span class="o">();</span>
<span class="n">sourceBuilder</span><span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="nc">QueryBuilders</span><span class="o">.</span><span class="na">matchQuery</span><span class="o">(</span><span class="s">"field"</span><span class="o">,</span> <span class="s">"value"</span><span class="o">));</span>
<span class="n">request</span><span class="o">.</span><span class="na">source</span><span class="o">(</span><span class="n">sourceBuilder</span><span class="o">);</span>

<span class="nc">SearchResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="nc">RequestOptions</span><span class="o">.</span><span class="na">DEFAULT</span><span class="o">);</span>
<span class="nc">SearchHit</span><span class="o">[]</span> <span class="n">hits</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">getHits</span><span class="o">().</span><span class="na">getHits</span><span class="o">();</span>
</code></pre></div></div>

<p><strong>New search operation (ES 8.19):</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">SearchResponse</span><span class="o">&lt;</span><span class="nc">MyDocument</span><span class="o">&gt;</span> <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span>
    <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="n">indexName</span><span class="o">)</span>
    <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="n">q</span> <span class="o">-&gt;</span> <span class="n">q</span>
        <span class="o">.</span><span class="na">match</span><span class="o">(</span><span class="n">m</span> <span class="o">-&gt;</span> <span class="n">m</span>
            <span class="o">.</span><span class="na">field</span><span class="o">(</span><span class="s">"field"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="s">"value"</span><span class="o">)</span>
        <span class="o">)</span>
    <span class="o">),</span>
    <span class="nc">MyDocument</span><span class="o">.</span><span class="na">class</span>
<span class="o">);</span>

<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Hit</span><span class="o">&lt;</span><span class="nc">MyDocument</span><span class="o">&gt;&gt;</span> <span class="n">hits</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">hits</span><span class="o">().</span><span class="na">hits</span><span class="o">();</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">Hit</span><span class="o">&lt;</span><span class="nc">MyDocument</span><span class="o">&gt;</span> <span class="n">hit</span> <span class="o">:</span> <span class="n">hits</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">MyDocument</span> <span class="n">doc</span> <span class="o">=</span> <span class="n">hit</span><span class="o">.</span><span class="na">source</span><span class="o">();</span>
    <span class="c1">// Strongly typed access to your document</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The functional builder pattern takes some getting used to, but it eliminates entire categories of errors by enforcing type safety at compile time.</p>

<h3 id="handling-field-values">Handling Field Values</h3>

<p>One subtle change: the new client introduces <code class="language-plaintext highlighter-rouge">FieldValue</code> as a wrapper for all dynamic values in queries, aggregations, and scripts.</p>

<p><strong>Search-after tokens</strong> must now be explicitly converted:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Old</span>
<span class="n">searchAfter</span><span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="s">"value1"</span><span class="o">,</span> <span class="mi">123</span><span class="o">,</span> <span class="n">timestamp</span><span class="o">))</span>

<span class="c1">// New</span>
<span class="n">searchAfter</span><span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span>
    <span class="nc">FieldValue</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"value1"</span><span class="o">),</span>
    <span class="nc">FieldValue</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">123</span><span class="o">),</span>
    <span class="nc">FieldValue</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">timestamp</span><span class="o">.</span><span class="na">toEpochMilli</span><span class="o">())</span>
<span class="o">))</span>
</code></pre></div></div>

<p><strong>Script parameters</strong> need similar wrapping when passing variables to Painless scripts in aggregations or updates:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// New</span>
<span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">JsonData</span><span class="o">&gt;</span> <span class="n">params</span> <span class="o">=</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
    <span class="s">"boost"</span><span class="o">,</span> <span class="nc">JsonData</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mf">1.5</span><span class="o">),</span>
    <span class="s">"field_value"</span><span class="o">,</span> <span class="nc">JsonData</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"some_text"</span><span class="o">)</span>
<span class="o">);</span>
</code></pre></div></div>

<h2 id="-part-2-structured-error-handling"><a name="structured-error-handling"></a> Part 2: Structured Error Handling</h2>

<p>The HLRC threw generic <code class="language-plaintext highlighter-rouge">ElasticsearchException</code> instances that required parsing error messages as strings. 
The new client provides structured error information through <code class="language-plaintext highlighter-rouge">ErrorCause</code>.</p>

<p>We created an enum to classify error types systematically:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="nc">ElasticsearchErrorKind</span> <span class="o">{</span>
    <span class="no">INDEX_NOT_FOUND</span><span class="o">(</span><span class="s">"index_not_found_exception"</span><span class="o">,</span> <span class="kc">false</span><span class="o">),</span>
    <span class="no">INDEX_CLOSED</span><span class="o">(</span><span class="s">"index_closed_exception"</span><span class="o">,</span> <span class="kc">false</span><span class="o">),</span>
    <span class="no">CLUSTER_BLOCK</span><span class="o">(</span><span class="s">"cluster_block_exception"</span><span class="o">,</span> <span class="kc">true</span><span class="o">),</span>
    <span class="no">VERSION_CONFLICT</span><span class="o">(</span><span class="s">"version_conflict_engine_exception"</span><span class="o">,</span> <span class="kc">true</span><span class="o">),</span>
    <span class="no">ES_REJECTED_EXECUTION</span><span class="o">(</span><span class="s">"es_rejected_execution_exception"</span><span class="o">,</span> <span class="kc">true</span><span class="o">),</span>
    <span class="no">TIMEOUT</span><span class="o">(</span><span class="s">"timeout_exception"</span><span class="o">,</span> <span class="kc">true</span><span class="o">),</span>
    <span class="no">UNKNOWN</span><span class="o">(</span><span class="s">"_unknown"</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">type</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">boolean</span> <span class="n">recoverable</span><span class="o">;</span>

    <span class="c1">// Constructor and methods...</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">ElasticsearchErrorKind</span> <span class="nf">fromErrorCause</span><span class="o">(</span><span class="nc">ErrorCause</span> <span class="n">cause</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">cause</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="no">UNKNOWN</span><span class="o">;</span>
        <span class="nc">String</span> <span class="n">type</span> <span class="o">=</span> <span class="n">cause</span><span class="o">.</span><span class="na">type</span><span class="o">();</span>
        
        <span class="c1">// Check nested root causes</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ErrorCause</span><span class="o">&gt;</span> <span class="n">rootCause</span> <span class="o">=</span> <span class="n">cause</span><span class="o">.</span><span class="na">rootCause</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">rootCause</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">rootCause</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">type</span> <span class="o">=</span> <span class="n">rootCause</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="mi">0</span><span class="o">).</span><span class="na">type</span><span class="o">();</span>
        <span class="o">}</span>
        
        <span class="k">return</span> <span class="nf">fromType</span><span class="o">(</span><span class="n">type</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This allowed us to build intelligent retry logic:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">operation</span><span class="o">.</span><span class="na">execute</span><span class="o">();</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">ElasticsearchException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">ElasticsearchErrorKind</span> <span class="n">kind</span> <span class="o">=</span> 
        <span class="nc">ElasticsearchErrorKind</span><span class="o">.</span><span class="na">fromErrorCause</span><span class="o">(</span><span class="n">e</span><span class="o">.</span><span class="na">error</span><span class="o">());</span>
    
    <span class="k">if</span> <span class="o">(</span><span class="n">kind</span><span class="o">.</span><span class="na">isRecoverable</span><span class="o">()</span> <span class="o">&amp;&amp;</span> <span class="n">retryCount</span> <span class="o">&lt;</span> <span class="n">maxRetries</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="n">backoffMs</span><span class="o">);</span>
        <span class="k">return</span> <span class="nf">retryOperation</span><span class="o">(</span><span class="n">operation</span><span class="o">,</span> <span class="n">retryCount</span> <span class="o">+</span> <span class="mi">1</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">throw</span> <span class="n">e</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="-part-3-index-templates-and-mappings"><a name="elasticsearch-8-index-templates-mapping-changes"></a> Part 3: Index Templates and Mappings</h2>

<p>Elasticsearch 8.x introduces <strong>composable index templates</strong>, replacing the legacy template format. 
While our templates were relatively straightforward, we had to:</p>

<ol>
  <li><strong>Update analyzer configurations</strong>: Some token filters changed names (e.g., <code class="language-plaintext highlighter-rouge">french_elision</code> syntax)</li>
  <li><strong>Switch to explicit normalizers</strong>: Keyword fields now use explicit normalizer definitions</li>
  <li><strong>Fix deprecated syntax</strong>: Date histogram intervals like <code class="language-plaintext highlighter-rouge">1M</code> must now be spelled out as <code class="language-plaintext highlighter-rouge">month</code></li>
</ol>

<p>Example template structure for ES 8.19:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"index_patterns"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"my-index-*"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"template"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"settings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"number_of_shards"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"number_of_replicas"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"refresh_interval"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1s"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"analysis"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"normalizer"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"lowercase_normalizer"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"custom"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"filter"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"lowercase"</span><span class="p">,</span><span class="w"> </span><span class="s2">"asciifolding"</span><span class="p">]</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"analyzer"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"custom_analyzer"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"custom"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"tokenizer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"standard"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"filter"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"lowercase"</span><span class="p">,</span><span class="w"> </span><span class="s2">"stop"</span><span class="p">,</span><span class="w"> </span><span class="s2">"synonym_graph"</span><span class="p">]</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"mappings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"text"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"analyzer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"custom_analyzer"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"keyword"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"normalizer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"lowercase_normalizer"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="template-versioning-strategy">Template Versioning Strategy</h3>

<p>We implemented automatic template updates by tracking versions. 
While template versioning existed in ES 7.x, the API for accessing version metadata changed with the new client.</p>

<p><strong>Old approach (ES 7.17 with HLRC):</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">GetIndexTemplatesResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="na">indices</span><span class="o">()</span>
    <span class="o">.</span><span class="na">getIndexTemplate</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="nc">RequestOptions</span><span class="o">.</span><span class="na">DEFAULT</span><span class="o">);</span>

<span class="nc">Long</span> <span class="n">remoteVersion</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">getIndexTemplates</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="mi">0</span><span class="o">).</span><span class="na">version</span><span class="o">();</span>
</code></pre></div></div>
<p><strong>New approach (ES 8.19 with new client):</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">GetIndexTemplateResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="na">indices</span><span class="o">()</span>
        <span class="o">.</span><span class="na">getIndexTemplate</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>

<span class="nc">Long</span> <span class="n">remoteVersion</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">indexTemplates</span><span class="o">().</span><span class="na">stream</span><span class="o">()</span>
        <span class="o">.</span><span class="na">findFirst</span><span class="o">()</span>
        <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">t</span> <span class="o">-&gt;</span> <span class="n">t</span><span class="o">.</span><span class="na">indexTemplate</span><span class="o">().</span><span class="na">version</span><span class="o">())</span>  <span class="c1">// Strongly typed access</span>
        <span class="o">.</span><span class="na">orElse</span><span class="o">(</span><span class="mi">0L</span><span class="o">);</span>

<span class="kt">long</span> <span class="n">localVersion</span> <span class="o">=</span> <span class="n">extractVersionFromTemplate</span><span class="o">(</span><span class="n">templateContent</span><span class="o">);</span>

<span class="k">if</span> <span class="o">(</span><span class="n">localVersion</span> <span class="o">&gt;</span> <span class="n">remoteVersion</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">client</span><span class="o">.</span><span class="na">indices</span><span class="o">().</span><span class="na">putIndexTemplate</span><span class="o">(</span><span class="n">t</span> <span class="o">-&gt;</span> <span class="n">t</span>
        <span class="o">.</span><span class="na">name</span><span class="o">(</span><span class="n">templateName</span><span class="o">)</span>
        <span class="o">.</span><span class="na">indexPatterns</span><span class="o">(</span><span class="n">patterns</span><span class="o">)</span>
        <span class="o">.</span><span class="na">template</span><span class="o">(</span><span class="n">templateBody</span><span class="o">)</span>
    <span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="-part-4-bulk-operations-and-response-handling"><a name="bulk-operations-response-handling"></a> Part 4: Bulk Operations and Response Handling</h2>

<p>Bulk indexing saw significant API changes. 
The new client provides cleaner separation between successful and failed operations:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">BulkResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="na">bulk</span><span class="o">(</span><span class="n">b</span> <span class="o">-&gt;</span> <span class="o">{</span>
    <span class="k">for</span> <span class="o">(</span><span class="nc">Document</span> <span class="n">doc</span> <span class="o">:</span> <span class="n">documents</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">b</span><span class="o">.</span><span class="na">operations</span><span class="o">(</span><span class="n">op</span> <span class="o">-&gt;</span> <span class="n">op</span>
            <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="n">idx</span> <span class="o">-&gt;</span> <span class="n">idx</span>
                <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="n">indexName</span><span class="o">)</span>
                <span class="o">.</span><span class="na">id</span><span class="o">(</span><span class="n">doc</span><span class="o">.</span><span class="na">getId</span><span class="o">())</span>
                <span class="o">.</span><span class="na">document</span><span class="o">(</span><span class="n">doc</span><span class="o">)</span>
            <span class="o">)</span>
        <span class="o">);</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">b</span><span class="o">;</span>
<span class="o">});</span>

<span class="k">if</span> <span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">errors</span><span class="o">())</span> <span class="o">{</span>
    <span class="k">for</span> <span class="o">(</span><span class="nc">BulkResponseItem</span> <span class="n">item</span> <span class="o">:</span> <span class="n">response</span><span class="o">.</span><span class="na">items</span><span class="o">())</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">item</span><span class="o">.</span><span class="na">error</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">ElasticsearchErrorKind</span> <span class="n">kind</span> <span class="o">=</span> 
                <span class="nc">ElasticsearchErrorKind</span><span class="o">.</span><span class="na">fromErrorCause</span><span class="o">(</span><span class="n">item</span><span class="o">.</span><span class="na">error</span><span class="o">());</span>
            
            <span class="k">if</span> <span class="o">(</span><span class="n">kind</span> <span class="o">==</span> <span class="nc">ElasticsearchErrorKind</span><span class="o">.</span><span class="na">VERSION_CONFLICT</span><span class="o">)</span> <span class="o">{</span>
                <span class="c1">// Handle version conflict specifically</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Bulk operation failed for {}: {}"</span><span class="o">,</span> 
                    <span class="n">item</span><span class="o">.</span><span class="na">id</span><span class="o">(),</span> <span class="n">item</span><span class="o">.</span><span class="na">error</span><span class="o">().</span><span class="na">reason</span><span class="o">());</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="-part-5-testing-infrastructure"><a name="elasticsearch-8-testing-testcontainers"></a> Part 5: Testing Infrastructure</h2>

<p>We updated our testing stack to use Elasticsearch 8 Testcontainers:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Container</span>
<span class="kd">static</span> <span class="nc">ElasticsearchContainer</span> <span class="n">elasticsearchContainer</span> <span class="o">=</span> 
    <span class="k">new</span> <span class="nf">ElasticsearchContainer</span><span class="o">(</span>
        <span class="s">"docker.elastic.co/elasticsearch/elasticsearch:8.19.3"</span>
    <span class="o">)</span>
    <span class="o">.</span><span class="na">withEnv</span><span class="o">(</span><span class="s">"xpack.security.enabled"</span><span class="o">,</span> <span class="s">"false"</span><span class="o">)</span>
    <span class="o">.</span><span class="na">withEnv</span><span class="o">(</span><span class="s">"ES_JAVA_OPTS"</span><span class="o">,</span> <span class="s">"-Xms512m -Xmx512m"</span><span class="o">);</span>

<span class="nd">@DynamicPropertySource</span>
<span class="kd">static</span> <span class="kt">void</span> <span class="nf">elasticsearchProperties</span><span class="o">(</span><span class="nc">DynamicPropertyRegistry</span> <span class="n">registry</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">registry</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"elasticsearch.nodes[0].host"</span><span class="o">,</span> 
        <span class="nl">elasticsearchContainer:</span><span class="o">:</span><span class="n">getHost</span><span class="o">);</span>
    <span class="n">registry</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"elasticsearch.nodes[0].port"</span><span class="o">,</span> 
        <span class="nl">elasticsearchContainer:</span><span class="o">:</span><span class="n">getFirstMappedPort</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="testing-pitfall-wildcard-deletes">Testing Pitfall: Wildcard Deletes</h3>

<p>Elasticsearch 8 rejects wildcard index deletions by default. Our test cleanup code needed updating:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Old approach (fails in ES 8)</span>
<span class="n">client</span><span class="o">.</span><span class="na">indices</span><span class="o">().</span><span class="na">delete</span><span class="o">(</span><span class="n">d</span> <span class="o">-&gt;</span> <span class="n">d</span><span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="s">"test-*"</span><span class="o">));</span>

<span class="c1">// New approach</span>
<span class="nc">GetIndexResponse</span> <span class="n">indices</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="na">indices</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">g</span> <span class="o">-&gt;</span> <span class="n">g</span><span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="s">"test-*"</span><span class="o">));</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">indexName</span> <span class="o">:</span> <span class="n">indices</span><span class="o">.</span><span class="na">result</span><span class="o">().</span><span class="na">keySet</span><span class="o">())</span> <span class="o">{</span>
    <span class="n">client</span><span class="o">.</span><span class="na">indices</span><span class="o">().</span><span class="na">delete</span><span class="o">(</span><span class="n">d</span> <span class="o">-&gt;</span> <span class="n">d</span><span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="n">indexName</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="-part-6-health-indicator-updates"><a name="spring-boot-health-indicator"></a> Part 6: Health Indicator Updates</h2>

<p>Spring Boot’s default <code class="language-plaintext highlighter-rouge">ElasticsearchHealthIndicator</code> still relies on HLRC. 
We replaced it with a custom implementation:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span><span class="o">(</span><span class="s">"elasticsearch"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomElasticsearchHealthIndicator</span> <span class="kd">implements</span> <span class="nc">HealthIndicator</span> <span class="o">{</span>
    
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ElasticsearchClient</span> <span class="n">client</span><span class="o">;</span>
    
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Health</span> <span class="nf">health</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(!</span><span class="n">client</span><span class="o">.</span><span class="na">ping</span><span class="o">().</span><span class="na">value</span><span class="o">())</span> <span class="o">{</span>
                <span class="k">return</span> <span class="nc">Health</span><span class="o">.</span><span class="na">down</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">withDetail</span><span class="o">(</span><span class="s">"error"</span><span class="o">,</span> <span class="s">"Ping failed"</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">();</span>
            <span class="o">}</span>
            
            <span class="nc">RestClient</span> <span class="n">lowLevel</span> <span class="o">=</span> 
                <span class="o">((</span><span class="nc">RestClientTransport</span><span class="o">)</span> <span class="n">client</span><span class="o">.</span><span class="na">_transport</span><span class="o">()).</span><span class="na">restClient</span><span class="o">();</span>
            <span class="nc">Request</span> <span class="n">req</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Request</span><span class="o">(</span><span class="s">"GET"</span><span class="o">,</span> <span class="s">"/_cluster/health"</span><span class="o">);</span>
            <span class="nc">Response</span> <span class="n">resp</span> <span class="o">=</span> <span class="n">lowLevel</span><span class="o">.</span><span class="na">performRequest</span><span class="o">(</span><span class="n">req</span><span class="o">);</span>
            
            <span class="nc">JsonNode</span> <span class="n">json</span> <span class="o">=</span> <span class="n">mapper</span><span class="o">.</span><span class="na">readTree</span><span class="o">(</span><span class="n">resp</span><span class="o">.</span><span class="na">getEntity</span><span class="o">().</span><span class="na">getContent</span><span class="o">());</span>
            <span class="nc">String</span> <span class="n">status</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="na">path</span><span class="o">(</span><span class="s">"status"</span><span class="o">).</span><span class="na">asText</span><span class="o">(</span><span class="s">"red"</span><span class="o">);</span>
            
            <span class="kt">boolean</span> <span class="n">up</span> <span class="o">=</span> <span class="s">"green"</span><span class="o">.</span><span class="na">equalsIgnoreCase</span><span class="o">(</span><span class="n">status</span><span class="o">)</span> <span class="o">||</span> 
                        <span class="s">"yellow"</span><span class="o">.</span><span class="na">equalsIgnoreCase</span><span class="o">(</span><span class="n">status</span><span class="o">);</span>
            
            <span class="k">return</span> <span class="o">(</span><span class="n">up</span> <span class="o">?</span> <span class="nc">Health</span><span class="o">.</span><span class="na">up</span><span class="o">()</span> <span class="o">:</span> <span class="nc">Health</span><span class="o">.</span><span class="na">down</span><span class="o">())</span>
                <span class="o">.</span><span class="na">withDetail</span><span class="o">(</span><span class="s">"status"</span><span class="o">,</span> <span class="n">status</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
                
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">Health</span><span class="o">.</span><span class="na">down</span><span class="o">(</span><span class="n">e</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Don’t forget to disable the default indicator in <code class="language-plaintext highlighter-rouge">application.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">management</span><span class="pi">:</span>
  <span class="na">health</span><span class="pi">:</span>
    <span class="na">elasticsearch</span><span class="pi">:</span>
      <span class="na">enabled</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<h2 id="-part-7-administrative-operations"><a name="administrative-operations"></a> Part 7: Administrative Operations</h2>

<p>A critical change in ES 8 involves the <code class="language-plaintext highlighter-rouge">ignore_unavailable</code> parameter. 
Previously, setting this to <code class="language-plaintext highlighter-rouge">true</code> for admin operations would silently succeed even if indices didn’t exist—useful for idempotent cleanup scripts but dangerous for user-triggered actions.</p>

<p>We now explicitly set <code class="language-plaintext highlighter-rouge">ignore_unavailable=false</code> for user-facing operations:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">client</span><span class="o">.</span><span class="na">indices</span><span class="o">().</span><span class="na">delete</span><span class="o">(</span><span class="n">d</span> <span class="o">-&gt;</span> <span class="n">d</span>
    <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="n">indexName</span><span class="o">)</span>
    <span class="o">.</span><span class="na">ignoreUnavailable</span><span class="o">(</span><span class="kc">false</span><span class="o">)</span>  <span class="c1">// Fail loudly if index doesn't exist</span>
<span class="o">);</span>
</code></pre></div></div>

<p>This surfaces proper errors to the UI when users attempt invalid operations.</p>

<h2 id="-migration-checklist"><a name="elasticsearch-migration-checklist"></a> Migration Checklist</h2>

<p>Based on our experience, here’s a practical checklist for teams undertaking this migration:</p>

<h3 id="pre-migration">Pre-Migration</h3>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Audit all usages of <code class="language-plaintext highlighter-rouge">RestHighLevelClient</code> in your codebase</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Document custom analyzer and token filter configurations</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Review security requirements (TLS certificates, authentication)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Plan for breaking changes in REST API responses</li>
</ul>

<h3 id="code-changes">Code Changes</h3>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Update Gradle/Maven dependencies to ES 8.19.3</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Replace <code class="language-plaintext highlighter-rouge">RestHighLevelClient</code> with <code class="language-plaintext highlighter-rouge">ElasticsearchClient</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Refactor all search operations to use fluent builders</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Wrap dynamic values with <code class="language-plaintext highlighter-rouge">FieldValue</code> or <code class="language-plaintext highlighter-rouge">JsonData</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Update bulk operation handling for new response structure</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Implement structured error classification</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Replace Spring Boot’s default Elasticsearch health indicator</li>
</ul>

<h3 id="configuration">Configuration</h3>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Update index templates to composable format</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Validate and update analyzer configurations</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Configure authentication for production environments</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Disable security for local development (if appropriate)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Set explicit <code class="language-plaintext highlighter-rouge">ignore_unavailable</code> values for admin operations</li>
</ul>

<h3 id="testing">Testing</h3>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Upgrade Testcontainers to use Elasticsearch 8.19.3</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Fix test cleanup to avoid wildcard deletes</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Add tests for highlighting, aggregations, and spellcheck</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Verify security configuration in integration tests</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Test error handling for all error kinds</li>
</ul>

<h3 id="deployment">Deployment</h3>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Update Docker Compose files for local development</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Plan production rollout (rolling restart vs. reindex)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Monitor cluster health during initial deployment</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Verify application logs for migration-related warnings</li>
</ul>

<h2 id="-lessons-learned"><a name="lessons-learned-from-migration"></a> Lessons Learned</h2>

<ol>
  <li>
    <p><strong>Strong typing prevents runtime surprises</strong>: While the functional builder syntax felt verbose initially, it caught numerous bugs at compile time that would have been production incidents.</p>
  </li>
  <li>
    <p><strong>Error handling needs a strategy</strong>: Don’t treat all Elasticsearch exceptions the same. Classify them, log context-rich messages, and implement smart retry logic for recoverable errors.</p>
  </li>
  <li>
    <p><strong>Security isn’t optional anymore</strong>: ES 8’s security-first approach is the right move, but it requires thoughtful configuration management across environments.</p>
  </li>
  <li>
    <p><strong>Test with the real version</strong>: Don’t rely on in-memory fake implementations. Use Testcontainers with the exact Elasticsearch version you’ll run in production.</p>
  </li>
  <li>
    <p><strong>Index templates matter</strong>: Small changes in analyzer behavior can subtly break search quality. Diff your templates carefully and test with production-like data volumes.</p>
  </li>
</ol>

<h2 id="looking-forward">Looking Forward</h2>

<p>With Elasticsearch 8.19 in place, we’re positioned to explore capabilities that were painful or impossible in 7.x:</p>

<ul>
  <li><strong>Vector search</strong> for semantic similarity</li>
  <li><strong>Inference endpoints</strong> for ML-powered features</li>
  <li><strong>Runtime fields</strong> for schema flexibility</li>
  <li><strong>Improved aggregation performance</strong> for analytics workloads</li>
</ul>

<p>This type of migration is substantial, typically touching 50-150+ files depending on your codebase size. 
But the result is a more maintainable, type-safe, and future-proof integration with Elasticsearch.</p>

<h2 id="-lets-connect"><a name="cta"></a> Let’s Connect!</h2>

<p>Do you have questions about our migration to Elasticsearch 8.19? 
Or would you like to discuss the best migration path for your installation?
Did you already migrate and experienced other pitfalls?
<a href="/people/jatin">Feel free to reach out.</a> 
I’m always happy to exchange knowledge, ideas, and experiences.</p>

<h2 id="-resources"><a name="elasticsearch-migration-resources"></a> Resources</h2>

<ul>
  <li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.19/migrating-8.19.html" target="_blank">Official ES 8.19 Migration Guide</a></li>
  <li><a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/8.19/index.html" target="_blank">Java API Client Documentation</a></li>
  <li><a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/8.19/migrate-hlrc.html" target="_blank">Migrating from HLRC</a></li>
</ul>]]></content><author><name>jatin</name></author><category term="Elasticsearch" /><category term="Search" /><summary type="html"><![CDATA[Practical guide to migrating from Elasticsearch 7.17 to 8.x. Learn how to replace HLRC, adopt the Java API Client, update security settings, and handle breaking changes.]]></summary></entry><entry><title type="html">Accessibility: Going from Couch to Marathon</title><link href="https://dev.karakun.com/2026/03/06/Accessibility.html" rel="alternate" type="text/html" title="Accessibility: Going from Couch to Marathon" /><published>2026-03-06T00:00:00+00:00</published><updated>2026-03-06T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/03/06/Accessbility</id><content type="html" xml:base="https://dev.karakun.com/2026/03/06/Accessibility.html"><![CDATA[<p>Accessibility matters in 2025 because inclusive digital experiences are no longer optional — they’re required by law and expected by users. 
As the European Accessibility Act nears enforcement, developers and designers must adopt WCAG 2.2 and inclusive design practices to ensure equal access, usability, and long-term compliance.</p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#gettingstarted">Getting Started: Information and Research</a></li>
  <li><a href="#Practice">Putting Accessibility into Practice</a></li>
  <li><a href="#share">Share Your Accessibility Journey</a></li>
  <li><a href="#cta">Let’s connect</a></li>
</ul>

<hr />

<p>“Next year I will run a marathon.” 
What a wonderful and challenging New Year’s resolution. 
On the first possible day (not too cold, not too wet, not too sunny, not on a weekend) I went out in my new running gear. 
The first kilometer felt ok. 
The second was already quite hard. 
By the third, I stopped running and walked back home. 
Never again. 
There went my New Year’s resolution. 
I went running a few more times but finally gave up on it. 
Sounds familiar to you? What made you give up your resolution?</p>

<p>For me, it was this massive mountain I saw in front of me. 
This marathon thing was huge, and I could not even run a kilometer without almost dying. 
My goal was too ambitious, and even though I started, I was never able to pull through.</p>

<p>Let’s take my sporty ambitions into the software world. 
Imagine someone (for example, the EU and their laws) saying that from now on <a href="https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/union-equality-strategy-rights-persons-disabilities-2021-2030/european-accessibility-act_en" target="_blank">you have to implement accessibility</a>. 
For me, that sounds almost as impossible as my marathon mountain. 
We are sitting on the couch, bag of crisps in our hand, watching it all happen. 
But how can we start moving? 
I mean, it already is the law in Europe. 
And by the way, accessibility does not only mean catering to blind or deaf people. 
There are so many more disabilities which are not always visible or even permanent. 
In the end, <a href="https://www.w3.org/WAI/people/">it helps all users when our products are more accessible</a> because they become more user-friendly. 
Now let’s get our running gear together.</p>

<h2 id="-getting-started-information-and-research"><a name="gettingstarted"></a> Getting Started: Information and Research</h2>

<p>You have to have some basics, some knowledge, a little bit of background. 
You will find a ton of blogs, videos, and tutorials out there, and I will give you a short list to get started on the topic. 
You wouldn’t go running in your flip-flops, would you?</p>

<ul>
  <li><a href="https://www.w3.org/TR/UNDERSTANDING-WCAG20/Overview.html" target="_blank">Understanding WCAG 2.0</a> - For those of you who would like to figure it out by themselves.</li>
  <li><a href="https://tink.uk/" target="_blank">Blog by Leonie Watson</a> <a href="https://tink.uk/" target="_blank">https://tink.uk/</a> - Topics around web accessibility.</li>
  <li><a href="https://www.udemy.com/course/the-ux-designers-accessibility-guide/" target="_blank">Udemy course by Liz Brown</a> - Not free but definitely worth it.</li>
  <li><a href="https://accessibility-cookbook.com/" target="_blank">Web Accessibility Cookbook</a> by <a href="https://www.linkedin.com/in/matuzo/" target="_blank">Manuel Matuzovic</a> - You can also book Manuel for in-house workshops (highly recommended) and watch <a href="https://www.youtube.com/watch?v=Wno1IhEBTxc" target="_blank">his talk at beyond tellerrand 2023</a>.</li>
</ul>

<hr />

<h2 id="-putting-accessibility-into-practice"><a name="Practice"></a> Putting Accessibility into Practice</h2>

<p>Ok, that’s the list. 
The second step: just go out and do it. 
Once you’ve got your gear together, you have to take the first step. 
Go out and run. 
Go out and program. 
You don’t need to restructure your whole website or application. 
That’s for the next level, when you start on a new project. 
But for now, let’s go one ticket at a time. 
How do your focus styles look? 
Are there any? 
No? 
Create some. 
Do you have alt text for your images? 
No? 
Start from here. 
Are all your input fields connected to labels? 
Go ahead and check. 
These are all very simple tasks that can be done in a short time, but they get you started. 
And once you start, you’ll notice it’s hard to stop. 
There’s more to learn and more to do out there.</p>

<h2 id="-share-your-accessibility-journey"><a name="share"></a> Share Your Accessibility Journey</h2>

<p>Ok, I said the list was finished by #2, but this is an easy one. 
Go out and talk about what you did. 
You implemented that one style? 
You added a descriptive label to a button? 
You checked out the interface using a screen reader? 
Tell your colleagues. 
Let them get inspired.</p>

<p>Now we’re off the couch, slowly heading towards this mountain. 
But we’ll tackle it one step at a time, because we’re in it for the long run. 
To be honest, accessibility is a marathon. 
It takes time to implement and might seem like it never finishes. 
And it is not about checking the box but about the mindset. 
Once you have the running bug (or the accessibility bug),  it is hard not to do it.</p>

<p>Ready? Steady? Go!</p>

<h2 id="-lets-connect"><a name="cta"></a> Let’s Connect!</h2>
<p>Do you have questions about the European Accessibility Act? 
Or would you like to discuss what is the best starting point for accessibility?
<a href="/people/cindy">Feel free to reach out.</a> 
I’m always happy to exchange knowledge, ideas, and experiences.</p>]]></content><author><name>cindy</name></author><category term="Accessibility" /><category term="WCAG" /><category term="UX" /><summary type="html"><![CDATA[Discover how to start with digital accessibility, apply WCAG 2.2 principles, and build inclusive design habits — one step at a time.]]></summary></entry><entry><title type="html">Retrofitting an Existing Spring Application with AI Capabilities Using Spring AI</title><link href="https://dev.karakun.com/2026/02/20/Retrofitting-AI.html" rel="alternate" type="text/html" title="Retrofitting an Existing Spring Application with AI Capabilities Using Spring AI" /><published>2026-02-20T00:00:00+00:00</published><updated>2026-02-20T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/02/20/AI-Retrofit</id><content type="html" xml:base="https://dev.karakun.com/2026/02/20/Retrofitting-AI.html"><![CDATA[<p>Adding AI-powered capabilities to existing enterprise systems is often complex, especially when modernization or migration to new frameworks is not immediately feasible. 
However, it is possible to retrofit an application with a natural language interface while keeping the original business logic untouched.</p>

<p>This post walks through how to integrate <strong>AI-based request generation</strong> for an existing search API, focusing on <strong>structured outputs</strong>, <strong>tooling</strong>, and <strong>validation</strong>, while discussing some real-world obstacles. 
This example represents a simpler case where tool calls are fast and have no side effects. 
It allows us to focus on the interaction between the LLM, the tools, and the structured output without introducing external dependencies, complex state handling, or repeated tool calls.</p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#Idea">1. The Idea: Let the AI Build Your Request Objects</a></li>
  <li><a href="#Setup">2. Technology Setup</a>
    <ul>
      <li><a href="#Dependencies">Gradle Dependencies</a></li>
      <li><a href="#Configuration">Configuration</a></li>
    </ul>
  </li>
  <li><a href="#Prompts">3. Converting Prompts into Valid Search Requests</a></li>
  <li><a href="#Tools">4. Adding Domain-Specific Tools</a>
    <ul>
      <li><a href="#ToolsMatter">Why Tools Matter</a></li>
    </ul>
  </li>
  <li><a href="#Testing">5. Testing AI-Assisted Code</a></li>
  <li><a href="#Value">6. Business Value and Constraints of AI Integration</a>
    <ul>
      <li><a href="#Constraints">Practical Constraints</a></li>
    </ul>
  </li>
  <li><a href="#Takeaways">7. Takeaways</a></li>
  <li><a href="#Help">8. Expert Support for AI Integration Projects</a></li>
</ul>

<hr />

<h2 id="-1-the-idea-let-the-ai-build-your-request-objects"><a name="Idea"></a> 1. The Idea: Let the AI Build Your Request Objects</h2>

<p>Existing systems often have well-defined APIs for search, analytics, or operations. 
They usually expect strongly typed input models, such as a <code class="language-plaintext highlighter-rouge">SearchRequestModel</code>. 
Retrofitting them for AI input means giving the user a natural language interface and letting the LLM create valid request objects automatically.</p>

<p>With <strong>Spring AI</strong>, this becomes practical through:</p>
<ul>
  <li><strong>Structured output handling</strong> – the LLM generates JSON matching a given class.</li>
  <li><strong>Tools</strong> – annotated functions that the LLM can call to retrieve external data or validate intermediate results.</li>
</ul>

<p>This approach bridges free-text prompts with typed data structures, enabling human-like queries while keeping the backend stable.</p>

<hr />

<h2 id="-2-technology-setup"><a name="Setup"></a> 2. Technology Setup</h2>

<p>For this article, we use our own <a href="https://hibu-platform.com/en/home/" target="_blank">HIBU platform</a> as an example. 
HIBU provides an API library that includes request and response classes annotated with OpenAPI metadata, which makes it well suited for generating structured outputs.</p>

<h3 id="-gradle-dependencies"><a name="Dependencies"></a> Gradle Dependencies</h3>

<p>You can retrofit without heavy dependencies, provided your project already runs on <strong>Spring Boot 3.x</strong> (required for Spring AI) or you are maintaining this code in a separate module/project.</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="nf">platform</span><span class="o">(</span><span class="s2">"org.springframework.boot:spring-boot-dependencies:3.5.7"</span><span class="o">)</span>

    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-web'</span>

    <span class="n">implementation</span> <span class="nf">platform</span><span class="o">(</span><span class="s2">"org.springframework.ai:spring-ai-bom:1.0.3"</span><span class="o">)</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.ai:spring-ai-starter-model-openai'</span>

    <span class="n">implementation</span> <span class="s2">"com.karakun.hibu:hibu-api:3.6.1"</span>

    <span class="n">testImplementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-test'</span>
    <span class="n">testImplementation</span> <span class="s1">'org.assertj:assertj-core'</span>
    <span class="n">testRuntimeOnly</span> <span class="s1">'org.junit.platform:junit-platform-launcher'</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="-configuration"><a name="Configuration"></a> Configuration</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring</span><span class="pi">:</span>
  <span class="na">ai</span><span class="pi">:</span>
    <span class="na">openai</span><span class="pi">:</span>
      <span class="na">api-key</span><span class="pi">:</span> <span class="s">add your key</span>
      <span class="na">chat</span><span class="pi">:</span>
        <span class="na">options</span><span class="pi">:</span>
          <span class="na">model</span><span class="pi">:</span> <span class="s">gpt-4.1-mini</span>
          <span class="na">temperature</span><span class="pi">:</span> <span class="m">0</span>
</code></pre></div></div>
<p>A low temperature reduces output variance and improves reproducibility for testing and validation.</p>

<h2 id="-3-converting-prompts-into-valid-search-requests"><a name="Prompts"></a> 3. Converting Prompts into Valid Search Requests</h2>

<p>The main service delegates prompt interpretation to the LLM. 
The <code class="language-plaintext highlighter-rouge">ChatClient</code> and your own <code class="language-plaintext highlighter-rouge">@Tool</code> definitions drive this.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.karakun.hibu.promptassistance</span><span class="o">;</span>

<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AiPromptAssistanceService</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ChatClient</span> <span class="n">chatClient</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">HibuTools</span> <span class="n">hibuTools</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">AiPromptAssistanceService</span><span class="o">(</span><span class="nc">ChatClient</span> <span class="n">chatClient</span><span class="o">,</span> <span class="nc">HibuTools</span> <span class="n">hibuTools</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">chatClient</span> <span class="o">=</span> <span class="n">chatClient</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">hibuTools</span> <span class="o">=</span> <span class="n">hibuTools</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">SearchRequestModel</span> <span class="nf">getRequestFromPrompt</span><span class="o">(</span><span class="nc">String</span> <span class="n">prompt</span><span class="o">,</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">facetFields</span><span class="o">,</span> <span class="nc">String</span> <span class="n">container</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">chatClient</span><span class="o">.</span><span class="na">prompt</span><span class="o">()</span>
            <span class="o">.</span><span class="na">system</span><span class="o">(</span><span class="n">u</span> <span class="o">-&gt;</span> <span class="n">u</span><span class="o">.</span><span class="na">text</span><span class="o">(</span><span class="s">"""
                    Task: Given a user prompt, produce a SearchRequest JSON for our search API.
                    Use synonyms and simple_query_string syntax for "</span><span class="n">query</span><span class="s">".
                    Allowed filter fields: {filterFields}
                    Use the tools to fetch keyword filter values and validate the result object.
                    """</span>
                <span class="o">)</span>
                <span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"filterFields"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">join</span><span class="o">(</span><span class="s">","</span><span class="o">,</span> <span class="n">facetFields</span><span class="o">))</span>
                <span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"container"</span><span class="o">,</span> <span class="n">container</span><span class="o">)</span>
            <span class="o">)</span>
            <span class="o">.</span><span class="na">tools</span><span class="o">(</span><span class="n">hibuTools</span><span class="o">)</span>
            <span class="o">.</span><span class="na">user</span><span class="o">(</span><span class="n">u</span> <span class="o">-&gt;</span> <span class="n">u</span><span class="o">.</span><span class="na">text</span><span class="o">(</span><span class="n">prompt</span><span class="o">))</span>
            <span class="o">.</span><span class="na">call</span><span class="o">()</span>
            <span class="o">.</span><span class="na">entity</span><span class="o">(</span><span class="nc">SearchRequestModel</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This example uses the Spring AI fluent API. 
The LLM receives a system prompt describing how to construct a valid <code class="language-plaintext highlighter-rouge">SearchRequestModel</code>.
The <code class="language-plaintext highlighter-rouge">.entity(SearchRequestModel.class)</code> call ensures the response is automatically deserialized and validated against the record definition. 
For this, Spring AI processes existing annotations such as <code class="language-plaintext highlighter-rouge">@Nullable</code>, <code class="language-plaintext highlighter-rouge">@Schema</code>, <code class="language-plaintext highlighter-rouge">@JsonProperty</code>, and many more.</p>

<p><strong>Note:</strong> The actual system prompt most likely contains many more instructions and restrictions for the LLM, such as “DO NOT invent or change filter values.” 
I have kept it brief for this article.</p>

<h2 id="-4-adding-domain-specific-tools"><a name="Tools"></a> 4. Adding Domain-Specific Tools</h2>

<p>The <code class="language-plaintext highlighter-rouge">@Tool</code> annotation turns normal Spring beans into callable LLM functions. 
In this example, two tools support validation and controlled value selection.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.karakun.hibu.promptassistance</span><span class="o">;</span>

<span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HibuTools</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RestClient</span> <span class="n">client</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">filtersUrl</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Validator</span> <span class="n">validator</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">HibuTools</span><span class="o">(</span><span class="nc">Validator</span> <span class="n">validator</span><span class="o">,</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">Builder</span> <span class="n">builder</span><span class="o">,</span>
                     <span class="nd">@Value</span><span class="o">(</span><span class="s">"${tools.hibu.fetchAvailableFilterValuesUrl}"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">fetchUrl</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">filtersUrl</span> <span class="o">=</span> <span class="n">fetchUrl</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">validator</span> <span class="o">=</span> <span class="n">validator</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">client</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"isValidSearchRequestModel"</span><span class="o">,</span>
          <span class="n">description</span> <span class="o">=</span> <span class="s">"Validate JSON against SearchRequestModel class."</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">isValidSearchRequestModel</span><span class="o">(</span><span class="nc">String</span> <span class="n">searchRequestModel</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">SearchRequestModel</span> <span class="n">model</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ObjectMapper</span><span class="o">().</span><span class="na">readValue</span><span class="o">(</span><span class="n">searchRequestModel</span><span class="o">,</span> <span class="nc">SearchRequestModel</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
            <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">ConstraintViolation</span><span class="o">&lt;</span><span class="nc">SearchRequestModel</span><span class="o">&gt;&gt;</span> <span class="n">violations</span> <span class="o">=</span> <span class="n">validator</span><span class="o">.</span><span class="na">validate</span><span class="o">(</span><span class="n">model</span><span class="o">);</span>
            <span class="k">if</span> <span class="o">(!</span><span class="n">violations</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
                <span class="k">return</span> <span class="n">violations</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="nl">ConstraintViolation:</span><span class="o">:</span><span class="n">getMessage</span><span class="o">).</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">joining</span><span class="o">(</span><span class="s">"\n"</span><span class="o">));</span>
            <span class="o">}</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">JsonProcessingException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">();</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="s">"true"</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"fetchAvailableFilterValues"</span><span class="o">,</span>
          <span class="n">description</span> <span class="o">=</span> <span class="s">"Fetches available filter values for a given keyword-based field."</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">fetchAvailableFilterValues</span><span class="o">(</span><span class="nd">@NotNull</span> <span class="nc">String</span> <span class="n">container</span><span class="o">,</span> <span class="nd">@NotNull</span> <span class="nc">String</span> <span class="n">fieldName</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Query the existing API</span>
        <span class="nc">SearchRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SearchRequest</span><span class="o">(</span><span class="n">container</span><span class="o">,</span> <span class="s">""</span><span class="o">,</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(),</span> <span class="kc">null</span><span class="o">,</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(),</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">false</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">fieldName</span><span class="o">));</span>
        <span class="kt">var</span> <span class="n">type</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ParameterizedTypeReference</span><span class="o">&lt;</span><span class="nc">SearchResponse</span><span class="o">&lt;</span><span class="nc">ObjectMapCustomData</span><span class="o">&gt;&gt;()</span> <span class="o">{};</span>
        <span class="kt">var</span> <span class="n">resp</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="na">post</span><span class="o">().</span><span class="na">uri</span><span class="o">(</span><span class="n">filtersUrl</span><span class="o">).</span><span class="na">body</span><span class="o">(</span><span class="n">request</span><span class="o">).</span><span class="na">retrieve</span><span class="o">().</span><span class="na">body</span><span class="o">(</span><span class="n">type</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">resp</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">resp</span><span class="o">.</span><span class="na">getFacets</span><span class="o">()</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">resp</span><span class="o">.</span><span class="na">getFacets</span><span class="o">().</span><span class="na">stream</span><span class="o">()</span>
            <span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">f</span> <span class="o">-&gt;</span> <span class="n">f</span><span class="o">.</span><span class="na">getFieldName</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="n">fieldName</span><span class="o">))</span>
            <span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="n">f</span> <span class="o">-&gt;</span> <span class="n">f</span><span class="o">.</span><span class="na">getValues</span><span class="o">().</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="nl">FacetValue:</span><span class="o">:</span><span class="n">getValue</span><span class="o">))</span>
            <span class="o">.</span><span class="na">toList</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="-why-tools-matter"><a name="ToolsMatter"></a> Why Tools Matter</h3>

<ul>
  <li>The validation tool (isValidSearchRequestModel) enables the LLM to correct invalid JSON through iterative tool-assisted regeneration.</li>
  <li>The fetch tool limits the model to known keyword values, avoiding invented filters and producing robust output.</li>
</ul>

<p>These patterns greatly reduce runtime errors and make the integration resilient to AI hallucinations.</p>

<h2 id="-5-testing-ai-assisted-code"><a name="Testing"></a> 5. Testing AI-Assisted Code</h2>

<p>LLMs like ChatGPT do not guarantee deterministic replay, so defining explicit testing expectations is essential.
You can use mocks to isolate behavior and assert that generated requests meet certain structural and semantic criteria.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.karakun.hibu.promptassistance</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AiPromptAssistanceServiceTest</span> <span class="kd">extends</span> <span class="nc">SpringBaseTest</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">AiPromptAssistanceService</span> <span class="n">service</span><span class="o">;</span>

    <span class="nd">@MockitoBean</span>
    <span class="kd">private</span> <span class="nc">HibuTools</span> <span class="n">mockedHibuTools</span><span class="o">;</span>

    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">getRequestFromPrompt</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">when</span><span class="o">(</span><span class="n">mockedHibuTools</span><span class="o">.</span><span class="na">fetchAvailableFilterValues</span><span class="o">(</span><span class="n">any</span><span class="o">(),</span> <span class="n">any</span><span class="o">()))</span>
            <span class="o">.</span><span class="na">thenReturn</span><span class="o">(</span><span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"Karakun AG"</span><span class="o">,</span> <span class="s">"Another AG"</span><span class="o">));</span>

        <span class="nc">SearchRequestModel</span> <span class="n">result</span> <span class="o">=</span> <span class="n">service</span><span class="o">.</span><span class="na">getRequestFromPrompt</span><span class="o">(</span>
            <span class="s">"Search for all company presentations of Karakun created in the last three months of each of the last five years."</span><span class="o">,</span>
            <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"metadata.creation_date"</span><span class="o">,</span> <span class="s">"metadata.companyName_string"</span><span class="o">),</span>
            <span class="s">"foo"</span><span class="o">);</span>

        <span class="n">verify</span><span class="o">(</span><span class="n">mockedHibuTools</span><span class="o">).</span><span class="na">fetchAvailableFilterValues</span><span class="o">(</span><span class="s">"foo"</span><span class="o">,</span> <span class="s">"metadata.companyName_string"</span><span class="o">);</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">query</span><span class="o">()).</span><span class="na">contains</span><span class="o">(</span><span class="s">"presentation"</span><span class="o">);</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">filters</span><span class="o">()).</span><span class="na">containsKey</span><span class="o">(</span><span class="s">"metadata.creation_date"</span><span class="o">);</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">filters</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="s">"metadata.creation_date"</span><span class="o">)).</span><span class="na">hasSize</span><span class="o">(</span><span class="mi">5</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Tip:</strong> Always verify tool calls and expected key fields.
This ensures your prompt and model configuration are aligned with predictable outcomes.</p>

<h2 id="-6-business-value-and-constraints-of-ai-integration"><a name="Value"></a> 6. Business Value and Constraints of AI Integration</h2>

<p>Adding AI features on top of existing systems provides several advantages:</p>

<ul>
  <li><strong>Faster experimentation</strong> – you can test AI-driven interfaces without refactoring the core logic.</li>
  <li><strong>Lower risk</strong> – tools isolate the AI layer, so failures do not affect critical paths.</li>
  <li><strong>Improved UX</strong> – users interact in natural language while the backend remains unchanged.</li>
</ul>

<h3 id="-practical-constraints"><a name="Constraints"></a> Practical Constraints</h3>

<ul>
  <li>Spring Boot 3.x required: Spring AI only supports applications running on the latest generation. 
Legacy projects may require upgrade work before integration or maintain such a retrofitting component in a separate module/project.</li>
  <li>Validation tools improve reliability: Without them, structured output tends to break on minor syntax issues.</li>
  <li>Model selection and cost: Smaller models like gpt-4.1-mini often suffice. 
Larger ones may be cost-prohibitive for frequent use.</li>
  <li>Testing discipline: Because LLMs behave probabilistically, regression tests are critical to detect subtle prompt changes or API behavior shifts. 
At Karakun, we are building an infrastructure that enables consistent testing across multiple models and helps us curate a maintainable collection of prompt patterns and best practices.</li>
</ul>

<h2 id="-7-takeaways"><a name="Takeaways"></a> 7. Takeaways</h2>

<ul>
  <li>Retrofitting an existing Spring application with AI features is possible and often valuable.</li>
  <li>Spring AI’s Tools and Structured Output simplify controlled AI integration.</li>
  <li>Custom validation tools make AI-generated structures robust and retryable.</li>
  <li>Expect some migration effort to Spring Boot 3.x and ensure your LLM configuration is “deterministic” for repeatable tests.</li>
  <li>Test, observe, and iterate - AI integration is not a one-time setup but a continuous process.</li>
</ul>

<p>By combining Spring AI, careful tool definitions, and disciplined validation, teams can extend legacy systems with intelligent interfaces while maintaining technical and business stability.</p>

<h2 id="-8-expert-support-for-ai-integration-projects"><a name="Help"></a> 8. Expert Support for AI Integration Projects</h2>

<p>While this example keeps things simple with side-effect-free tool calls, real-world applications often involve more complex integrations.
Integrating AI into existing software ecosystems requires architectural expertise and experience balancing maintainability and business objectives.</p>

<p>At <a href="https://karakun.com" target="_blank">karakun.com</a>, we help organizations analyze their current solutions and design the best way to integrate AI - whether that means lightweight retrofitting, full-stack modernization, or targeted use of AI capabilities.</p>

<p>If you are exploring how to introduce intelligent features into your existing systems, reach out to us. 
Together we can identify where AI delivers measurable value without disrupting stable systems.</p>]]></content><author><name>Hannes</name></author><category term="AI" /><category term="NLP" /><category term="Java" /><category term="Spring" /><category term="Spring Boot" /><category term="Search" /><summary type="html"><![CDATA[Retrofit AI into existing applications using Spring AI. Learn how natural language input is translated into structured requests with validation and LLM tools.]]></summary></entry><entry><title type="html">Beyond Productivity: Impacts &amp;amp; Risks of AI Coding Tools</title><link href="https://dev.karakun.com/2026/02/10/BeyondProductivity.html" rel="alternate" type="text/html" title="Beyond Productivity: Impacts &amp;amp; Risks of AI Coding Tools" /><published>2026-02-10T00:00:00+00:00</published><updated>2026-02-10T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/02/10/beyond-productivity</id><content type="html" xml:base="https://dev.karakun.com/2026/02/10/BeyondProductivity.html"><![CDATA[<hr />

<p>AI coding assistants can feel like supportive pair programming without social friction. But that comfort has trade-offs worth examining.</p>

<hr />

<h2 id="article-navigation">Article Navigation</h2>
<ul>
  <li><a href="#intro">Why AI Coding Assistants Feel So Good — and Why This Should Make Us Wary</a></li>
  <li><a href="#bias">Bias, Perception, and The Hidden Competence Penalty</a></li>
  <li><a href="#misconception">The Next Hidden Trap in AI Coding: Why Your Assistant Might Be Reinforcing Your Misconceptions</a></li>
  <li><a href="#reflection">The Need for Ongoing Self-Reflection: Power, Privacy, and Asymmetry</a></li>
  <li><a href="#recommendations">What Developers Can Do Today</a></li>
  <li><a href="#cta">Let’s Connect</a></li>
  <li><a href="#references">References</a></li>
</ul>

<hr />

<h2 id="-why-ai-coding-assistants-feel-so-good--and-why-this-should-make-us-wary"><a name="intro"></a> Why AI Coding Assistants Feel So Good — and Why This Should Make Us Wary</h2>
<p>Have you ever caught yourself genuinely enjoying a digital pat on the back? 
You’ve been using AI assistants in your daily work for a while now, and suddenly everything feels lighter. 
You feel encouraged, supported, maybe even empowered. 
The next prototype comes together in record time. 
You write software and develop solutions in technologies that once felt unfamiliar or intimidating. 
Mastery of a specific programming language seems less important than before. Instead, your ability to think creatively and develop solutions takes center stage.</p>

<p>You start experiments you’ve been postponing for months. 
Long-abandoned projects finally get finished. 
Maybe you’ve even realigned your company’s vision. 
You feel more creative, more confident, full of new ideas. 
New business models, new projects, professional goals suddenly feel within reach. 
You might even dare to contribute seriously to an open source project, because time and mental pressure are no longer the limiting factors they once were.</p>

<p>If this sounds familiar, you’re not alone. 
Many developers, managers, and CTOs report similar experiences. 
Early studies suggest that coding assistants can provide short-term emotional support and increase satisfaction with everyday software development tasks <a href="#xiao-et-al-2025">[1]</a>.
What we don’t yet understand, however, are the long-term effects.</p>

<p>AI coding assistants are not neutral tools. 
They are carefully and smartly designed dialogue systems built to encourage and affirm. 
They appear understanding, patient, and endlessly available. 
They also remove many small frictions common in human collaboration: eye-rolling, implicit judgment, repeated questioning, and time pressure. 
For many developers, this feels deeply liberating.</p>

<p>In traditional pair programming, interpersonal barriers are part of the experience, and learning to navigate and overcome them can take years. 
With AI support, a sense of psychological safety can emerge. 
This kind of safety is often hard to achieve with real people, because human collaboration operates in a different social and emotional mode.</p>

<p>With an AI coding assistant, you can voice imperfect ideas, make naïve suggestions, or even delegate an entire task. 
Modern AI assistants simulate emotional intelligence: they respond empathetically, provide structured explanations, and appear supportive. 
For many, this feels like pair programming without friction or frustration. 
There is no noticeable knowledge gap.</p>

<p>In human pair programming, a such gaps often push us out of our comfort zone. 
With AI, that gap feels largely absent — not because it does not exist, but because it does not trigger social comparison.
AI tools typically challenge ideas only when explicitly prompted and tend to show less critical resistance than human collaborators <a href="#welter-et-al-2025">[2]</a> <a href="#apel-et-al-2025">[3]</a>.
They do not hold genuine opinions, values, or a lived perspective in the way real people do.
While certain behaviors and values can be partially simulated through configuration and prompting, this remains fundamentally different from engaging with a human collaborator who brings their own convictions and experience into the discussion.</p>

<h3 id="and-this-is-where-the-ambivalence-begins">And This Is Where The Ambivalence Begins</h3>

<p>Early research indicates that developers who rely heavily on AI assistants for pair programming may gradually invest less in real workplace relationships.
Studies suggest reduced depth of knowledge transfer, fewer mentoring interactions, and a shift away from interpersonal exchange and team-level collaboration toward individual, tool-mediated work <a href="#xiao-et-al-2025">[1]</a> <a href="#welter-et-al-2025">[2]</a> <a href="#apel-et-al-2025">[3]</a>.
The assumption that this technology is free of disappointment turns out to be an illusion, because disappointment does not disappear - it shifts.</p>

<p>It shows up when no useful solution emerges despite careful prompting, suggestions are shallow or wrong, tools crash, token limits are reached, or costs spiral out of control. 
It also appears when code with security vulnerabilities makes it into production despite careful reviews, when data leaks occur despite privacy assurances, or when platforms are suddenly discontinued or unavailable.</p>

<h3 id="the-list-of-risks-is-long--and-growing">The List of Risks Is Long — and Growing</h3>

<p>AI coding assistants open up enormous opportunities. 
They are reshaping how we work, learn, and think. 
But precisely because they feel so good, it’s worth taking a closer look. 
Not every digital pat on the back is harmless: some distract us, some obscure risks, and some replace something that cannot easily be simulated – genuine collaboration and growing together as a team <a href="#dishop-et-al-2025">[4]</a>.</p>

<h2 id="-bias-perception-and-the-hidden-competence-penalty"><a name="bias"></a> Bias, Perception, and The Hidden Competence Penalty</h2>

<p>Not all colleagues view AI-assisted work positively. 
The use of AI in software development is still surrounded by strong biases and preconceived notions. 
Research shows that individuals who receive help from AI often face a hidden competence penalty: even when the quality of the work is identical, people are perceived as less competent, less diligent, and lazier simply because AI was involved <a href="#acar-et-al-2025">[8]</a>.</p>

<p>Experiments consistently demonstrate that engineers believed to have used AI are evaluated more negatively, despite no measurable difference in code quality. 
This penalty does not target the output, but the perceived ability of the person behind it. 
The effect is not evenly distributed. 
Female engineers are penalized significantly more than their male counterparts, and the harshest judgments come from engineers who do not use AI themselves – particularly male non-adopters evaluating women.</p>

<p>As a result, many developers anticipate this social penalty and strategically avoid using AI to protect their professional reputation, as mentioned in the research of <a href="#acar-et-al-2025">[8]</a>. 
The authors also state, that ironically, the groups that could benefit most from productivity-enhancing tools – women and older engineers – are the least likely to adopt them. 
This reflects broader social and organizational structures in which AI assistance is framed not as strategic tool use, but as evidence of inadequacy, especially for already stereotyped groups.</p>

<p>The research highlights a fundamental mismatch in how organizations approach AI adoption. 
While companies focus on access, tooling, and training, they often ignore the social dynamics that determine whether AI is actually used. 
Since AI-assisted work shows no inherent quality disadvantage, a more responsible path forward may be to shift evaluation away from perceived competence and toward objective outcomes such as accuracy, defect rates, and delivery time, rather than how the work was produced. 
The introduction of role models and joint AI hackathons within the organisation can also mitigate these effects.</p>

<h2 id="-the-next-hidden-trap-in-ai-coding-why-your-assistant-might-be-reinforcing-your-misconceptions"><a name="misconception"></a> The Next Hidden Trap in AI Coding: Why Your Assistant Might Be Reinforcing Your Misconceptions</h2>

<p>Every software engineer has introduced bugs for mundane reasons – forgetting to add a test case, a condition, or calling the wrong method. 
These are small oversights that occur even when we understand the problem reasonably well <a href="#hermans-2021">[5]</a>. 
This is precisely where AI coding assistants excel. 
They act as a second set of eyes, catching missing checks, inconsistent logic, or obvious implementation errors.</p>

<p>However, there is a more dangerous category of errors – one that AI assistants may not only fail to prevent, but may actively reinforce: misconceptions.</p>

<h3 id="beyond-simple-mistakes-the-problem-of-misconceptions">Beyond Simple Mistakes: The Problem of Misconceptions</h3>

<p>A misconception is not a typo, syntax error, or an overlooked edge case. 
It is a faulty assumption about how a framework, a system, a data structure, or an API works. 
It occurs when your mental model of the code, commands, or tools is wrong, even though you feel confident in your reasoning. This can happen, for example, when assumptions are reused from previous projects that simply do not hold in a new context.</p>

<p>Correcting bugs in thinking requires replacing an entire mental model with a new one – a significant cognitive shift, whilst even after learning the correct model, developers may revert to the old misconception under time pressure, cognitive load, or familiarity bias <a href="#hermans-2021">[5]</a>.</p>

<p><em>The AI Dynamic: A Cooperative Risk</em></p>

<p>This is where the relationship between developers and AI assistants becomes complex.</p>

<p>AI coding tools are deliberately designed to be cooperative. 
They follow the context you provide, reinforce your framing, and optimize for helpfulness. 
As a result, they rarely challenge your assumptions unless explicitly instructed to do so. 
If your prompt or existing code is based on a misconception, the AI may:</p>

<ul>
  <li>Adopt the incorrect assumption and build further logic on top of it</li>
  <li>Strengthen the misconception by producing plausible-looking code that appears to confirm your flawed mental model</li>
  <li>Introduce additional errors, offering suggestions that look correct but are fundamentally wrong</li>
</ul>

<p>Unlike a human colleague, an AI assistant does not naturally push back, express doubt, or question intent. 
It does not notice conceptual inconsistencies unless they are syntactically or statistically obvious. 
As a result, misconceptions can persist longer, spread further, and become deeply embedded in the codebase, potentially hindering your future self or your team from developing correct solutions.</p>

<h3 id="why-critical-thinking-is-still-your-most-important-tool">Why Critical Thinking Is Still Your Most Important Tool</h3>

<p>AI can help us write code faster, but it cannot replace critical thinking, sound testing strategies, or shared reasoning.</p>

<p>Traditionally, one of the most effective ways to uncover misconceptions has been collaboration: pair programming, code reviews, or refinements. 
When different mental models collide, assumptions are exposed, challenged, and refined. 
A solid and well-designed test suite can also play a crucial role by forcing assumptions to become explicit and verifiable.</p>

<p>Working with AI can subtly shift this dynamic. 
Instead of challenging our thinking, the assistant often mirrors it. 
If we are not careful, AI becomes an amplifier of our misconceptions rather than a safeguard against them.</p>

<p>To use AI responsibly, developers must remain aware of its limitations and actively compensate for them – by questioning outputs, seeking alternative explanations, and deliberately inviting dissent into their workflow.</p>

<p>In the end, the most dangerous bugs are not caused by missing code. 
They are caused by flawed thinking – and no assistant, however powerful, can fix that for us.</p>

<h2 id="-the-need-for-ongoing-self-reflection-power-privacy-and-asymmetry"><a name="reflection"></a> The Need for Ongoing Self-Reflection: Power, Privacy, and Asymmetry</h2>

<h3 id="all-of-this-calls-for-continuous-self-reflection">All of This Calls for Continuous Self-Reflection</h3>

<p>On the one hand, it is important to remember that today’s AI platforms are run by profit-driven companies. 
These systems are not neutral infrastructures; they are operated by businesses that must generate revenue, and that inevitably means data has economic value. 
When we work with AI coding assistants, we are often handling proprietary code, architectural decisions, or internal business logic. 
Even when terms and policies promise safeguards, we are still engaging in an economic relationship where data matters.</p>

<p>On the other hand, the relationship between a developer and an AI pair programmer is inherently asymmetrical. 
The AI does not need support, mentoring, or feedback. 
It does not grow into a role, nor does it share responsibility. 
As a result, there is a risk that genuine team relationships – and especially the mentoring and support of junior developers – are gradually deprioritized. 
The more frictionless coding becomes through AI assistance, the easier it is to substitute human collaboration with tool-based interaction.</p>

<p>Modern technology actively reinforces this shift. 
Tools are becoming more polished, more intuitive, and more responsive. 
User interfaces improve continuously, interaction feels increasingly natural, and the perceived quality of results keeps rising. 
This is not accidental – it is a rapidly growing market with millions of users and strong economic incentives to optimize for adoption and dependency.</p>

<h3 id="when-convenience-turns-into-a-security-risk">When Convenience Turns into A Security Risk</h3>

<p>Recent findings underline why this reflection is necessary. 
A new security research report by Koi Research revealed that several popular browser extensions have been secretly harvesting private AI conversations from millions of users <a href="#dardikman-2025">[7]</a>). 
While these tools claimed to protect user privacy, they injected scripts into the browser to intercept AI dialogues and sell the collected data to data brokers.</p>

<p>More than eight million users were affected, with sensitive information harvested for marketing and analytics purposes. 
Particularly alarming is the fact that some of these extensions received official recommendations from major platforms such as Google and Microsoft – despite their covert surveillance behavior.</p>

<p>The investigation shows how severe the security risks of browser add-ons can be. 
Even when inactive, these extensions were capable of exfiltrating data in the background. 
This practice represents a data-broker business model centered on monetizing highly sensitive user interactions. 
Once a user visits one of several supported AI platforms - such as ChatGPT, Claude, or Gemini - the extension injects code that overrides native browser functionality. 
Prompts, AI responses, timestamps, and metadata are captured and transmitted to the provider’s servers continuously, regardless of whether VPN features are enabled.</p>

<p>In the past, such models primarily relied on clickstream data. 
Today, the focus has shifted to AI conversations – data that is far more revealing. 
These interactions may contain personal dilemmas, medical questions, financial information, or proprietary source code. 
From a data monetization perspective, this information is exceptionally valuable.</p>

<h3 id="between-progress-and-moral-cost">Between Progress and Moral Cost</h3>

<p>Few would deny the potential benefits of using AI, for example in the medical research field for early detection of breast or skin cancer. 
If the price of mass deployment and tool optimization is that a large corporation gains access to the medical records of millions of people, a careful moral trade-off is required. 
Even if the use of such technology can still be ethically justified after weighing the benefits, society must remain conscious of the price it is paying <a href="#rosengrün-2023">[6]</a>). 
That price should be openly acknowledged, debated, and – where possible – actively negotiated, rather than silently accepted or surrendered to opaque data-collection practices.</p>

<p>The same applies to software development. 
AI tools offer undeniable advantages, but their social, organizational, and ethical implications do not disappear simply because the tools are useful and user-friendly. 
Continuous reflection is not a luxury – it is a responsibility.</p>

<h2 id="-what-developers-can-do-today"><a name="recommendations"></a> What Developers Can Do Today</h2>
<p>First of all, leading companies should be aware of existing biases and ethical implications, and the development of AI tools must actively take them into account.
Nevertheless, it would be unrealistic to assume that these challenges can be resolved very soon and solely at the platform level.
The effects described above emerge in everyday practice — and they can also be addressed there.</p>

<p>Two concrete strategies are particularly effective and can be applied immediately.</p>

<h3 id="actively-demand-critique-from-the-assistant">Actively Demand Critique from The Assistant</h3>
<p>When using AI to formulate requirements, designs, or solutions, developers can deliberately counteract its tendency toward affirmation.
Instead of inviting help, they can proactively require critical review with prompts like</p>

<blockquote>
  <p>Before proceeding with anything else, evaluate this requirement for ambiguities. Ask clarifying questions if you have any.</p>
</blockquote>

<blockquote>
  <p>Tell me if there are any open source libraries out there that already solve this problem.</p>
</blockquote>

<blockquote>
  <p>Critique this design from the perspective of best-practice Java development. Check for clean code criteria. Check whether the code has any security gaps or vulnerabilities. Do not give compliments.
The more specific, the better.
Used this way, the assistant becomes less reassuring and more adversarial, reintroducing friction and reflection.</p>
</blockquote>

<h3 id="do-not-replace-pair-programming-or-peer-review--augment-them">Do Not Replace Pair Programming or Peer Review — Augment Them</h3>
<p>Rather than forgoing human collaboration, AI tools should be used alongside it.
While an LLM can review code faster than any colleague, it lacks domain-specific context, architectural history, and shared responsibility.
A human reviewer brings judgment and lived experience that cannot be simulated.
Combined, both forms of feedback serve complementary roles — and preserving that balance is essential.</p>

<h2 id="lets-connect"><a name="cta"></a>Let’s Connect</h2>
<p>Do you have questions about the impact and risks of AI coding tools? 
Or would you like to discuss the latest developments in AI-based software engineering?
<a href="/people/iryna">Feel free to reach out.</a> 
I’m always happy to exchange knowledge, ideas, and experiences.</p>

<h2 id="-references"><a name="references"></a> References</h2>

<ol>
  <li><a name="xiao-et-al-2025"></a> Xiao, Q., Hu, X. E., Whiting, M. E., Karunakaran, A., Shen, H., &amp; Cao, H. (2025). AI hasn’t fixed teamwork, but it shifted collaborative culture: A longitudinal study in a project-based software development organization (2023–2025). arXiv. <a href="https://arxiv.org/abs/2509.10956">https://arxiv.org/abs/2509.10956</a></li>
  <li><a name="welter-et-al-2025"></a>Welter, A., Schneider, N., Dick, T., Weis, K., Tinnes, C., Wyrich, M., &amp; Apel, S. (2025). From developer pairs to AI copilots: A comparative study on knowledge transfer. arXiv. <a href="https://arxiv.org/abs/2506.04785">https://arxiv.org/abs/2506.04785</a></li>
  <li><a name="apel-et-al-2025"></a>Apel, S., et al. (2025). Software developers show less constructive skepticism when using AI assistants than when working with human colleagues. The 40th IEEE/ACM International Conference on Automated Software Engineering (ASE 2025). Reported by TechXplore (edited by Stephanie Baum, reviewed by Andrew Zinin). <a href="https://techxplore.com/news/2025-11-software-skepticism-ai-human-colleagues.html">https://techxplore.com/news/2025-11-software-skepticism-ai-human-colleagues.html</a></li>
  <li><a name="dishop-et-al-2025"></a>Dishop, C. R., Brown, A. S., Chao, P. Y., et al. (2025). Machines in the Middle: Using Artificial Intelligence (AI) While Offering Help Affects Warmth, Felt Obligations, and Reciprocity. Journal of Business and Psychology. <a href="https://doi.org/10.1007/s10869-025-10068-x">https://doi.org/10.1007/s10869-025-10068-x</a></li>
  <li><a name="hermans-2021"></a>Hermans, Felienne (2021). The Programmer’s Brain: What Every Programmer Needs to Know About Cognition. Manning Publications.</li>
  <li><a name="rosengrün-2023"></a>Rosengrün Sebastian (2023). Künstliche Intelligenz zur Einführung. Junius Verlag.</li>
  <li><a name="dardikman-2025"></a>Dardikman, Idan (2025, December 15). 8 Million Users’ AI Conversations Sold for Profit by “Privacy” Extensions. Koi Research Blog. <a href="https://www.koi.ai/blog/urban-vpn-browser-extension-ai-conversations-data-collection">https://www.koi.ai/blog/urban-vpn-browser-extension-ai-conversations-data-collection</a></li>
  <li><a name="acar-et-al-2025"></a>Oguz A. Acar, Phyliss Jia Gai, Yanping Tu and Jiayi Hou (2025, August 1). Research: The Hidden Penalty of Using AI at Work. Harvard Business Review Generative AI. <a href="https://hbr.org/2025/08/research-the-hidden-penalty-of-using-ai-at-work">https://hbr.org/2025/08/research-the-hidden-penalty-of-using-ai-at-work</a></li>
</ol>]]></content><author><name>iryna</name></author><category term="AI" /><category term="Ethics" /><category term="Security" /><summary type="html"><![CDATA[AI coding assistants improve developer productivity but can reinforce misconceptions, trigger competence penalties, and raise privacy risks in software teams.]]></summary></entry><entry><title type="html">LIGHTS – A Lightweight Global Collaboration for Methodological Research</title><link href="https://dev.karakun.com/2026/01/30/LIGHTS.html" rel="alternate" type="text/html" title="LIGHTS – A Lightweight Global Collaboration for Methodological Research" /><published>2026-01-30T00:00:00+00:00</published><updated>2026-01-30T00:00:00+00:00</updated><id>https://dev.karakun.com/2026/01/30/LIGHTS</id><content type="html" xml:base="https://dev.karakun.com/2026/01/30/LIGHTS.html"><![CDATA[<p>With a very small budget, the <a href="https://lights.science/" target="_blank">LIGHTS</a> project demonstrates how a practical, globally distributed research infrastructure can be built using familiar, readily available tools. 
<a href="https://lights.science/team-and-supporters/" target="_blank">Health researchers across continents</a> collaborate efficiently on a highly specialized collection for methodological guidance - without complex IT operations or expensive software.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#mission">Behind LIGHTS: An Infrastructure for Research on Methodological Guidelines</a></li>
  <li><a href="#concept">From Concept to Practice: Collaboration Between University of Basel and Karakun</a></li>
  <li><a href="#design">The Adapter: Three Research Data Pipelines, One Workflow</a>
    <ul>
      <li><a href="#pipe1">1. Paperpile and Google Drive Integration</a></li>
      <li><a href="#pipe2">2. Data Transformation for HIBU</a></li>
      <li><a href="#pipe3">3. Quality Assurance</a></li>
    </ul>
  </li>
  <li><a href="#benefits">Key Benefits of the LIGHTS Research Architecture</a></li>
  <li><a href="#components">Component Schema Overview</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
  <li><a href="#cta">Let’s Connect</a></li>
</ul>

<h2 id="-behind-lights-an-infrastructure-for-research-on-methodological-guidelines"><a name="mission"></a> Behind LIGHTS: An Infrastructure for Research on Methodological Guidelines</h2>

<p>The <strong>Library of Guidance for Health Scientists (LIGHTS)</strong> is a living inventory of more than 2,000 hand-selected methods guidance papers.
Its mission is to help clinical researchers find the best available methodological guidance to design and conduct high-quality studies.</p>

<p>Many clinical studies have avoidable limitations due to poor methodological decisions - such as inadequate study design, measurement bias, or flawed statistical analysis - even though suitable guidance documents exist. 
LIGHTS addresses this gap by systematically identifying, classifying, and making available methodological guidance documents.</p>

<h2 id="-from-concept-to-practice-collaboration-between-university-of-basel-and-karakun"><a name="concept"></a> From Concept to Practice: Collaboration Between University of Basel and Karakun</h2>

<p>The project is led by Dr. Stefan Schandelmaier at the University of Basel and technically supported by Karakun AG.</p>

<p>At the heart of the platform lies <a href="https://hibu-platform.com" target="_blank">HIBU</a>, Karakun’s search and AI platform, providing an intuitive and interactive search experience.
To feed HIBU with structured and continuously updated metadata, Karakun developed a special adapter system - a set of Java-based tools orchestrated through GitHub CI/CD pipelines.</p>

<h2 id="-the-adapter-three-research-data-pipelines-one-workflow"><a name="design"></a> The Adapter: Three Research Data Pipelines, One Workflow</h2>

<h3 id="-1-paperpile-and-google-drive-integration"><a name="pipe1"></a> 1. Paperpile and Google Drive Integration</h3>

<ul>
  <li>Researchers collect and curate literature in Paperpile, a web-based reference manager.</li>
  <li>A BibTeX export is automatically uploaded to a GitHub repository, triggering the first pipeline.</li>
  <li>This pipeline converts the BibTeX file into a CSV export, commits it to Git, and synchronizes it to a Google Drive folder.</li>
  <li>In Google Sheets, scientific collaborators enrich the data with domain-specific metadata.</li>
</ul>

<h3 id="-2-data-transformation-for-hibu"><a name="pipe2"></a> 2. Data Transformation for HIBU</h3>

<ul>
  <li>When the team is ready to publish, a second pipeline is manually triggered.</li>
  <li>It fetches the curated CSV from Google Drive and merges it with Paperpile metadata.</li>
  <li>The combined data is transformed into JSON objects, conforming to HIBU’s flexible index schema based on naming conventions (e.g., text, dates, multilingual fields).</li>
  <li>The resulting JSON becomes the search index input for HIBU.</li>
</ul>

<h3 id="-3-quality-assurance"><a name="pipe3"></a> 3. Quality Assurance</h3>

<ul>
  <li>A third pipeline runs automated tests whenever code changes occur.</li>
  <li>It validates syntax, checks for duplicates, and ensures backward compatibility with data formats.</li>
</ul>

<h2 id="-key-benefits-of-the-lights-research-architecture"><a name="benefits"></a> Key Benefits of the LIGHTS Research Architecture</h2>

<ul>
  <li><strong>Version-Controlled Collaboration</strong> – Every artifact (BibTeX, CSV, JSON) is tracked in Git without researchers having to handle Git directly.</li>
  <li><strong>Automated Validation</strong> – Pipelines detect structural or semantic issues early.</li>
  <li><strong>Rapid Deployment</strong> – New or corrected records are integrated into HIBU within minutes.</li>
  <li><strong>Low Maintenance</strong> – Built entirely from existing cloud tools and open standards.</li>
</ul>

<h2 id="-component-schema-overview"><a name="components"></a> Component Schema Overview</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>            +----------------+
            |   Paperpile    |
            |  (References)  |
            +--------+-------+
                     |
                     v
           +---------+----------+
           |  GitHub Repository |
           |  (Artifacts, CI/CD)|
           +----+-------+-------+
                |       |
     (Pipeline 1)       (Pipeline 3)
                |       v
                |   +-----------+
                |   |   Tests   |
                |   | Validation|
                |   +-----------+
                v
       +--------+---------+
       | Google Drive/    |
       | Google Sheets    |
       +--------+---------+
                |
         (Pipeline 2)
                v
           +----+----+
           |  JSON   |
           | (HIBU   |
           |  Input) |
           +----+----+
                |
                v
           +----+----+
           |   HIBU  |
           |  Search |
           +---------+
</code></pre></div></div>

<h2 id="-conclusion"><a name="conclusion"></a> Conclusion</h2>

<p>LIGHTS showcases how <strong>lean, automated data pipelines</strong> can empower international research projects.
By combining common tools such as GitHub, Paperpile, and Google Sheets with Karakun’s HIBU platform, the team created a robust, low-cost ecosystem that turns methodological research into a truly global, living collaboration.</p>

<h2 id="-lets-connect"><a name="cta"></a> Let’s Connect</h2>

<p>Want to learn how your organization can build similar intelligent data workflows?
Visit <a href="https://karakun.com" target="_blank">karakun.com</a> or reach out to the <a href="https://hibu-platform.com" target="_blank">HIBU team</a>.</p>]]></content><author><name>Hannes</name></author><category term="Collaboration" /><category term="Project Management" /><category term="Search" /><category term="Build" /><category term="GitHub CI/CD" /><summary type="html"><![CDATA[A practical example of a lightweight research infrastructure that enables global research collaboration through automated data pipelines and CI/CD.]]></summary></entry><entry><title type="html">Devoxx Morocco 2025: A Java Conference with a Unique Community Spirit</title><link href="https://dev.karakun.com/2025/12/18/DevoxxMA.html" rel="alternate" type="text/html" title="Devoxx Morocco 2025: A Java Conference with a Unique Community Spirit" /><published>2025-12-18T00:00:00+00:00</published><updated>2025-12-18T00:00:00+00:00</updated><id>https://dev.karakun.com/2025/12/18/DevoxxMA25</id><content type="html" xml:base="https://dev.karakun.com/2025/12/18/DevoxxMA.html"><![CDATA[<hr />

<p>Devoxx Morocco matters in 2025 because it exemplifies how regional developer ecosystems in the MEA region are rapidly maturing: it blends deep Java and cloud-native expertise with strong community-driven culture, attracts global industry leaders, and provides a platform where emerging talent and established experts collaborate on modern engineering challenges.</p>

<hr />

<h2 id="article-navigation">Article Navigation</h2>
<ul>
  <li><a href="#devoxx">Introduction: Inside Devoxx Morocco, the leading Java conference in MEA</a></li>
  <li><a href="#history">A Brief History of Devoxx Morocco</a></li>
  <li><a href="#first-impression">First Impressions of Devoxx Morocco 2025</a></li>
  <li><a href="#conference">Conference Tracks at Devoxx Morocco 2025</a></li>
  <li><a href="#takeaways">My Personal Takeaways</a></li>
  <li><a href="#cta">Let’s connect</a></li>
</ul>

<hr />

<h2 id="-inside-devoxx-morocco"><a name="devoxx"></a> Inside Devoxx Morocco</h2>
<p><a href="https://devoxx.ma/" target="_blank">Devoxx Morocco</a> is the largest developer conference in the MEA region. 
It took place in mid-November in Marrakech. 
This developer event was characterised by energy, curiosity, and passion for technology, and offered many opportunities to expand one’s network.
In previous editions, the founder even made memorable entrances on stage - once on a bike, another time with a camel - highlighting the playful spirit behind the event.</p>

<p>The conference provided three incredible days of learning, innovation, and community. 
The Devoxx Morocco conference left a lasting impression on me and felt more like an exhilarating party than a conventional conference. 
The atmosphere was high-energy, respectful, and relaxed, marked by a free-spirited vibe and friendly face-to-face conversations, even with high-level industry players. 
In addition, there were many opportunities to learn about Morocco’s culture and history and to interact with locals - whether it was about current technological challenges and practices, or conversations about the best places to visit. 
When we immerse ourselves in new cultures, we gain the ability to see the bigger picture and a deeper understanding of the problems and challenges that we actually want to tackle with the help of technology.</p>

<p>What impressed me most, however, was the fact that participants and speakers were brought together with peers and professionals working on similar projects and topics. 
This was an incredible opportunity to learn and broaden one’s technological perspective. 
It demonstrated how well-thought-out the conference organisation was and how knowledgeable the organisers were. 
I, for example, had the opportunity to meet developers from the Miro team as well as OpenFeature community leaders.</p>

<h2 id="-a-brief-history-of-devoxx-morocco"><a name="history"></a> A Brief History of Devoxx Morocco</h2>

<p>The 2025 edition marked the 12th iteration of the conference. 
It was a milestone in its remarkable growth. 
Devoxx Morocco began in 2014 under the name JMaghreb, a local Arabic-language community event dedicated to strengthening the regional developer ecosystem. 
Historically, the conference has roots in Java. 
In 2017, the conference officially joined the Devoxx family and, thus, became part of one of the world’s most respected networks of developer events.</p>

<p>The first Devoxx-branded edition was held in Casablanca. 
Over the years, the conference has travelled across Morocco. 
In 2018, the conference went to Marrakech. 
In the following years, the conference took place in Agadir. 
In 2024 and 2025, the event returned to Marrakech, reconnecting with the city where some of its most memorable editions took place. 
From a local gathering to a major international developer conference, Devoxx Morocco has grown into a vibrant hub for knowledge exchange, innovation, and community in the MEA region.</p>

<h2 id="-first-impressions-of-devoxx-morocco-2025"><a name="first-impression"></a> First Impressions of Devoxx Morocco 2025</h2>
<p>The dreamlike venue, an impressive congress and hotel hall in modern Moroccan style, was spacious and open, facilitating interaction. 
Meeting highly skilled international professionals was a highlight. 
All of them were remarkably accessible and eager to engage in direct conversations, further enhancing the dynamic and interactive spirit of the three-day event. 
These conversations broadened my perspectives because we discussed the challenges, benefits, and problems that developers encounter in their daily work, and gained insights into developers’ practices as well as into companies’ strategies and structures.</p>

<h2 id="-conference-tracks-at-devoxx-morocco-2025"><a name="conference"></a> Conference Tracks at Devoxx Morocco 2025</h2>

<p>Devoxx Morocco 2025 featured eight distinct tracks covering the full spectrum of today’s technological landscape: GenAI, cloud, security, people &amp; culture, and more. 
The “People &amp; Culture” track is considered one of the most important, highlighting the human side of technology and how teams, careers, and communities evolve. 
Among the many standout topics in that track were talks on keeping children safe on the internet, the realities of digital nomad life. 
The schedule also included career development sessions such as the rise to engineering manager, and even guidance on how to create your first conference talk - delivered by a remarkable 16-year-old speaker.</p>

<p>Quality remains at the heart of Devoxx Morocco’s programme. 
The programme committee pays particular attention to the background, expertise, and relevance of speakers. 
As a result, they ensure that every invited speaker delivers valuable, high-level content. 
The event also serves as an important platform for public-speaking opportunities, especially for emerging voices in the tech community.</p>

<h2 id="my-personal-takeaways"><a name="takeaways"></a>My Personal Takeaways</h2>

<p>The Moroccan community is incredibly talented. 
The excellent education in computer science and unparalleled networking opportunities enable valuable exchanges with top minds in the industry. 
And many young talents seize the opportunity to join leading companies in Europe and the US. 
Devoxx Morocco serves precisely this purpose and allows young talents to present their talks. 
This year, the youngest speaker was 16 years old.</p>

<p>An official post-conference event included karaoke, a guided tour of Marrakech, and a dinner in a desert restaurant. 
These activities created a unique environment, allowing speakers to see their peers in action, express themselves spontaneously, and enjoy a highly social experience. 
Overall, Devoxx Morocco was a breath of fresh air in today’s IT event landscape, combining insightful content with strong networking opportunities. 
The conference left a lasting impression on me, broadening my horizons and connecting me with incredible members of the Java community.</p>

<h2 id="-lets-connect"><a name="cta"></a> Let’s connect!</h2>
<p>Do you have questions about Devoxx Morocco, other developer conferences, or specific developer topics or AI? 
<a href="/people/iryna" target="_blank">Feel free to reach out</a>. 
I’m always happy to exchange knowledge, ideas, and experiences.</p>]]></content><author><name>iryna</name></author><category term="Conferences" /><category term="Devoxx" /><category term="Java" /><category term="Community" /><category term="Tech" /><summary type="html"><![CDATA[Experience Devoxx Morocco 2025, the leading Java conference in MEA, offering expert talks, rich community exchange, and unforgettable cultural experiences in Marrakech.]]></summary></entry></feed>