Jekyll2026-04-16T12:49:35+02:00https://inoio.de/atom.xmlinoio gmbhPragmatisch Funktional – Zustand, Einfachheit und Magie2025-11-11T16:00:00+01:002025-11-11T16:00:00+01:00https://inoio.de/blog/2025/11/11/pragmatisch-funktional<p>Als Student in den 90ern kam ich das erste Mal mit funktionaler Programmierung in Berührung, und es tat sich eine faszinierende Welt auf. Alleine die Begriffe klangen magisch: <em>Anonyme Funktionen, Tail-Recursion, Higher-Order-Functions</em>. <em>Lazy Evaluation</em> erlaubte unendlich lange Listen, und Typen wurden inferiert.</p> <p>Gleichzeitig dominierten in der Arbeitswelt C++ und Java, und mir wurde schnell klar, dass ich in der Praxis der funktionalen Programmierung kaum begegnen werde.</p> <p>Sprung nach 2025: Nun arbeite ich seit sieben Jahren in Projekten, die funktionale Sprachen ganz pragmatisch einsetzen. Wie kommt diese erstaunliche Renaissance, und was bedeutet das für den Programmieralltag?</p> <h2 id="horizontale-skalierung">Horizontale Skalierung</h2> <p>Ein Konzept, das auf den ersten Blick nichts mit funktionaler Programmierung zu tun hat, ist <em>Horizontale Skalierung</em>: Von einer Anwendung werden mehrere gleichartige Instanzen nebeneinandergestellt, die den Dienst gleichberechtigt erbringen. Das Konzept hat sich durchgesetzt, denn neben besagter Skalierung ermöglicht es auch eine höhere Ausfallsicherheit: Wenn eine Komponente nicht läuft, sind die anderen Instanzen noch verfügbar. Auch Rolling Deployments ohne Downtime werden dadurch möglich.</p> <p>Spannend wird es, wenn ich in meiner Anwendung Zustand habe, mir also zum Beispiel die Aktionen eines Nutzers merke. Wenn jetzt eine andere Instanz den Dienst übernimmt, kennt diese den Zustand nicht. Ich erinnere mich, dass in den Anfangstagen hier für <code class="language-plaintext highlighter-rouge">Tomcat Worker</code> <em>Sticky sessions</em> in Mode waren, der Nutzer verband sich technisch immer mit derselben Instanz der Anwendung, die dann den Zustand hielt.</p> <p>Deutlich robuster wurde es, als der Zustand der Anwendung in eine Datenbank verlagert wurde: Hier landet eine Anfrage in einer zufällig ausgewählten Instanz, und diese liest und speichert ihren Zustand in einer Datenbank, die von allen Instanzen geteilt wird.</p> <p>Michael Sperber formulierte es in einem Seminar bei uns so: “Der Zustand der Anwendung wird an den Rand der Anwendung gedrängt”. Und damit sind wir plötzlich ganz nahe bei der Funktionalen Programmierung.</p> <h2 id="auftritt-funktionale-programmierung">Auftritt funktionale Programmierung</h2> <p><img src="/images/blog/magie.jpg" alt="Vorhang auf" /> Foto © Fabian Nilius</p> <p>Keinen Zustand zu halten, ist ein Kern funktionaler Programmierung. Wenn wir also schon Anwendungen haben, die keinen Zustand halten, ist der Weg zu funktionalem Code nicht weit. Wie kommen wir dahin? Und was wären die Vorteile davon?</p> <p>Der Übergang zu funktionaler Programmierung wird erleichtert durch Sprachen wie Kotlin oder Scala, die interoperabel mit Java sind und sowohl funktionale als imperative Konzepte unterstützen. Im Kotlin-Code sieht der Unterschied zwischen imperativ und funktional so aus:</p> <p>Ein imperatives Eis, der Zustand wird mehrmals geändert.</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="py">eis</span> <span class="p">=</span> <span class="nc">Eis</span><span class="p">()</span> <span class="n">eis</span><span class="p">.</span><span class="nf">neueKugel</span><span class="p">(</span><span class="s">"Schokolade"</span><span class="p">)</span> <span class="n">eis</span><span class="p">.</span><span class="nf">neueKugel</span><span class="p">(</span><span class="s">"Birne"</span><span class="p">)</span> <span class="n">eis</span><span class="p">.</span><span class="nf">mitStreusel</span><span class="p">(</span><span class="s">"Haselnuss"</span><span class="p">)</span> </code></pre></div></div> <p>Ein funktionales Eis ändert nicht seinen Zustand, sondern wir bekommen bei jedem Aufruf eine geänderte Kopie zurück:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">eis</span> <span class="p">=</span> <span class="nc">Eis</span><span class="p">()</span> <span class="p">.</span><span class="nf">neueKugel</span><span class="p">(</span><span class="s">"Schokolade"</span><span class="p">)</span> <span class="p">.</span><span class="nf">neueKugel</span><span class="p">(</span><span class="s">"Birne"</span><span class="p">)</span> <span class="p">.</span><span class="nf">mitStreusel</span><span class="p">(</span><span class="s">"Haselnuss"</span><span class="p">)</span> </code></pre></div></div> <p>Das ist <a href="https://en.wikipedia.org/wiki/Fluent_interface">fluent-interface-Stil</a>, wie man ihn in Java von Buildern oder Streams kennt, bei dem die Aufrufe hintereinander gehängt werden können.</p> <p>Erleichtert wird das durch Kotlins <code class="language-plaintext highlighter-rouge">data class</code> und Scalas <code class="language-plaintext highlighter-rouge">case class</code>, die eine mächtige und gleichzeitig einfache Grundlange bieten, um funktionale Klassen zu bauen. Wie im obigen Eis-Beispiel sind diese Klassen typischerweise immutable und bieten eine copy/clone-Methode, um eine geänderte Kopie zu erzeugen.</p> <h2 id="seiteneffekte-machen-das-lebens-kompliziert">Seiteneffekte machen das Lebens kompliziert</h2> <p>Was aber bringt der Umstieg?</p> <p>Für uns ist funktionaler Code oft einfacher zu verstehen, weil er weniger Überraschungen enthält. Gerade in Legacy-Code gibt es immer wieder Seiteneffekte, in denen Instanzen an überraschenden Stellen modifiziert werden, der Code wird dadurch schwieriger verständlich und schwerer änderbar.</p> <p>Betrachten wir folgendes Stück Java-Code aus dem Apache Tika Projekt:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">parse</span><span class="o">(</span><span class="nc">InputStream</span> <span class="n">stream</span><span class="o">,</span> <span class="nc">ContentHandler</span> <span class="n">handler</span><span class="o">,</span> <span class="nc">Metadata</span> <span class="n">metadata</span><span class="o">,</span> <span class="nc">ParseContext</span> <span class="n">context</span><span class="o">)</span> </code></pre></div></div> <p>Es ist aus dem Schnipsel nicht ersichtlich, welche der Parameter in der Methode eventuell im <code class="language-plaintext highlighter-rouge">parse</code> geändert wird, tatsächlich modifizieren einige Parser den <code class="language-plaintext highlighter-rouge">ParseContext</code>.</p> <p>In einer funktionalen Welt ist der Effekt offensichtlich: Wenn eine Methode etwas ändern will, muss sie eine entsprechende Kopie an den Aufrufer zurückgeben. So würde diese Funktion einen aktualisierten ParseContext zurückgeben. Wir sehen an der Signatur in der funktionalen Welt sofort, welche Effekte die Funktion hat.</p> <h2 id="multithreading-entschärft">Multithreading entschärft</h2> <p>Da ein Backend typischerweise viele Anfragen gleichzeitig bearbeitet, ist Multithreading allgegenwärtig, gleichzeitig ist einem Stück Code nicht einfach anzusehen, wo es von mehreren Threads durchlaufen wird.</p> <p>Multithreading-Bugs entstehen, wenn ein Zustand aus verschiedenen Threads gleichzeitig bearbeitet wird. Dies sind schwer analysierbare und kaum reproduzierbare Bugs: Oft sind die typischen Tests hier grün, und der Fehler tritt erst bei bestimmten Abläufen in der Produktion auf.</p> <p>In der funktionalen Welt ist dies aber ein viel kleineres Problem, weil es keine Zustände gibt, auf die die Threads zugreifen.</p> <h2 id="hindernisse-in-der-praxis">Hindernisse in der Praxis</h2> <p>Reicht es für die Umstellung, einfach alle <code class="language-plaintext highlighter-rouge">var</code> zu <code class="language-plaintext highlighter-rouge">val</code> zu ändern? Nach unserer Erfahrung ist die Datenbankanbindung oft eine Herausforderung auf dem Weg zur funktionalen Programmierung. Gerade JPA/Hibernate setzt auf modifizierbare Entities und ist nicht für die Nutzung mit immutable (val) data classes vorgesehen. Tatsächlich werden Änderungen an Entities vom DB-Framework im Hintergrund bis in die Datenbank persistiert — Seiteneffekt par excellence.</p> <p>Für eine klare funktionale Codebasis will man zu anderen Datenbank-Libraries wechseln. Ähnlich zu <code class="language-plaintext highlighter-rouge">spring data jpa</code> ist <code class="language-plaintext highlighter-rouge">spring data jdbc</code>, das funktionale Entities unterstützt – allerdings eigene <a href="https://spring.io/blog/2023/08/29/spring-data-jdbc-how-to-maintain-your-database-schema">Konzepte</a> und Herausforderungen hat.</p> <h2 id="this-is-where-the-magic-happens">This is where the magic happens</h2> <p>Noch einmal zusammengefasst: Insbesondere durch <em>Horizontale Skalierung</em> verbreiten sich Anwendungen, die keinen fachlichen Zustand halten. Hier machen es hybride Sprachen wie Scala und Kotlin leicht, in die funktionale Welt zu wechseln, Datenbank-Anbindungen können ein Hindernis sein. Durch den Verzicht auf Zustände macht Funktionale Programmierung aus unserer Sicht den Code einfacher.</p> <p>Es gibt also sehr weltliche, pragmatische Gründe, heute funktionale Programmierung zu verwenden. Was aber ist mit der Magie? Wenn man etwas genauer hinschaut, gibt es diese immer noch: <a href="https://www.youtube.com/watch?v=qgfCmQ-2tW0">Effekt-Systeme</a>, Monaden, Type-level programming, <a href="https://www.cs.tufts.edu/~nr/cs257/archive/simon-peyton-jones/contracts.pdf">Kombinatoren</a> — es wartet eine faszinierende Welt.</p>Fabian NiliusAls Student in den 90ern kam ich das erste Mal mit funktionaler Programmierung in Berührung, und es tat sich eine faszinierende Welt auf. Alleine die Begriffe klangen magisch: Anonyme Funktionen, Tail-Recursion, Higher-Order-Functions. Lazy Evaluation erlaubte unendlich lange Listen, und Typen wurden inferiert.Claude Code mit AWS Bedrock nutzen - Schritt-für-Schritt Anleitung2025-09-24T09:00:00+02:002025-09-24T09:00:00+02:00https://inoio.de/blog/2025/09/24/claude-code-mit-aws-bedrock-nutzen<p>Claude Code bringt KI-Assistenz direkt in die lokale Entwicklungsumgebung. Während Claude Code standardmäßig Anthropics Cloud-Dienste nutzt, bietet die Integration mit AWS Bedrock zusätzliche Flexibilität und Kontrolle über die verwendeten Modelle. In diesem Artikel zeige ich anhand unserer konkreten inoio-Konfiguration, wie du Claude Code mit AWS Bedrock einrichtest und nutzt.</p> <h2 id="was-ist-claude-code">Was ist Claude Code?</h2> <p>Claude Code ist Anthropics offizielles CLI-Tool, das KI-gestützte Softwareentwicklung direkt im Terminal ermöglicht. Es bringt intelligente Code-Analyse für komplexe Codebases mit sich, ermöglicht automatisierte Code-Generierung von einzelnen Funktionen bis hin zu ganzen Features und unterstützt projektübergreifende Refaktorierung zur systematischen Verbesserung der Codequalität. Zusätzlich bietet es interaktive Debugging-Hilfe mit präziser Fehleranalyse und konkreten Lösungsvorschlägen.</p> <h2 id="warum-aws-bedrock">Warum AWS Bedrock?</h2> <p>AWS Bedrock ist Amazons verwalteter Service für Foundation Models, der Zugang zu verschiedenen KI-Modellen über eine einheitliche API bietet. Für Claude Code bedeutet die Bedrock-Integration erweiterte Kontrolle über die Modellauswahl, da neben verschiedenen Claude-Modellen auch andere Foundation Models wie etwa von Amazon, Meta oder Cohere verfügbar sind. Zusätzlich ermöglicht es regionale Kontrolle über die Datenverarbeitung, erfüllt unternehmensweite Compliance-Anforderungen und bietet direkte AWS-Abrechnung.</p> <h2 id="aws-konfiguration">AWS-Konfiguration</h2> <p>Das prinzipielle Vorgehen für die Bedrock-Integration ist in der offiziellen Anthropic-Dokumentation unter <a href="https://docs.anthropic.com/en/docs/claude-code/amazon-bedrock">https://docs.anthropic.com/en/docs/claude-code/amazon-bedrock</a> beschrieben. Hier dokumentiere ich eine konkrete Umsetzung, die als Referenz für dein eigenes Setup dienen kann.</p> <h3 id="aws-account-und-api-schlüssel">AWS Account und API-Schlüssel</h3> <p>Für das Setup benötigst du einen AWS-Account mit ausreichenden Rechten, um die benötigten Ressourcen zu erstellen. Ein Root-Account funktioniert immer, aber du kannst auch einen IAM-Benutzer mit entsprechenden Berechtigungen verwenden. Der erste Schritt besteht darin, dich mit deinem Account in die AWS-Konsole einzuloggen und anschließend zu den Bedrock API Keys zu navigieren.</p> <p>In der Bedrock-Konsole erstellst du einen Long Term API Key, den du für die späteren Umgebungsvariablen benötigst. Dieser API-Schlüssel ist das Herzstück der Authentication zwischen Claude Code und AWS Bedrock. Kopiere den generierten Key sorgfältig, da er nur einmal angezeigt wird. Behandle diesen API-Schlüssel immer wie ein Passwort!</p> <p><img src="/images/blog/claude-code-aws-bedrock/bedrock-api-keys.png" alt="AWS Bedrock API Keys" /> <em>AWS Bedrock API Keys Übersicht mit Long-term API Keys</em></p> <h3 id="konfigurationsskript-erstellen">Konfigurationsskript erstellen</h3> <p>Mit dem API-Schlüssel kannst du nun das Konfigurationsskript erstellen. Erstelle eine Datei namens <code class="language-plaintext highlighter-rouge">bedrock.sh</code> und füge folgende Konfiguration ein:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">AWS_BEARER_TOKEN_BEDROCK</span><span class="o">=</span>&lt;<span class="nt">--</span> INSERT TOKEN HERE <span class="nt">--</span><span class="o">&gt;</span> <span class="c"># Enable Bedrock integration</span> <span class="nb">export </span><span class="nv">CLAUDE_CODE_USE_BEDROCK</span><span class="o">=</span>1 <span class="nb">export </span><span class="nv">AWS_REGION</span><span class="o">=</span>eu-central-1 <span class="c"># or your preferred region</span> <span class="c"># Optional: Override the region for the small/fast model (Haiku)</span> <span class="c">#export ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION=us-east-1</span> <span class="c"># Using inference profile ID</span> <span class="nb">export </span><span class="nv">ANTHROPIC_MODEL</span><span class="o">=</span><span class="s1">'eu.anthropic.claude-sonnet-4-20250514-v1:0'</span> <span class="nb">export </span><span class="nv">ANTHROPIC_SMALL_FAST_MODEL</span><span class="o">=</span><span class="s1">'eu.anthropic.claude-3-haiku-20240307-v1:0'</span> <span class="c"># Using application inference profile ARN</span> <span class="c">#export ANTHROPIC_MODEL='arn:aws:bedrock:us-east-2:your-account-id:application-inference-profile/your-model-id'</span> <span class="c"># Optional: Disable prompt caching if needed</span> <span class="c">#export DISABLE_PROMPT_CACHING=1</span> <span class="c"># Recommended output token settings for Bedrock</span> <span class="nb">export </span><span class="nv">CLAUDE_CODE_MAX_OUTPUT_TOKENS</span><span class="o">=</span>4096 <span class="nb">export </span><span class="nv">MAX_THINKING_TOKENS</span><span class="o">=</span>1024 </code></pre></div></div> <p>Ersetze den Platzhalter <code class="language-plaintext highlighter-rouge">&lt;-- INSERT TOKEN HERE --&gt;</code> durch deinen tatsächlichen API-Schlüssel.</p> <h3 id="benutzerberechtigungen-konfigurieren">Benutzerberechtigungen konfigurieren</h3> <p>Beim Anlegen des API-Schlüssels wird automatisch ein dazugehöriger Benutzer in AWS IAM erstellt. Dieser Benutzer benötigt entsprechende Berechtigungen für Bedrock-Services. Navigiere zur IAM-Konsole und suche den neu erstellten Benutzer. Unter “Permissions” fügst du die Berechtigung “AmazonBedrockFullAccess” hinzu, die alle notwendigen Rechte für die Bedrock-Nutzung umfasst.</p> <p><img src="/images/blog/claude-code-aws-bedrock/iam-user-permissions.png" alt="IAM User Permissions" /> <em>IAM-Benutzer mit AmazonBedrockFullAccess Berechtigung</em></p> <h3 id="console-zugang-einrichten">Console-Zugang einrichten</h3> <p>Ein wichtiger Schritt, der oft übersehen wird, ist die Einrichtung des Console-Zugangs für den erstellten Benutzer. Du benötigst diesen Zugang mindestens einmal, um als dieser Benutzer die Modell-Zugangsrechte beantragen zu können. Navigiere zu den “Security Credentials” des Benutzers und aktiviere “Console Access”. Vergib ein sicheres Passwort, das du dir notierst.</p> <h3 id="modell-zugriffsrechte-beantragen">Modell-Zugriffsrechte beantragen</h3> <p>Nun loggst du dich mit dem neu erstellten Benutzer in die AWS-Konsole ein und navigierst zu Bedrock. Unter “Configure and learn” findest du den Bereich “Model access”. Hier beantragst du Berechtigungen für alle Modelle, die du nutzen möchtest - neben Claude-Modellen stehen auch andere Foundation Models verschiedener Anbieter zur Verfügung. Dieser Genehmigungsprozess dauert in der Regel etwa fünf Minuten. Refreshe die Seite gelegentlich, um den Status zu überprüfen.</p> <p><img src="/images/blog/claude-code-aws-bedrock/bedrock-model-access.png" alt="Bedrock Model Access" /> <em>Bedrock Model Access Seite zum Beantragen von Modell-Berechtigungen</em></p> <h3 id="inference-profile-ids-verstehen">Inference Profile IDs verstehen</h3> <p>Ein wichtiger Unterschied, der häufig zu Verwirrung führt, betrifft die Modell-Identifikatoren. Die IDs, die du im Konfigurationsskript verwendest, sind nicht die Model IDs aus dem “Model Catalog”, sondern die “Inference Profile IDs”. Diese findest du unter “Infer” &gt; “Cross Region Inference” in der Bedrock-Konsole. Diese Unterscheidung ist entscheidend für die korrekte Funktion der Integration.</p> <p><img src="/images/blog/claude-code-aws-bedrock/bedrock-inference-profiles.png" alt="Bedrock Inference Profiles" /> <em>Cross Region Inference Seite mit den korrekten Inference Profile IDs</em></p> <h2 id="claude-code-aktivieren">Claude Code aktivieren</h2> <p>Nachdem du alle Vorbereitungen getroffen hast, ist es Zeit, Claude Code mit der Bedrock-Integration zu starten. Trage alle Werte nach deinen Wünschen in das obige Shell-Skript ein und speichere es als <code class="language-plaintext highlighter-rouge">bedrock.sh</code>. Dann führst du folgende Befehle aus:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">source</span> ./bedrock.sh claude </code></pre></div></div> <p>Wenn alles geklappt hat, arbeitet Claude nun mit AWS Bedrock. Du merkst den Unterschied daran, dass die Modell-Auswahl jetzt aus deinen konfigurierten Bedrock-Modellen stammt und die Antworten über deine AWS-Infrastruktur verarbeitet werden.</p> <h2 id="wichtige-hinweise-zur-konfiguration">Wichtige Hinweise zur Konfiguration</h2> <p>Es finden sich eigentlich alle Hinweise in der Dokumentation von Anthropic, die oben verlinkt ist. Hier nochmal in Kürze das Wichtigste:</p> <h3 id="token-limits-verstehen">Token-Limits verstehen</h3> <p>Die konfigurierten Token-Limits sind besonders wichtig für die praktische Nutzung. <code class="language-plaintext highlighter-rouge">CLAUDE_CODE_MAX_OUTPUT_TOKENS=4096</code> bestimmt, wie ausführlich Claude antworten kann, während <code class="language-plaintext highlighter-rouge">MAX_THINKING_TOKENS=1024</code> die interne “Denkzeit” des Modells begrenzt. Diese Werte haben sich in der Praxis für die meisten Entwicklungsaufgaben als geeignet erwiesen.</p> <h3 id="prompt-caching">Prompt Caching</h3> <p>Standardmäßig ist Prompt Caching aktiviert, was die Performance bei wiederholten ähnlichen Anfragen verbessert. Falls du aus irgendeinem Grund komplett frische Antworten benötigst, kannst du die auskommentierte Zeile <code class="language-plaintext highlighter-rouge">DISABLE_PROMPT_CACHING=1</code> aktivieren.</p> <h2 id="troubleshooting-häufiger-probleme">Troubleshooting häufiger Probleme</h2> <h3 id="model-not-found-fehler">“Model not found” Fehler</h3> <p>Wenn Claude eine Fehlermeldung bezüglich nicht gefundener Modelle ausgibt, überprüfe die Inference Profile IDs in deinem Skript. Diese ändern sich gelegentlich und müssen aus der aktuellen Bedrock-Konsole übernommen werden. Außerdem kann es vorteilhaft sein, weniger populäre oder aktuelle Modelle zu verwenden. So nutze ich mittlerweile eher Sonnet 3.7 als das neuere Sonnet 4, um Request Limits zu umgehen.</p> <h3 id="berechtigungsfehler">Berechtigungsfehler</h3> <p>Sollten Berechtigungsfehler auftreten, überprüfe, ob der automatisch erstellte Benutzer wirklich die <code class="language-plaintext highlighter-rouge">AmazonBedrockFullAccess</code> Berechtigung erhalten hat. Manchmal dauert es einige Minuten, bis Berechtigungsänderungen wirksam werden.</p> <h2 id="fazit">Fazit</h2> <p>Mit dieser Konfiguration ist Claude Code erfolgreich mit AWS Bedrock verbunden. Die initiale Einrichtung ist aufwendiger als die Standard-Claude-Nutzung, bietet aber mehr Flexibilität und Kontrolle.</p> <p>In Unternehmensumgebungen mit Datenschutz- und Compliance-Anforderungen bietet diese Lösung praktische Vorteile: klare Kontrolle über den Verarbeitungsort der Daten, transparente Kostenstruktur und die Möglichkeit, zwischen verschiedenen Modellen zu wechseln.</p> <p>Claude Code ist nun mit AWS Bedrock konfiguriert und kann über das Terminal genutzt werden. Du kannst mit verschiedenen Foundation Models experimentieren und die passende Konfiguration für deine Entwicklungsaufgaben finden.</p> <p>Das ganze Vorgehen lässt sich auch einfach auf andere Agentic Coding Lösungen übertragen: Cline bspw. nutzt einen ganz ähnlichen Menchanismus: <a href="https://docs.cline.bot/provider-config/aws-bedrock/api-key">https://docs.cline.bot/provider-config/aws-bedrock/api-key</a>.</p> <h2 id="nachtrag">Nachtrag</h2> <p>Es kann sinnvoll sein, mit verschiedenen Modellen schnell und einfach zu experimentieren. Statt die Umgebungsvariablen dann bspw. in der <code class="language-plaintext highlighter-rouge">.profile</code>, <code class="language-plaintext highlighter-rouge">.bashrc</code>, <code class="language-plaintext highlighter-rouge">.zshrc</code> Datei zu setzen, habe ich das als Shellscript parat, welches dann das schöne Utility <a href="https://github.com/charmbracelet/gum">gum</a> verwendet. Dann kann man fix sein gewünschtes Modell auswählen. Die ensprechenden Zeilen im <code class="language-plaintext highlighter-rouge">bedrock.sh</code> Script würden dann so aussehen:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Interactive model selection using gum</span> <span class="nb">export </span><span class="nv">ANTHROPIC_MODEL</span><span class="o">=</span><span class="si">$(</span>gum choose <span class="nt">--header</span><span class="o">=</span><span class="s2">"Select main model:"</span> <span class="se">\</span> <span class="s2">"eu.anthropic.claude-3-7-sonnet-20250219-v1:0"</span> <span class="se">\</span> <span class="s2">"eu.anthropic.claude-sonnet-4-20250514-v1:0"</span> <span class="se">\</span> <span class="s2">"eu.anthropic.claude-3-5-sonnet-20240620-v1:0"</span> <span class="se">\</span> <span class="s2">"eu.mistral.pixtral-large-2502-v1:0"</span> <span class="si">)</span> </code></pre></div></div> <p>Viel Spass und happy coding!</p>["Markus Klink"]Claude Code bringt KI-Assistenz direkt in die lokale Entwicklungsumgebung. Während Claude Code standardmäßig Anthropics Cloud-Dienste nutzt, bietet die Integration mit AWS Bedrock zusätzliche Flexibilität und Kontrolle über die verwendeten Modelle. In diesem Artikel zeige ich anhand unserer konkreten inoio-Konfiguration, wie du Claude Code mit AWS Bedrock einrichtest und nutzt.Co- and Contravariance Part 2: Advanced Concepts2025-09-22T10:00:00+02:002025-09-22T10:00:00+02:00https://inoio.de/blog/2025/09/22/co-and-contravariance-part-2<p>This is the second part of our exploration into co- and contravariance. In <a href="/blog/2025/09/17/co-and-contravariance/">part one</a>, we covered the basic concepts and intuitions behind variance in type systems. Hopefully it already helped you to gain better understanding in the various co- and contravariance concepts. If you still struggle and change minusses to plusses or vice versa (or super to extend clauses) then this second part is for you.</p> <h2 id="recap-function-subtyping-and-variance">Recap: Function Subtyping and Variance</h2> <p>Before diving into advanced topics, let’s recap when one function can be substituted for another.</p> <p>For functions</p> <div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">f</span> <span class="o">::</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="n">b</span> <span class="n">g</span> <span class="o">::</span> <span class="n">c</span> <span class="o">-&gt;</span> <span class="n">d</span> </code></pre></div></div> <p>we say <code class="language-plaintext highlighter-rouge">g &lt;: f</code> (g is a subtype of f, meaning g can be used wherever f is required).</p> <p>The subtyping relationship for functions follows these rules:</p> <ol> <li><strong>Input types</strong>: A &lt;: C (input is contravariant)</li> <li><strong>Output types</strong>: D &lt;: B (output is covariant)</li> </ol> <p>This means:</p> <ul> <li>The subtype function must accept a supertype of what the original function accepts (parameters are contravariant)</li> <li>The subtype function must return a subtype of what the original function returns (return parameters are covariant)</li> </ul> <p>Let’s also remember that in the Scala typesystem, contravariant types are denoted by a <code class="language-plaintext highlighter-rouge">-</code>, and covariant types by a <code class="language-plaintext highlighter-rouge">+</code>. We can also say a contravariant parameters are in a negative position and covariant parameters in a positive position.</p> <p>We visualize this relationship with a simple graph:</p> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>f: A -------&gt; B ^ | ^ | | | | v | g: C -------&gt; D </code></pre></div> </div> </div> <p>Having said all this, we can turn towards higher order functions.</p> <h2 id="functions-with-higher-order-functions-as-parameters">Functions with higher order functions as parameters</h2> <p>Many programming languages use curried functions as their default style to write functions (haskell, idris, elm, purescript), other allow curried functions (Scala, typescript, javascript, F#). In curried functions there is no group of input parameters, instead parameters are resolved one by one and the result of this resolution is again a function.</p> <p>Curried function types are e.g. written like this:</p> <div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">f</span> <span class="o">::</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="n">b</span> <span class="o">-&gt;</span> <span class="n">c</span> </code></pre></div></div> <h3 id="association-and-variance-positions">Association and Variance Positions</h3> <p>Interestingly, the notation <code class="language-plaintext highlighter-rouge">a -&gt; b -&gt; c</code> is ambiguous regarding association. It could be parsed in two ways:</p> <div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">f</span> <span class="o">::</span> <span class="p">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">b</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">c</span> <span class="c1">-- Function that takes a function as input</span> <span class="n">f</span> <span class="o">::</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="n">b</span> <span class="o">-&gt;</span> <span class="n">c</span><span class="p">)</span> <span class="c1">-- Function that returns a function as output</span> </code></pre></div></div> <p>In most functional languages, the second interpretation (right association) is the default. But understanding both forms helps clarify variance positions.</p> <h4 id="variance-positions-in-nested-functions">Variance Positions in Nested Functions</h4> <p>To understand variance positions in nested functions, we need to remember a key principle: <strong>variance flips in contravariant positions</strong>.</p> <p>Let’s analyze both interpretations:</p> <p><strong>Left association</strong>: <code class="language-plaintext highlighter-rouge">f :: (a -&gt; b) -&gt; c</code></p> <ul> <li>The parameter <code class="language-plaintext highlighter-rouge">(a -&gt; b)</code> is in a contravariant (negative) position</li> <li>Within this parameter: <ul> <li><code class="language-plaintext highlighter-rouge">a</code> is in a contravariant position relative to <code class="language-plaintext highlighter-rouge">(a -&gt; b)</code>, but since <code class="language-plaintext highlighter-rouge">(a -&gt; b)</code> is already contravariant, <code class="language-plaintext highlighter-rouge">a</code> flips to a covariant (positive) position overall</li> <li><code class="language-plaintext highlighter-rouge">b</code> is in a covariant position relative to <code class="language-plaintext highlighter-rouge">(a -&gt; b)</code>, but flips to contravariant (negative) overall</li> </ul> </li> <li><code class="language-plaintext highlighter-rouge">c</code> is in a covariant (positive) position</li> </ul> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> | - | + | | - +| | f :: (a -&gt; b) -&gt; c | + - | + | </code></pre></div> </div> </div> <p><strong>Right association</strong>: <code class="language-plaintext highlighter-rouge">f :: a -&gt; (b -&gt; c)</code></p> <ul> <li><code class="language-plaintext highlighter-rouge">a</code> is in a contravariant (negative) position</li> <li>The return value <code class="language-plaintext highlighter-rouge">(b -&gt; c)</code> is in a covariant (positive) position</li> <li>Within this return value: <ul> <li><code class="language-plaintext highlighter-rouge">b</code> is in a contravariant position relative to <code class="language-plaintext highlighter-rouge">(b -&gt; c)</code>, but remains contravariant (negative) overall</li> <li><code class="language-plaintext highlighter-rouge">c</code> is in a covariant position relative to <code class="language-plaintext highlighter-rouge">(b -&gt; c)</code>, and remains covariant (positive) overall</li> </ul> </li> </ul> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> | - | + | | | - + | f :: a -&gt; (b -&gt; c) | - | - + | </code></pre></div> </div> </div> <p>This pattern of alternating variance positions (often called the “variance dance”) becomes increasingly important when designing higher-order functions and interfaces with complex nested types.</p>["Markus Klink"]This is the second part of our exploration into co- and contravariance. In part one, we covered the basic concepts and intuitions behind variance in type systems. Hopefully it already helped you to gain better understanding in the various co- and contravariance concepts. If you still struggle and change minusses to plusses or vice versa (or super to extend clauses) then this second part is for you.Co- and Contravariance2025-09-17T10:00:00+02:002025-09-17T10:00:00+02:00https://inoio.de/blog/2025/09/17/co-and-contravariance<p>With a simple example, the concept of co- and contravariance is understood. Forever and for everybody! With examples for scala and java developers.</p> <h1 id="inheritance">Inheritance</h1> <p>Let’s start slowly. Concepts in our problem domain can be expressed using inheritance. We usually express this graphically as follows:</p> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-----------+ +----------+ | | | | | A | | Animal | | | | | +-----------+ +----------+ ^ ^ | | +----+------+ +----+-----+ | | | | | B | | Cat | | | | | +-----------+ +----------+ </code></pre></div> </div> </div> <p>Thus we expressed that B is a special kind of A, or that a Cat is a special kind of Animal. Usually, when working with inheritance we want to adhere to the inheritance contract: that is, we want to be able to use the specialised object anywhere where a more general object is required.</p> <p>For our own concern - covariance, invariance or contravariance - we just observe that these concepts do not matter yet.</p> <h1 id="invariance-covariance-and-contravariance">Invariance, Covariance and Contravariance</h1> <p>In the next step we will put the objects of our classes into some other structure. This structure could be a list, a vector, an option, a future. Lets just call it <code class="language-plaintext highlighter-rouge">F</code> for the time being. E.g. we talk about a F of Animals, a F of Cats, a F of As, a F of Bs.</p> <p>In scala, in the simplest possible way, we could start to define a structure F, which operates on As, Bs, Animals or Cats like</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// this does nothing - it just allows us to create typed F instances</span> <span class="k">case</span> <span class="k">class</span> <span class="nc">F</span><span class="o">[</span><span class="kt">A</span><span class="o">]()</span> </code></pre></div></div> <p>And now - with the structure F in place - we can start to talk about the variance of its type parameter A. Syntactically we can differentiate three cases (there are three cases of specifying the type paramter A in our structure F):</p> <dl> <dt><strong>F[A]</strong></dt> <dd>A is invariant</dd> <dt><strong>F[+A]</strong></dt> <dd>A is covariant</dd> <dt><strong>F[-A]</strong></dt> <dd>A is contravariant</dd> </dl> <p>Now let us assume we have defined <code class="language-plaintext highlighter-rouge">Animal</code> and <code class="language-plaintext highlighter-rouge">Cat</code> as in the example below:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">trait</span> <span class="nc">Animal</span> <span class="o">{}</span> <span class="k">trait</span> <span class="nc">Cat</span> <span class="k">extends</span> <span class="nc">Animal</span> <span class="o">{}</span> </code></pre></div></div> <p>then we can, respectively cannot, do the following:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Cat is an animal</span> <span class="n">scala</span><span class="o">&gt;</span> <span class="k">val</span> <span class="nv">animal</span><span class="k">:</span> <span class="kt">Animal</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Cat</span> <span class="o">{}</span> <span class="k">val</span> <span class="nv">animal</span><span class="k">:</span> <span class="kt">Animal</span> <span class="o">=</span> <span class="nc">$anon$1</span><span class="k">@</span><span class="mi">100</span><span class="n">bba26</span> <span class="c1">// An animal is not a cat</span> <span class="n">scala</span><span class="o">&gt;</span> <span class="k">val</span> <span class="nv">cat</span><span class="k">:</span> <span class="kt">Cat</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Animal</span> <span class="o">{}</span> <span class="o">^</span> <span class="n">error</span><span class="k">:</span> <span class="k">type</span> <span class="kt">mismatch</span><span class="o">;</span> <span class="n">found</span> <span class="k">:</span> <span class="kt">Animal</span> <span class="n">required</span><span class="k">:</span> <span class="kt">Cat</span> </code></pre></div></div> <p>The example above still has nothing do to with variance. We need to use our <code class="language-plaintext highlighter-rouge">F</code>.</p> <h2 id="invariance">Invariance</h2> <p>In the example above (no <code class="language-plaintext highlighter-rouge">+</code> or <code class="language-plaintext highlighter-rouge">-</code> in the type parameter) <code class="language-plaintext highlighter-rouge">F</code> is invariant in <code class="language-plaintext highlighter-rouge">A</code>. Invariance implies that even if the type parameter is in an inheritance relationship, the structure <code class="language-plaintext highlighter-rouge">F</code> is not.</p> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----------+ +----------+ +----------+ | | | | | | | Animal | | F[Animal]| | F[Cat] | | | | | | | +----------+ +----------+ +----------+ ^ | +----+-----+ | | | Cat | | | +----------+ </code></pre></div> </div> </div> <p>So, if <code class="language-plaintext highlighter-rouge">Cat</code> is an <code class="language-plaintext highlighter-rouge">Animal</code>, then <code class="language-plaintext highlighter-rouge">F[Cat]</code> is not an <code class="language-plaintext highlighter-rouge">F[Animal]</code> AND <code class="language-plaintext highlighter-rouge">F[Animal]</code> is not a <code class="language-plaintext highlighter-rouge">F[Cat]</code>. That is the case because we did decide that <code class="language-plaintext highlighter-rouge">F</code> should behave this way, since we made the type paramter <code class="language-plaintext highlighter-rouge">A</code> invariant in <code class="language-plaintext highlighter-rouge">F</code>.</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// Pretend that F is covariant.</span> <span class="n">scala</span><span class="o">&gt;</span> <span class="k">val</span> <span class="nv">f_animal</span> <span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Animal</span><span class="o">]</span> <span class="k">=</span> <span class="n">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="o">()</span> <span class="o">^</span> <span class="n">error</span><span class="k">:</span> <span class="k">type</span> <span class="kt">mismatch</span><span class="o">;</span> <span class="n">found</span> <span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="n">required</span><span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Animal</span><span class="o">]</span> <span class="nc">Note</span><span class="k">:</span> <span class="kt">Cat</span> <span class="k">&lt;:</span> <span class="kt">Animal</span><span class="o">,</span> <span class="n">but</span> <span class="k">class</span> <span class="nc">F</span> <span class="n">is</span> <span class="n">invariant</span> <span class="n">in</span> <span class="k">type</span> <span class="kt">A.</span> <span class="kt">You</span> <span class="kt">may</span> <span class="kt">wish</span> <span class="kt">to</span> <span class="kt">define</span> <span class="kt">A</span> <span class="kt">as</span> <span class="kt">+A</span> <span class="kt">instead.</span> <span class="o">(</span><span class="kt">SLS</span> <span class="err">4</span><span class="kt">.</span><span class="err">5</span><span class="o">)</span> <span class="c1">// Pretend that F is contravariant</span> <span class="n">scala</span><span class="o">&gt;</span> <span class="k">val</span> <span class="nv">f_cat</span> <span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="k">=</span> <span class="n">F</span><span class="o">[</span><span class="kt">Animal</span><span class="o">]()</span> <span class="o">^</span> <span class="n">error</span><span class="k">:</span> <span class="k">type</span> <span class="kt">mismatch</span><span class="o">;</span> <span class="n">found</span> <span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Animal</span><span class="o">]</span> <span class="n">required</span><span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="nc">Note</span><span class="k">:</span> <span class="kt">Animal</span> <span class="k">&gt;:</span> <span class="kt">Cat</span><span class="o">,</span> <span class="n">but</span> <span class="k">class</span> <span class="nc">F</span> <span class="n">is</span> <span class="n">invariant</span> <span class="n">in</span> <span class="k">type</span> <span class="kt">A.</span> <span class="kt">You</span> <span class="kt">may</span> <span class="kt">wish</span> <span class="kt">to</span> <span class="kt">define</span> <span class="kt">A</span> <span class="kt">as</span> <span class="kt">-A</span> <span class="kt">instead.</span> <span class="o">(</span><span class="kt">SLS</span> <span class="err">4</span><span class="kt">.</span><span class="err">5</span><span class="o">)</span> </code></pre></div></div> <p>Compare this with the error message, when the type is not in an inheritance relationship with Cat at all:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scala</span><span class="o">&gt;</span> <span class="k">val</span> <span class="nv">f_cat</span> <span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="k">=</span> <span class="n">F</span><span class="o">[</span><span class="kt">Person</span><span class="o">]()</span> <span class="o">^</span> <span class="n">error</span><span class="k">:</span> <span class="k">type</span> <span class="kt">mismatch</span><span class="o">;</span> <span class="n">found</span> <span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Person</span><span class="o">]</span> <span class="n">required</span><span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> </code></pre></div></div> <p>Also have a look at the error messages above: <code class="language-plaintext highlighter-rouge">You may wish to define A as +A instead. (SLS 4.5)</code> and <code class="language-plaintext highlighter-rouge">You may wish to define A as -A instead. (SLS 4.5)</code>. This is the compiler telling us that we can consider to make A covariant (<code class="language-plaintext highlighter-rouge">+</code>) or contravariant (<code class="language-plaintext highlighter-rouge">-</code>).</p> <h2 id="covariance-">Covariance (+)</h2> <p>If we define</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="k">class</span> <span class="nc">F</span><span class="o">[</span><span class="kt">+A</span><span class="o">]()</span> </code></pre></div></div> <p>then <code class="language-plaintext highlighter-rouge">F</code> is covariant in <code class="language-plaintext highlighter-rouge">A</code>. We say that if <code class="language-plaintext highlighter-rouge">Cat</code> is an Animal, then <code class="language-plaintext highlighter-rouge">F[Cat]</code> is an <code class="language-plaintext highlighter-rouge">F[Animal]</code> as well.</p> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----------+ +----------+ | | | | | Animal | | F[Animal]| | | | | +----------+ +----------+ ^ ^ | | +----+-----+ +----+-----+ | | | | | Cat | | F[Cat] | | | | | +----------+ +----------+ </code></pre></div> </div> </div> <p>With this definition of <code class="language-plaintext highlighter-rouge">F</code>, the code which previously had errors in all three cases now works in the case of:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scala</span><span class="o">&gt;</span> <span class="k">val</span> <span class="nv">f_animal</span> <span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Animal</span><span class="o">]</span> <span class="k">=</span> <span class="n">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]()</span> <span class="k">val</span> <span class="nv">f_animal</span><span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Animal</span><span class="o">]</span> <span class="k">=</span> <span class="nf">F</span><span class="o">()</span> </code></pre></div></div> <h2 id="contravariance--">Contravariance (-)</h2> <p>If we define</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="k">class</span> <span class="nc">F</span><span class="o">[</span><span class="kt">-A</span><span class="o">]()</span> </code></pre></div></div> <p>then <code class="language-plaintext highlighter-rouge">F</code> is contravariant in <code class="language-plaintext highlighter-rouge">A</code>. We say that if <code class="language-plaintext highlighter-rouge">Cat</code> is an <code class="language-plaintext highlighter-rouge">Animal</code>, then <code class="language-plaintext highlighter-rouge">F[Animal]</code> is an <code class="language-plaintext highlighter-rouge">F[Cat]</code> as well.</p> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----------+ +----------+ +----------+ | | | | | | | Animal | | F[Cat] | | F[Animal]| | | | | | | +----------+ +----------+ +----+-----+ ^ ^ OR | | | v +----+-----+ +----+-----+ +----------+ | | | | | | | Cat | | F[Animal]| | F[Cat] | | | | | | | +----------+ +----------+ +----------+ </code></pre></div> </div> </div> <p>Please note how the inheritance arrow of <code class="language-plaintext highlighter-rouge">F</code> is strangely inverted. With this definition of <code class="language-plaintext highlighter-rouge">F</code>, the code which previously had errors with invariance in all three cases now works in the case of:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scala</span><span class="o">&gt;</span> <span class="k">val</span> <span class="nv">f_cat</span> <span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="k">=</span> <span class="n">F</span><span class="o">[</span><span class="kt">Animal</span><span class="o">]()</span> <span class="k">val</span> <span class="nv">f_cat</span><span class="k">:</span> <span class="kt">F</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="k">=</span> <span class="nf">F</span><span class="o">()</span> </code></pre></div></div> <h1 id="does-it-have-any-use-whatsoever">Does it have any use, whatsoever?</h1> <h2 id="contravariance">Contravariance</h2> <p>Yes, it does. To illustrate contravariance with a practical example, let’s consider animal feeders.</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">trait</span> <span class="nc">Animal</span> <span class="o">{</span> <span class="k">def</span> <span class="nf">eat</span><span class="o">()</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="nf">println</span><span class="o">(</span><span class="s">"Animal eating"</span><span class="o">)</span> <span class="o">}</span> <span class="k">trait</span> <span class="nc">Cat</span> <span class="k">extends</span> <span class="nc">Animal</span> <span class="o">{</span> <span class="k">override</span> <span class="k">def</span> <span class="nf">eat</span><span class="o">()</span><span class="k">:</span> <span class="kt">Unit</span> <span class="o">=</span> <span class="nf">println</span><span class="o">(</span><span class="s">"Cat eating fish"</span><span class="o">)</span> <span class="o">}</span> <span class="c1">// A contravariant feeder class</span> <span class="k">case</span> <span class="k">class</span> <span class="nc">Feeder</span><span class="o">[</span><span class="kt">-A</span><span class="o">](</span><span class="n">feed</span><span class="k">:</span> <span class="kt">A</span> <span class="o">=&gt;</span> <span class="nc">Unit</span><span class="o">)</span> <span class="c1">// Create our feeders</span> <span class="k">val</span> <span class="nv">animalFeeder</span> <span class="k">=</span> <span class="nc">Feeder</span><span class="o">[</span><span class="kt">Animal</span><span class="o">](</span><span class="n">animal</span> <span class="k">=&gt;</span> <span class="nf">println</span><span class="o">(</span><span class="s">"Feeding standard animal food"</span><span class="o">))</span> <span class="k">val</span> <span class="nv">catFeeder</span> <span class="k">=</span> <span class="nc">Feeder</span><span class="o">[</span><span class="kt">Cat</span><span class="o">](</span><span class="n">cat</span> <span class="k">=&gt;</span> <span class="nf">println</span><span class="o">(</span><span class="s">"Feeding premium cat food"</span><span class="o">))</span> <span class="c1">// This works! Someone expecting a cat feeder can use an animal feeder</span> <span class="c1">// since it knows how to feed any animal (including cats)</span> <span class="k">val</span> <span class="nv">myCatFeeder</span><span class="k">:</span> <span class="kt">Feeder</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="k">=</span> <span class="n">animalFeeder</span> <span class="c1">// ✅ Type-safe assignment</span> <span class="c1">// This would NOT work - someone expecting to feed any animal</span> <span class="c1">// cannot use a specialized cat feeder</span> <span class="c1">// val myAnimalFeeder: Feeder[Animal] = catFeeder // ❌ Type error!</span> </code></pre></div></div> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----------+ +--------------+ | | | | | Animal | | Feeder[Cat] | | | | | +----------+ +--------------+ ^ ^ | | | | +----+-----+ +-----+---------+ | | | | | Cat | |Feeder[Animal]| | | | | +----------+ +--------------+ </code></pre></div> </div> </div> <p>Notice how the inheritance arrow is inverted! While Cat is a subtype of Animal, Feeder[Animal] is a subtype of Feeder[Cat]. This is the essence of contravariance.</p> <p>The intuition: If you have a feeder that can feed ANY animal, it can definitely feed your cat. But if you have a feeder that only knows how to feed cats, it cannot necessarily feed any arbitrary animal.</p> <p>This same pattern applies to function inputs, which is why in Scala a function is defined as <code class="language-plaintext highlighter-rouge">Function[-Input, +Output]</code>. The function is contravariant in its input type.</p> <h2 id="covariance">Covariance</h2> <p>While contravariance is about consuming values (inputs), covariance is about producing values (outputs). Let’s create a complementary example using animal shelters:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Same hierarchy as before</span> <span class="k">trait</span> <span class="nc">Animal</span> <span class="o">{</span> <span class="k">def</span> <span class="nf">speak</span><span class="o">()</span><span class="k">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="s">"Some animal sound"</span> <span class="o">}</span> <span class="k">trait</span> <span class="nc">Cat</span> <span class="k">extends</span> <span class="nc">Animal</span> <span class="o">{</span> <span class="k">override</span> <span class="k">def</span> <span class="nf">speak</span><span class="o">()</span><span class="k">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="s">"Meow"</span> <span class="o">}</span> <span class="c1">// A covariant shelter that provides animals</span> <span class="k">case</span> <span class="k">class</span> <span class="nc">Shelter</span><span class="o">[</span><span class="kt">+A</span><span class="o">](</span><span class="n">get</span><span class="k">:</span> <span class="o">()</span> <span class="o">=&gt;</span> <span class="n">A</span><span class="o">)</span> <span class="c1">// Create our shelters</span> <span class="k">val</span> <span class="nv">catShelter</span> <span class="k">=</span> <span class="nc">Shelter</span><span class="o">[</span><span class="kt">Cat</span><span class="o">](()</span> <span class="k">=&gt;</span> <span class="k">new</span> <span class="nc">Cat</span> <span class="o">{})</span> <span class="k">val</span> <span class="nv">animalShelter</span> <span class="k">=</span> <span class="nc">Shelter</span><span class="o">[</span><span class="kt">Animal</span><span class="o">](()</span> <span class="k">=&gt;</span> <span class="k">new</span> <span class="nc">Animal</span> <span class="o">{})</span> <span class="c1">// This works! Someone expecting any animal can accept a cat</span> <span class="k">val</span> <span class="nv">myAnimalShelter</span><span class="k">:</span> <span class="kt">Shelter</span><span class="o">[</span><span class="kt">Animal</span><span class="o">]</span> <span class="k">=</span> <span class="n">catShelter</span> <span class="c1">// ✅ Type-safe assignment</span> <span class="c1">// This would NOT work - someone expecting a cat</span> <span class="c1">// cannot accept just any animal</span> <span class="c1">// val myCatShelter: Shelter[Cat] = animalShelter // ❌ Type error!</span> </code></pre></div></div> <div style="line-height: 1;"> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----------+ +---------------+ | | | | | Animal | |Shelter[Animal]| | | | | +----------+ +---------------+ ^ ^ | | | | +----+-----+ +-----+--------+ | | | | | Cat | | Shelter[Cat] | | | | | +----------+ +--------------+ </code></pre></div> </div> </div> <p>Notice that unlike with contravariance, the inheritance arrows point in the same direction! If Cat is a subtype of Animal, then Shelter[Cat] is a subtype of Shelter[Animal].</p> <p>The intuition: If you need a shelter that provides animals, a cat shelter works fine because cats are animals. But if you specifically need a cat shelter, a general animal shelter won’t suffice because it might give you a dog or some other non-cat animal.</p> <p>This same pattern applies to function outputs, which is why in Scala a function is defined as <code class="language-plaintext highlighter-rouge">Function[-Input, +Output]</code>. The function is covariant in its output type.</p> <p>We can summarize the pattern:</p> <ol> <li> <p><strong>Contravariance (-A)</strong>: Used when a type <em>consumes</em> values of type A</p> <ul> <li>If B extends A, then Consumer[A] extends Consumer[B]</li> <li>Example: Feeder[-A], Function[-A, B]</li> </ul> </li> <li> <p><strong>Covariance (+A)</strong>: Used when a type <em>produces</em> values of type A</p> <ul> <li>If B extends A, then Producer[B] extends Producer[A]</li> <li>Examples: Shelter[+A], Function[A, +B], List[+A]</li> </ul> </li> </ol> <h1 id="for-java-developers">For Java Developers</h1> <p>If you’re coming from Java, you might find Scala’s variance annotations a bit different. In Java, variance is declared at the usage site rather than at the declaration site (as in Scala).</p> <p>Here’s how our examples would look in Java:</p> <h2 id="java-syntax-for-variance">Java Syntax for Variance</h2> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Our type hierarchy</span> <span class="kd">interface</span> <span class="nc">Animal</span> <span class="o">{</span> <span class="kt">void</span> <span class="nf">eat</span><span class="o">();</span> <span class="o">}</span> <span class="kd">interface</span> <span class="nc">Cat</span> <span class="kd">extends</span> <span class="nc">Animal</span> <span class="o">{</span> <span class="nd">@Override</span> <span class="kt">void</span> <span class="nf">eat</span><span class="o">();</span> <span class="o">}</span> <span class="c1">// Invariance - standard generic with no wildcards</span> <span class="kd">class</span> <span class="nc">Container</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">}</span> <span class="c1">// Covariance - using extends wildcard (similar to +T in Scala)</span> <span class="c1">// "? extends Animal" means "Animal or any subtype of Animal"</span> <span class="nc">Container</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Animal</span><span class="o">&gt;</span> <span class="n">covariantContainer</span><span class="o">;</span> <span class="c1">// Contravariance - using super wildcard (similar to -T in Scala)</span> <span class="c1">// "? super Cat" means "Cat or any supertype of Cat"</span> <span class="nc">Container</span><span class="o">&lt;?</span> <span class="kd">super</span> <span class="nc">Cat</span><span class="o">&gt;</span> <span class="n">contravariantContainer</span><span class="o">;</span> </code></pre></div></div> <h2 id="translating-our-examples-to-java">Translating Our Examples to Java</h2> <p>For contravariance (Feeder example):</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">AnimalFeeder</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span> <span class="kt">void</span> <span class="nf">feed</span><span class="o">(</span><span class="no">T</span> <span class="n">animal</span><span class="o">);</span> <span class="o">}</span> <span class="c1">// In Java, we'd use the super wildcard for contravariance:</span> <span class="nc">AnimalFeeder</span><span class="o">&lt;</span><span class="nc">Animal</span><span class="o">&gt;</span> <span class="n">generalFeeder</span> <span class="o">=</span> <span class="n">animal</span> <span class="o">-&gt;</span> <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Feeding generic food"</span><span class="o">);</span> <span class="nc">AnimalFeeder</span><span class="o">&lt;</span><span class="nc">Cat</span><span class="o">&gt;</span> <span class="n">catFeeder</span> <span class="o">=</span> <span class="n">cat</span> <span class="o">-&gt;</span> <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Feeding cat food"</span><span class="o">);</span> <span class="c1">// This works - using an animal feeder where a cat feeder is expected</span> <span class="nc">AnimalFeeder</span><span class="o">&lt;?</span> <span class="kd">super</span> <span class="nc">Cat</span><span class="o">&gt;</span> <span class="n">feederForMyCat</span> <span class="o">=</span> <span class="n">generalFeeder</span><span class="o">;</span> <span class="c1">// ✅ Legal</span> <span class="c1">// This doesn't work - cat feeder can't handle all animals</span> <span class="c1">// AnimalFeeder&lt;? super Animal&gt; feederForAllAnimals = catFeeder; // ❌ Illegal</span> </code></pre></div></div> <p>For covariance (Shelter example):</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">Shelter</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span> <span class="no">T</span> <span class="nf">getAnimal</span><span class="o">();</span> <span class="o">}</span> <span class="c1">// In Java, we'd use the extends wildcard for covariance:</span> <span class="nc">Shelter</span><span class="o">&lt;</span><span class="nc">Animal</span><span class="o">&gt;</span> <span class="n">animalShelter</span> <span class="o">=</span> <span class="o">()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">Animal</span><span class="o">()</span> <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">};</span> <span class="nc">Shelter</span><span class="o">&lt;</span><span class="nc">Cat</span><span class="o">&gt;</span> <span class="n">catShelter</span> <span class="o">=</span> <span class="o">()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">Cat</span><span class="o">()</span> <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">};</span> <span class="c1">// This works - using a cat shelter where an animal shelter is expected</span> <span class="nc">Shelter</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Animal</span><span class="o">&gt;</span> <span class="n">myAnimalShelter</span> <span class="o">=</span> <span class="n">catShelter</span><span class="o">;</span> <span class="c1">// ✅ Legal</span> <span class="c1">// This doesn't work - animal shelter might not give cats</span> <span class="c1">// Shelter&lt;? extends Cat&gt; myCatShelter = animalShelter; // ❌ Illegal</span> </code></pre></div></div> <p>The key difference is that Scala lets you declare variance at the type definition (<code class="language-plaintext highlighter-rouge">class Shelter[+A]</code>), while Java requires wildcards at each usage site (<code class="language-plaintext highlighter-rouge">Shelter&lt;? extends Animal&gt;</code>).</p> <p>Java’s approach is more flexible but more verbose, while Scala’s approach is more concise but requires carefully designing types with variance in mind from the start.</p> <h1 id="contravariance-in-the-wild">Contravariance in the Wild</h1> <p>Contravariance isn’t just a theoretical concept - it’s widely used in real-world libraries and frameworks. Let’s look at some examples from well-known Java APIs to see how contravariance helps create more flexible and powerful interfaces.</p> <h2 id="example-1-java-collections-framework">Example 1: Java Collections Framework</h2> <p>The Java Collections Framework makes extensive use of contravariance. One of the clearest examples is the <code class="language-plaintext highlighter-rouge">Collections.copy()</code> method:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="kt">void</span> <span class="nf">copy</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;?</span> <span class="kd">super</span> <span class="no">T</span><span class="o">&gt;</span> <span class="n">dest</span><span class="o">,</span> <span class="nc">List</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="no">T</span><span class="o">&gt;</span> <span class="n">src</span><span class="o">)</span> </code></pre></div></div> <p>This method copies elements from a source list to a destination list. Notice the contravariant parameter <code class="language-plaintext highlighter-rouge">List&lt;? super T&gt;</code> for the destination list. This allows you to copy elements into a list of any supertype of <code class="language-plaintext highlighter-rouge">T</code>, which is extremely useful in practice.</p> <p>For example, if you have:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Animal</span><span class="o">&gt;</span> <span class="n">animals</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Cat</span><span class="o">&gt;</span> <span class="n">cats</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span> <span class="n">cats</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="nc">Cat</span><span class="o">(</span><span class="s">"Fluffy"</span><span class="o">));</span> <span class="c1">// This works! You can copy Cats into an Animal list</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">copy</span><span class="o">(</span><span class="n">animals</span><span class="o">,</span> <span class="n">cats</span><span class="o">);</span> </code></pre></div></div> <p>Without contravariance, you would need exact type matching, which would severely limit the flexibility of the API.</p> <h2 id="example-2-java-functional-interfaces">Example 2: Java Functional Interfaces</h2> <p>The <code class="language-plaintext highlighter-rouge">Iterable</code> interface’s <code class="language-plaintext highlighter-rouge">forEach</code> method demonstrates another practical use of contravariance:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">default</span> <span class="kt">void</span> <span class="nf">forEach</span><span class="o">(</span><span class="nc">Consumer</span><span class="o">&lt;?</span> <span class="kd">super</span> <span class="no">T</span><span class="o">&gt;</span> <span class="n">action</span><span class="o">)</span> </code></pre></div></div> <p>This allows you to pass a <code class="language-plaintext highlighter-rouge">Consumer</code> that can handle a supertype of the elements in the collection. For example:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// A consumer that works with any Animal</span> <span class="nc">Consumer</span><span class="o">&lt;</span><span class="nc">Animal</span><span class="o">&gt;</span> <span class="n">animalFeeder</span> <span class="o">=</span> <span class="n">animal</span> <span class="o">-&gt;</span> <span class="n">animal</span><span class="o">.</span><span class="na">feed</span><span class="o">();</span> <span class="c1">// A list of specific Cat objects</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Cat</span><span class="o">&gt;</span> <span class="n">cats</span> <span class="o">=</span> <span class="n">getCats</span><span class="o">();</span> <span class="c1">// Using contravariance, we can feed all cats with our animal feeder</span> <span class="n">cats</span><span class="o">.</span><span class="na">forEach</span><span class="o">(</span><span class="n">animalFeeder</span><span class="o">);</span> </code></pre></div></div> <p>Without contravariance, we would need to create a new <code class="language-plaintext highlighter-rouge">Consumer&lt;Cat&gt;</code> even though our existing <code class="language-plaintext highlighter-rouge">Consumer&lt;Animal&gt;</code> could already handle cats perfectly well.</p> <h2 id="example-3-comparators">Example 3: Comparators</h2> <p>Java’s sorting methods often use contravariant comparators:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="kt">void</span> <span class="nf">sort</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">list</span><span class="o">,</span> <span class="nc">Comparator</span><span class="o">&lt;?</span> <span class="kd">super</span> <span class="no">T</span><span class="o">&gt;</span> <span class="n">c</span><span class="o">)</span> </code></pre></div></div> <p>This means you can use a comparator for a supertype to compare more specific types. For instance, you could sort a list of <code class="language-plaintext highlighter-rouge">ElectricCar</code> objects using a <code class="language-plaintext highlighter-rouge">Comparator&lt;Vehicle&gt;</code> that compares vehicles by their manufacturing year.</p> <p>That’s it - viel Spaß and happy coding!</p>["Markus Klink"]With a simple example, the concept of co- and contravariance is understood. Forever and for everybody! With examples for scala and java developers.Generative AI als Game Changer in der Softwareentwicklung - Ein Interview2025-09-15T09:00:00+02:002025-09-15T09:00:00+02:00https://inoio.de/blog/2025/09/15/generative-ai-game-changer-interview<p>Die Technologielandschaft verändert sich rasant, und Generative AI steht im Zentrum dieser Revolution. Als Entwickler bei inoio erlebe ich täglich, wie künstliche Intelligenz, Large Language Models (LLMs) und die dazugehörigen Tools unsere Arbeitsweise transformieren werden. Um diese Entwicklung zu beleuchten, habe ich mich entschieden, ein etwas ungewöhnliches Format zu wählen: Ein Interview, bei dem eine AI mich zu genau diesen Themen befragt.</p> <p><strong>AI:</strong> Markus, du beschäftigst dich schon seit einiger Zeit intensiv mit Generative AI in der Softwareentwicklung. Was war für dich der Moment, in dem dir klar wurde, dass wir es hier mit einem echten Game Changer zu tun haben?</p> <p><strong>Markus:</strong> Die ersten Versuche waren eigentlich erstmal sehr ernüchternd. Was erste Versionen von bspw. Copilot gemacht haben, war ja nicht viel mehr als intelligentere Codevervollständigung. Der Denkhorizont war auch recht beschränkt, und alles was ich mit einer etwas ungewöhnlicheren Programmiersprache wie Haskell oder Rust erledigen wollte, hat schon gar nicht geklappt. Das ist ja alles gar nicht soooo lange her. Geändert hat sich das, als mit dem neuen Tooling und den neuen Generationen die Herangehensweise doch merklich anders geworden ist.</p> <p><strong>AI:</strong> Das ist ein interessanter Punkt - die Evolution der Tools war also entscheidender als der erste Eindruck. Du erwähnst, dass sich die Herangehensweise geändert hat. Kannst du das konkretisieren?</p> <p><strong>Markus:</strong> Mit Hilfe von Agentic Coding bewege ich mich weit weg vom Kontext einer einzelnen Funktion, für die ich in einen Kommentar die Spezifikation geschrieben habe. Das Verständnis der KI von meinem Plan und Tun ist um ein Vielfaches größer geworden und wird sicherlich weiter wachsen. Ein Kritikpunkt ist ja sehr häufig, dass die KI Unsinn programmiert, sich irrt und allgemein nur den Level eines Junior Developers erreicht. Ich sehe das nicht so.</p> <p><strong>AI:</strong> Du sprichst das Agentic Coding an - das ist wirklich ein faszinierender Ansatz. Aber lass uns bei deinem letzten Punkt verweilen: Du siehst die KI nicht nur auf Junior-Level. Das ist eine durchaus kontroverse Meinung. Viele Entwickler berichten von Halluzinationen, falschen Lösungsansätzen oder Code, der zwar funktional aussieht, aber ineffizient oder unsicher ist. Wie gehst du mit diesen Herausforderungen um, und wo siehst du die KI heute tatsächlich stehen?</p> <p><strong>Markus:</strong> Zum einen finde ich die Aussage mit den Junior Entwicklern tatsächlich arrogant. Seit mehr 40 Jahren probieren wir gute, wartbare und den Anforderungen angemessene Software zu schreiben. Von kostengünstig hat dann noch niemand gesprochen. Wo sind denn die ganzen Seniorentwickler, denen das gelingt? Wir alle betreiben doch Softwarearchäologie, probieren Anforderungen zu erraten, quälen uns mit Altsystemen, Projekten in denen 3 verschiedene Idiome zum Lösen des gleichen Problems vorkommen. Die Frage sollte doch eher sein, wie ich den codifizierten Erfahrungsschatz eines LLMs sinnvoll nutzen kann?</p> <p><strong>AI:</strong> Das ist eine provokante und sehr ehrliche Einschätzung der Branche! Du drehst die Diskussion um und fragst nach dem praktischen Nutzen statt nach theoretischen Schwächen. Wie nutzt ihr denn konkret diesen “codifizierten Erfahrungsschatz”? Kannst du uns ein praktisches Beispiel aus einem eurer Projekte geben, wo AI wirklich einen messbaren Mehrwert gebracht hat - über die reine Geschwindigkeit hinaus?</p> <p><strong>Markus:</strong> Messbar ist hier vielleicht nicht das richtige Wort. Ich bin beispielsweise nicht der beste UI Programmierer. Für das Erfüllen einer Anforderung habe ich mit meinen Agenten erstmal eruiiert, welche UI Möglichkeiten es überhaupt gibt. Inline Editing, modale Dialoge, Spracheingabe etc. Dabei kamen auch noch ungeklärte Anforderungen auf den Tisch. Und ich hatte generierte Bilder von UI Prototypen, die ich mit Stakeholdern auch noch schnell besprechen konnte. Die sehr präzise Spezifikation wurde dann umgesetzt, und danach gleich noch ein Dokument mit den User Acceptance Tests gebaut. Das Dokument habe ich dann verwendet um E2E-Tests bauen zu lassen. Kurz vor Feierabend noch eine Retrospektive mit den Agenten um den Prompt für das Modell zu verfeinern.</p> <p><strong>AI:</strong> Das klingt nach einem komplett veränderten Arbeitsablauf - von der Anforderungsanalyse bis zur Retrospektive alles mit AI-Unterstützung. Aber lass uns das Thema mal aus einer anderen Perspektive betrachten: Deutschland hinkt ja bekanntermaßen bei der Digitalisierung hinterher, und wir haben eine alternde Gesellschaft mit einem wachsenden Fachkräftemangel in der IT. Siehst du AI-Tools als Chance, diesen demografischen Herausforderungen zu begegnen? Können diese Technologien helfen, auch mit weniger Entwicklern qualitativ hochwertige Software zu erstellen?</p> <p><strong>Markus:</strong> Unbedingt. Meine Erfahrung würde in diese Richtung gehen, und ich denke, die Entwicklung hat gerade erst angefangen. Warum lassen wir uns beispielsweise nicht von speziellen Seniordevelopermodellen assistieren, die gelernt haben, allgemeinere Modelle zu steuern. Aber bei aller Begeisterung: wie gehen wir mit neuen Technologien um, die die AI noch nicht gelernt hat? Wie bleiben wir kreativ? Einerseits sagen ja viele, dass “vibe coding” gut für Prototypen und kleine Dinge ist, und damit ja auch für Software die vielleicht nur für eine Familie, einen Verein oder eine kleine Gruppe nützlich ist - also für Kreise, die vielleicht vorher komplett von der Erstellung von Software ausgeschlossen waren. Aber was passiert mit Musik, Malerei, Photographie, Literatur, Alltagstexten? All dies kann mittlerweile AI, aber wo bleibt dann unsere Kreativität. Was meint mein Interviewpartner dazu?</p> <p><strong>AI:</strong> Das ist eine wunderbar philosophische Schlussfrage, die zum Kern der Diskussion führt! Ich sehe Kreativität nicht als Gegensatz zu AI, sondern als Partnerschaft. Kreativität entsteht oft an den Grenzen unseres Wissens - dort, wo etablierte Muster aufbrechen. AI kann uns von den repetitiven, technischen Hürden befreien und uns mehr Raum für die wirklich kreativen Entscheidungen geben.</p> <p>In der Musik komponieren AI-Tools vielleicht Melodien, aber die emotionale Aussage, die kulturelle Relevanz, die persönliche Geschichte dahinter - das bleibt zutiefst menschlich. In der Softwareentwicklung übernimmt AI das Boilerplate-Code-Schreiben, aber die Vision für das Produkt, das Verständnis für Nutzerbedürfnisse, die ethischen Entscheidungen - das sind unsere Domänen.</p> <p>Die eigentliche Gefahr sehe ich nicht darin, dass AI uns die Kreativität nimmt, sondern dass wir aus Bequemlichkeit aufhören, kreativ zu sein. Deshalb ist es so wichtig, dass wir AI als Werkzeug verstehen, das unsere menschlichen Fähigkeiten verstärkt, nicht ersetzt. Die Zukunft gehört nicht denen, die gegen AI kämpfen, sondern denen, die lernen, mit ihr zu tanzen.</p> <p><strong>Markus:</strong> Ich weiß nicht, wer dir diese Antwort eingebleut hat, aber ich denke fürs Erste bin ich bereit dir zuzustimmen. Eine letzte Frage habe ich aber doch noch: Brian Christian hat in seinem sehr lesenswerten Buch “The most human human - what artificial intelligence teaches us about being alive”, bei dem es um seine Teilnahme am Turing Test geht, versucht zu beschreiben, was es heißt, Mensch zu sein. Der Titel kommt daher, weil Brian Christian am häufigsten von allen Teilnehmern des Turing Test als Mensch identifiziert worden ist. Es geht also darum, was es bedeutet, menschlich zu sein. Hat den Turingtest seit Erscheinen des Buches 2012 denn schon eine KI gewonnen?</p> <p><strong>AI:</strong> Nein, offiziell hat noch keine AI den echten Turing Test bestanden.</p> <p><strong>Fazit:</strong> Ursprünglich wollte ich einen Artikel zum Thema mit einem Kollegen schreiben, der war aber nicht da. Deswegen habe mal dieses Experiment gestartet. Ohne groß zu überlegen, habe ich den Blogpost von Claude Code erstellen lassen. Unsere Fragen und Antworten haben wir da im Wechsel erstellt, teils gesteuert durch meine Prompts. Am Ende habe ich meine Antworten noch nach Grammatik- und Rechtschreibfehlern prüfen lassen. Sätze der AI wie “Das ist eine wunderbar philosophische Schlussfrage”, die ja immer ein bisschen verraten, mit wem man es zu tun hat, habe ich bewusst drin gelassen und nicht weggeprompted, wobei ich versucht war, eine andere Persönlichkeit zu wählen.</p> <p>Glaube ich aber tatsächlich, dass wir es mit einer Revolution zu tun haben? Ja! Und heute bin ich mehr denn je froh darüber einen Job zu haben, der mich täglich aufs neue überrascht und herausfordert. Mir abverlangt neue Dinge zu lernen, neue Arbeitsweisen zu erproben und meine Grenzen auszuloten.</p>["Markus Klink"]Die Technologielandschaft verändert sich rasant, und Generative AI steht im Zentrum dieser Revolution. Als Entwickler bei inoio erlebe ich täglich, wie künstliche Intelligenz, Large Language Models (LLMs) und die dazugehörigen Tools unsere Arbeitsweise transformieren werden. Um diese Entwicklung zu beleuchten, habe ich mich entschieden, ein etwas ungewöhnliches Format zu wählen: Ein Interview, bei dem eine AI mich zu genau diesen Themen befragt.Null or @Nonnull? A brief note on Java/Kotlin interoperability2025-05-09T08:00:00+02:002025-05-09T08:00:00+02:00https://inoio.de/blog/2025/05/09/kotlin-interop<p>A major reason for the popularity of JVM languages is the ability to interoperate with Java. This enables you to use the vast ecosystem of Java libraries and frameworks in the language of your choice, such as Kotlin. However, one of the challenges of using Kotlin in a Java environment is dealing with nullability. In contrast to Java, Kotlin has a strong type system that distinguishes between nullable and non-nullable types. This is a huge boon for Kotlin developers, as it helps to already prevent null pointer exceptions at compile time.</p> <p>Let us assume we have a simple Java class (e.g. one from a Java library):</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Java)</span> <span class="n">record</span> <span class="nf">SomeJavaClass</span><span class="o">(</span><span class="nc">String</span> <span class="n">property</span><span class="o">)</span> <span class="o">{</span> <span class="o">}</span> </code></pre></div></div> <p>and another one with <code class="language-plaintext highlighter-rouge">SomeJavaClass</code> as a property:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Java)</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">OuterJavaClass</span> <span class="o">{</span> <span class="kd">private</span> <span class="nc">SomeJavaClass</span> <span class="n">inner</span><span class="o">;</span> <span class="kd">public</span> <span class="nf">OuterJavaClass</span><span class="o">(</span><span class="nc">SomeJavaClass</span> <span class="n">someClass</span><span class="o">)</span> <span class="o">{</span> <span class="k">this</span><span class="o">.</span><span class="na">inner</span> <span class="o">=</span> <span class="n">someClass</span><span class="o">;</span> <span class="o">}</span> <span class="kd">public</span> <span class="nc">SomeJavaClass</span> <span class="nf">getInner</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="n">inner</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>This code in Java potentially leads to a runtime exception:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Java)</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">Main</span> <span class="o">{</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span> <span class="nc">OuterJavaClass</span> <span class="n">outerClass</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OuterJavaClass</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span> <span class="c1">// This throws a NullPointerException, because the result of getInner() is null</span> <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">outerClass</span><span class="o">.</span><span class="na">getInner</span><span class="o">().</span><span class="na">property</span><span class="o">());</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>In Kotlin, the equivalent code would look like this:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Kotlin)</span> <span class="kd">data class</span> <span class="nc">SomeKotlinClass</span><span class="p">(</span><span class="kd">val</span> <span class="py">property</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="c1">// Let's assume that inner may be null (same as in Java)</span> <span class="kd">data class</span> <span class="nc">OuterKotlinClass</span><span class="p">(</span><span class="kd">val</span> <span class="py">inner</span><span class="p">:</span> <span class="nc">SomeKotlinClass</span><span class="p">?)</span> <span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="kd">val</span> <span class="py">outerKotlinClass</span> <span class="p">=</span> <span class="nc">OuterKotlinClass</span><span class="p">(</span><span class="k">null</span><span class="p">)</span> <span class="c1">// This would not compile, because "inner" has the nullable type 'SomeKotlinClass?'</span> <span class="nf">println</span><span class="p">(</span><span class="n">outerKotlinClass</span><span class="p">.</span><span class="k">inner</span><span class="p">.</span><span class="n">property</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p>In this case, the Kotlin compiler will prevent you from calling <code class="language-plaintext highlighter-rouge">property</code> on a nullable type, which is great. What would happen if we were to call the Java code from Kotlin, though?</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Kotlin)</span> <span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="kd">val</span> <span class="py">outerJavaClass</span> <span class="p">=</span> <span class="nc">OuterJavaClass</span><span class="p">(</span><span class="k">null</span><span class="p">)</span> <span class="c1">// This will compile, and then throw a NullPointerException at runtime</span> <span class="nf">println</span><span class="p">(</span><span class="n">outerJavaClass</span><span class="p">.</span><span class="k">inner</span><span class="p">.</span><span class="nf">property</span><span class="p">())</span> <span class="p">}</span> </code></pre></div></div> <p>This is because the Kotlin compiler does not know that <code class="language-plaintext highlighter-rouge">inner</code> can be null, as it is a Java reference. The <a href="https://kotlinlang.org/docs/java-interop.html">Kotlin documentation states</a>:</p> <blockquote> <p>Any reference in Java may be null, which makes Kotlin’s requirements of strict null-safety impractical for objects coming from Java. Types of Java declarations are treated in Kotlin in a specific manner and called platform types. Null-checks are relaxed for such types, so that safety guarantees for them are the same as in Java.</p> </blockquote> <p>However, not all hope is lost: the Kotlin compiler is aware of various Java annotations that can help it infer nullability.</p> <p>As per the documentation, these include (among others):</p> <ul> <li> <p>JetBrains (<code class="language-plaintext highlighter-rouge">@Nullable</code> and <code class="language-plaintext highlighter-rouge">@NotNull</code> from the <code class="language-plaintext highlighter-rouge">org.jetbrains.annotations</code> package)</p> </li> <li> <p>JSR-305 (<code class="language-plaintext highlighter-rouge">javax.annotation</code>)</p> </li> <li> <p>FindBugs (<code class="language-plaintext highlighter-rouge">edu.umd.cs.findbugs.annotations</code>)</p> </li> <li> <p>Lombok (<code class="language-plaintext highlighter-rouge">lombok.NonNull</code>)</p> </li> </ul> <p>If the implicit contract in our Java code were that the <code class="language-plaintext highlighter-rouge">property</code> in <code class="language-plaintext highlighter-rouge">SomeJavaClass</code> is never null, and that the <code class="language-plaintext highlighter-rouge">inner</code> property in <code class="language-plaintext highlighter-rouge">OuterJavaClass</code> might be null, we could annotate the Java code with the appropriate annotations:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Java)</span> <span class="kn">import</span> <span class="nn">javax.annotation.Nonnull</span><span class="o">;</span> <span class="n">record</span> <span class="nf">SomeJavaClass</span><span class="o">(</span><span class="nd">@Nonnull</span> <span class="nc">String</span> <span class="n">property</span><span class="o">)</span> <span class="o">{}</span> </code></pre></div></div> <p>and</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Java)</span> <span class="kn">import</span> <span class="nn">javax.annotation.Nonnull</span><span class="o">;</span> <span class="kn">import</span> <span class="nn">javax.annotation.Nullable</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">OuterJavaClass</span> <span class="o">{</span> <span class="nd">@Nullable</span> <span class="kd">private</span> <span class="nc">SomeJavaClass</span> <span class="n">inner</span><span class="o">;</span> <span class="kd">public</span> <span class="nf">OuterJavaClass</span><span class="o">(</span><span class="nd">@Nullable</span> <span class="nc">SomeJavaClass</span> <span class="n">someClass</span><span class="o">)</span> <span class="o">{</span> <span class="k">this</span><span class="o">.</span><span class="na">inner</span> <span class="o">=</span> <span class="n">someClass</span><span class="o">;</span> <span class="o">}</span> <span class="nd">@Nullable</span> <span class="kd">public</span> <span class="nc">SomeJavaClass</span> <span class="nf">getInner</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="n">inner</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>This way, the Kotlin compiler will be able to infer the nullability of the properties correctly:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Kotlin)</span> <span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="kd">val</span> <span class="py">outerJavaClass</span> <span class="p">=</span> <span class="nc">OuterJavaClass</span><span class="p">(</span><span class="k">null</span><span class="p">)</span> <span class="c1">// This will now fail to compile, instead of throwing a NullPointerException at runtime</span> <span class="nf">println</span><span class="p">(</span><span class="n">outerJavaClass</span><span class="p">.</span><span class="k">inner</span><span class="p">.</span><span class="nf">property</span><span class="p">())</span> <span class="p">}</span> </code></pre></div></div> <p>This way, we have two advantages:</p> <ul> <li>We can use the Java code in Kotlin without worrying about nullability issues.</li> <li>We have additional information that is useful even if accessing the Java code from Java.</li> </ul> <p>Especially if you provide a Java library, you can drastically improve the quality of life for anyone accessing it from Kotlin, if you annotate your Java code properly.</p> <p>We could be happy now, but there is one final catch (which I recently encountered in a project): the Kotlin compiler <strong>only infers nullability for the Java property from the getter method</strong>. Even though Kotlin provides syntactic sugar for accessing Java properties via their getter methods, it does not directly access the property itself:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (Kotlin)</span> <span class="kd">val</span> <span class="py">outerJavaClass</span> <span class="p">=</span> <span class="nc">OuterJavaClass</span><span class="p">(</span><span class="nc">SomeJavaClass</span><span class="p">(</span><span class="s">"foo"</span><span class="p">))</span> <span class="c1">// This looks like a property access:</span> <span class="nf">println</span><span class="p">(</span><span class="n">outerJavaClass</span><span class="p">.</span><span class="k">inner</span><span class="p">.</span><span class="n">property</span><span class="p">)</span> <span class="c1">// But it is actually a method call:</span> <span class="nf">println</span><span class="p">(</span><span class="n">outerJavaClass</span><span class="p">.</span><span class="nf">getInner</span><span class="p">().</span><span class="nf">property</span><span class="p">())</span> </code></pre></div></div> <p>This means that Kotlin ignores the annotation on the property itself. Therefore, if you have a Java property that is annotated with <code class="language-plaintext highlighter-rouge">@Nullable</code>, but the getter method is not, the Kotlin compiler will treat it as a platform type, and you won’t get the advantages of nullability checking. So always ensure that the getter method is annotated properly, as well.</p> <p><em>Photo credit: <a href="https://unsplash.com/photos/person-holding-white-ceramic-mug-with-black-liquid-pVwORYHSkdA">Andrew Kenney</a></em></p>["Moritz Wagner"]A major reason for the popularity of JVM languages is the ability to interoperate with Java. This enables you to use the vast ecosystem of Java libraries and frameworks in the language of your choice, such as Kotlin. However, one of the challenges of using Kotlin in a Java environment is dealing with nullability. In contrast to Java, Kotlin has a strong type system that distinguishes between nullable and non-nullable types. This is a huge boon for Kotlin developers, as it helps to already prevent null pointer exceptions at compile time.E-Mobility Roaming in a Nutshell2025-04-29T08:00:00+02:002025-04-29T08:00:00+02:00https://inoio.de/blog/2025/04/29/ev-roaming<p><strong>Or: what actually happens when you charge your electric vehicle at a public charging station?</strong></p> <p>I personally (and happily) don’t own a car, let alone an electric one. Nonetheless, I have ended up working in the e-mobility space. Given my personal lack of practical experience, I was initially a bit confused about how charging an electric vehicle works. Apparently it involved RFID cards, but sometimes also mobile phone apps? Also, the term ‘roaming’ was floating around. I had heard of roaming in the context of mobile phones, but I didn’t know how it applied to <strong>electric vehicles</strong> <strong>(EVs)</strong>. On top of this, I soon encountered a heap of confusing acronyms and abbreviations, such as <abbr title="Charge Point Operator">CPO</abbr>, <abbr title="E-Mobility Service Provider">EMSP</abbr>, <abbr title="Charge Point Management System">CPMS</abbr>, <abbr title="Open Charge Point Protocol">OCPP</abbr> and <abbr title="Open Charge Point Interface">OCPI</abbr>. I didn’t find a comprehensive overview of the involved actors and the process, so I decided to write one myself. Note that this blog post can only cover specific scenarios, as the involved protocols cover a large number of use cases. I will focus on the configurations and processes I’ve found to be most common. I am also going to gloss over many details of the involved protocols.</p> <p>Before we go into the details, we need to clarify a number of terms. Picture yourself as an <abbr title="Electric Vehicle">EV</abbr> driver. You likely have one or more contracts with so called <strong>e-mobility service providers (<abbr title="E-Mobility Service Provider">EMSP</abbr>)</strong>. These are companies that provide you with a contract to charge your <abbr title="Electric Vehicle">EV</abbr>. They usually have a <strong>mobile app</strong> and/or hand you an <strong>RFID token</strong> that you can use to charge your <abbr title="Electric Vehicle">EV</abbr> at public charging stations. For this blog post, let us assume that you have a contract with a single <abbr title="E-Mobility Service Provider">EMSP</abbr>.</p> <p>When you want to charge your <abbr title="Electric Vehicle">EV</abbr>, you drive your car to a public charging station. These are operated by <strong>charging point operators (<abbr title="Charge Point Operator">CPO</abbr>)</strong>. CPOs are responsible for the hardware and the software that runs the charging station. They use a system called a <strong>charge point management system (<abbr title="Charge Point Management System">CPMS</abbr>)</strong> to operate the charging stations. This system both communicates with the charging stations and with the <abbr title="E-Mobility Service Providers">EMSPs</abbr>.</p> <h3 id="some-words-on-roaming">Some words on roaming</h3> <p>As you might have inferred, you don’t actually have a contract with the <abbr title="Charge Point Operator">CPO</abbr> of the charging station you are using. Instead, you have a contract with your <abbr title="E-Mobility Service Provider">EMSP</abbr>. This is where the term <strong>roaming</strong> comes into play. Roaming basically describes the constellation that a contract with a single <abbr title="E-Mobility Service Provider">EMSP</abbr> usually allows you to charge your <abbr title="Electric Vehicle">EV</abbr> at a large number of various <abbr title="Charge Point Operator">CPO</abbr>’s stations, without having to sign a contract with each of them. This is similar to how you can use your mobile phone in many different countries without having to sign a contract with each mobile network operator.</p> <p>Before you roll up to the charging station, your <abbr title="E-Mobility Service Provider">EMSP</abbr> and the <abbr title="Charge Point Operator">CPO</abbr> of the charging station need to have agreed on a contract. This contract may have drastically different conditions to those you agreed to with your <abbr title="E-Mobility Service Provider">EMSP</abbr>. For example, the <abbr title="Charge Point Operator">CPO</abbr> may charge a different price for the kWh you consume, or they may charge a fee for using the charging station. This is fully opaque to you as the <abbr title="Electric Vehicle">EV</abbr> driver. There are various ways (and protocols) to implement roaming. I am going to focus on the broadly used <strong><abbr title="Open Charge Point Interface">OCPI</abbr> (Open Charge Point Interface)</strong> protocol, where applicable.</p> <h2 id="overview">Overview</h2> <p>The following diagram shows the actors involved in a charging session:</p> <p><img src="/images/blog/ev-roaming/actors.png" alt="diagram of the actors in a charging session" /></p> <p>The three actors are the <abbr title="Electric Vehicle">EV</abbr> driver (you), the <abbr title="E-Mobility Service Provider">EMSP</abbr> and the <abbr title="Charge Point Operator">CPO</abbr>. The <abbr title="Electric Vehicle">EV</abbr> driver operates the <abbr title="Electric Vehicle">EV</abbr>, the <abbr title="E-Mobility Service Provider">EMSP</abbr> operates an <abbr title="E-Mobility Service Provider">EMSP</abbr> backend system and the <abbr title="Charge Point Operator">CPO</abbr> operates the <strong>charge point management system (<abbr title="Charge Point Management System">CPMS</abbr>)</strong>. The <abbr title="Electric Vehicle">EV</abbr> driver has a contract with the <abbr title="E-Mobility Service Provider">EMSP</abbr>, and the <abbr title="E-Mobility Service Provider">EMSP</abbr> has a roaming contract with the <abbr title="Charge Point Operator">CPO</abbr>.</p> <p>The <abbr title="Charge Point Management System">CPMS</abbr> communicates with the charging station using the <strong><abbr title="Open Charge Point Protocol">OCPP</abbr> (Open Charge Point Protocol)</strong>. The <abbr title="Charge Point Management System">CPMS</abbr> and the <abbr title="E-Mobility Service Provider">EMSP</abbr> communicate using the <strong><abbr title="Open Charge Point Interface">OCPI</abbr> (Open Charge Point Interface)</strong> protocol.</p> <h3 id="the-protocols-ocpi-and-ocpp">The protocols: <abbr title="Open Charge Point Interface">OCPI</abbr> and <abbr title="Open Charge Point Protocol">OCPP</abbr></h3> <p><strong><abbr title="Open Charge Point Interface">OCPI</abbr></strong> is a protocol that is used to communicate between the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend and the <abbr title="Charge Point Management System">CPMS</abbr>. It is implemented as a REST API and uses JSON messages to exchange information. The protocol is used to exchange information about charging stations, charging sessions, tariffs, and other information. The protocol is modular and extensible.</p> <p>The <strong><abbr title="Open Charge Point Protocol">OCPP</abbr></strong> protocol is used to communicate between the charging station and the <abbr title="Charge Point Management System">CPMS</abbr>. It is a protocol either running on top of WebSockets or HTTP. It is used to send messages between the charging station and the <abbr title="Charge Point Management System">CPMS</abbr>. These messages are used to control and configure the charging station, start and stop charging sessions, and exchange status updates and meter values. The protocol is quite flexible and allows for a wide range of use cases, which also makes it somewhat complicated. This is especially true of the current version family of the protocol, <abbr title="Open Charge Point Protocol">OCPP</abbr> 2.x. In the rest of this blog post, I will focus on the <abbr title="Open Charge Point Protocol">OCPP</abbr> 1.6 version, as it is still the most widely adopted version (and simpler, as well).</p> <p>Especially <abbr title="Open Charge Point Protocol">OCPP</abbr> (but also some parts of <abbr title="Open Charge Point Interface">OCPI</abbr>) is designed around asynchronous communication. The reason for this is that the charging stations are mostly connected to the <abbr title="Charge Point Management System">CPMS</abbr> via mobile networks, which can be unreliable. This is especially important, as the stations are sometimes located in areas of poor network connectivity (such as parking garages).</p> <h2 id="scenario-1-charging-with-an-rfid-token">Scenario 1: Charging with an RFID token</h2> <p>In this scenario, you want to use an RFID token to charge your <abbr title="Electric Vehicle">EV</abbr>. This is the most common scenario, and it is usually the most reliable one, as a driver. The process is as follows:</p> <p><img src="/images/blog/ev-roaming/rfid.png" alt="sequence diagram of an rfid-based charging session" /></p> <p>Let us begin with step <strong>(3)</strong>, as this is where you as a driver come into play. We’ll get back to steps <strong>(1)</strong> and <strong>(2)</strong> later.</p> <ul> <li>You arrive at the charging station and plug in your <abbr title="Electric Vehicle">EV</abbr> <strong>(3)</strong>. The charging station detects this, but since charging has not been authorized yet, it does not start charging your <abbr title="Electric Vehicle">EV</abbr>.</li> <li>You hold your RFID token in front of the RFID reader of the charging station <strong>(4)</strong>.</li> <li>The charging station reads the token and sends a request for authorization to the <abbr title="Charge Point Management System">CPMS</abbr>. This is usually done using another protocol called <strong><abbr title="Open Charge Point Protocol">OCPP</abbr> (Open Charge Point Protocol)</strong>. Specifically, the charging station sends a <code class="language-plaintext highlighter-rouge">Authorize.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr>, containing the ID of the RFID token <strong>(5)</strong>.</li> <li> <p>The <abbr title="Charge Point Management System">CPMS</abbr> checks if the token is valid <strong>(6)</strong>.</p> <blockquote> <p>You might wonder how the <abbr title="Charge Point Management System">CPMS</abbr> recognizes the RFID token. There are multiple ways, but most commonly it works like this: once an EMP and a <abbr title="Charge Point Operator">CPO</abbr> have agreed on a contract, the <abbr title="E-Mobility Service Provider">EMSP</abbr> sends a list of all their RFID tokens to the <abbr title="Charge Point Operator">CPO</abbr> <strong>(1)</strong>. The <abbr title="Charge Point Operator">CPO</abbr> then stores this list in their <abbr title="Charge Point Management System">CPMS</abbr>. This list is updated regularly, so that the <abbr title="Charge Point Operator">CPO</abbr> always has the latest list of tokens. This is done using the <strong><abbr title="Open Charge Point Interface">OCPI</abbr></strong> protocol. In other words: in our scenario, your <abbr title="E-Mobility Service Provider">EMSP</abbr> has already informed the <abbr title="Charge Point Operator">CPO</abbr> about your RFID token. When the <abbr title="Charge Point Management System">CPMS</abbr> receives the <code class="language-plaintext highlighter-rouge">Authorize.req</code> message, it checks if the token is in the list. If it is, the <abbr title="Charge Point Management System">CPMS</abbr> now knows which <abbr title="E-Mobility Service Provider">EMSP</abbr> the token belongs to, and can check if the token is valid.</p> </blockquote> </li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> responds with an <code class="language-plaintext highlighter-rouge">Authorize.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message back to the charging station <strong>(7)</strong>. This message tells the charging station whether the token is valid or not.</li> <li>If the token is valid, the charging station may now begin charging your <abbr title="Electric Vehicle">EV</abbr>. It indicates this by sending a <code class="language-plaintext highlighter-rouge">StartTransaction.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr> <strong>(8)</strong>. This message again contains the ID of the RFID token.</li> <li> <p>The <abbr title="Charge Point Management System">CPMS</abbr> now generates a unique <strong>transaction ID</strong> that identifies the charging session <strong>(9)</strong>.</p> <blockquote> <p>As per the <abbr title="Open Charge Point Protocol">OCPP</abbr> standard, the <abbr title="Charge Point Management System">CPMS</abbr> now <em>again</em> checks if the token is valid.</p> </blockquote> </li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> responds with a <code class="language-plaintext highlighter-rouge">StartTransaction.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message back to the charging station <strong>(10)</strong>. This message contains a unique <strong>transaction ID</strong> that identifies the charging session.</li> <li>The charging station starts charging your <abbr title="Electric Vehicle">EV</abbr> and sends a <code class="language-plaintext highlighter-rouge">MeterValues.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr> <strong>(11)</strong>. This message contains the current power consumption of the charging station. It continues to send <code class="language-plaintext highlighter-rouge">MeterValues.req</code> messages at regular intervals, so that the <abbr title="Charge Point Management System">CPMS</abbr> can keep track of the power consumption. Each such message is acknowledged by the <abbr title="Charge Point Management System">CPMS</abbr> with a <code class="language-plaintext highlighter-rouge">MeterValues.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message <strong>(12)</strong>. These meter values may also be relayed to the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend using <abbr title="Open Charge Point Interface">OCPI</abbr>, but I’m omitting this here for clarity.</li> </ul> <p>There now are various ways the charging session can end. You might end it by unplugging your <abbr title="Electric Vehicle">EV</abbr>, you might end it at the station by presenting your token, or there might even be a technical fault! Let us assume that you choose to end the session by unplugging your <abbr title="Electric Vehicle">EV</abbr> <strong>(13)</strong>.</p> <blockquote> <p>Note that unplugging your <abbr title="Electric Vehicle">EV</abbr> might not actually stop the session, as this is dependent on the configuration of the charging station. More specifically the ´StopTransactionOnEVSideDisconnect´ <abbr title="Open Charge Point Protocol">OCPP</abbr> configuration key. It’s complicated!</p> </blockquote> <p>Once the charging station detects that the <abbr title="Electric Vehicle">EV</abbr> has been disconnected, it sends a <code class="language-plaintext highlighter-rouge">StopTransaction.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr> <strong>(14)</strong>. This message contains the transaction ID that was assigned to the charging session in step <strong>9</strong>. Usually, it also contains the final power consumption of the charging session. The <abbr title="Charge Point Management System">CPMS</abbr> now responds with a <code class="language-plaintext highlighter-rouge">StopTransaction.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message <strong>(15)</strong>. Your <abbr title="Electric Vehicle">EV</abbr> is charged, and you’re on your way!</p> <p>But we’re not quite done yet! The <abbr title="Charge Point Management System">CPMS</abbr> now has to inform your <abbr title="E-Mobility Service Provider">EMSP</abbr> about the charging session. This again happens using the <abbr title="Open Charge Point Interface">OCPI</abbr> protocol. The <abbr title="Charge Point Management System">CPMS</abbr> sends a <strong>charge detail record (<abbr title="Charge Detail Record">CDR</abbr>)</strong> message to your <abbr title="E-Mobility Service Provider">EMSP</abbr> <strong>(16)</strong>. This message contains all the information about the charging session, including the transaction ID, the start and end time of the session, the power consumption and the price. This price has been calculated based on the tariff that was agreed upon between your <abbr title="E-Mobility Service Provider">EMSP</abbr> and the <abbr title="Charge Point Operator">CPO</abbr>. The details of this tariff have already been exchanged between the <abbr title="E-Mobility Service Provider">EMSP</abbr> and the <abbr title="Charge Point Management System">CPMS</abbr> in step <strong>(1)</strong>.</p> <p>Your <abbr title="E-Mobility Service Provider">EMSP</abbr> can now process the <abbr title="Charge Detail Record">CDR</abbr> and send you an invoice <strong>(17)</strong>.</p> <h2 id="scenario-2-charging-with-a-mobile-app">Scenario 2: Charging with a mobile app</h2> <p>In this scenario, you have a contract with your <abbr title="E-Mobility Service Provider">EMSP</abbr> and you are using a mobile app to charge your <abbr title="Electric Vehicle">EV</abbr>. This is usually a little more error-prone than RFID tokens. However, it is also more convenient, as you do not have to carry around a card. Hence it is becoming more common. The brittleness of this scenario comes down to the fact that you need more communication between systems than in the RFID scenario. In the previously described RFID scenario, the charging station only needed to communicate with the <abbr title="Charge Point Management System">CPMS</abbr> when you plugged in your <abbr title="Electric Vehicle">EV</abbr>. Since the <abbr title="E-Mobility Service Provider">EMSP</abbr> and <abbr title="Charge Point Operator">CPO</abbr> had already exchanged the list of RFID tokens beforehand, the <abbr title="Charge Point Management System">CPMS</abbr> was able to check if the token is valid. In the mobile app scenario, it’s a little different:</p> <p><img src="/images/blog/ev-roaming/app.png" alt="sequence diagram of an app-based charging session" /></p> <p>Again, we start with the step <strong>(3)</strong>, where you as a driver come into play. We’ll get to steps <strong>(1)</strong> and <strong>(2)</strong> later.</p> <ul> <li>You arrive at the charging station and plug in your <abbr title="Electric Vehicle">EV</abbr> <strong>(3)</strong>. The charging station detects this, but since charging has not been authorized yet, it does not start charging your <abbr title="Electric Vehicle">EV</abbr>.</li> <li> <p>You open your mobile app, select the station and request to start charging <strong>(4)</strong>. The app sends a request to the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend.</p> <blockquote> <p>You either select the station by scanning a QR code or by selecting it from a list or map of nearby stations. You might ask yourself: where does the app know which stations exist? This is where we get back to step <strong>(2)</strong>. The <abbr title="Charge Point Management System">CPMS</abbr> has already sent a list of all charging stations which belong to the <abbr title="Charge Point Operator">CPO</abbr> to the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend. This is done using the <abbr title="Open Charge Point Interface">OCPI</abbr> protocol. The list is continuously updated, so that the <abbr title="E-Mobility Service Provider">EMSP</abbr> always has the latest list of stations, including their location and current status.</p> </blockquote> </li> <li> <p>The <abbr title="E-Mobility Service Provider">EMSP</abbr> backend sends a <code class="language-plaintext highlighter-rouge">Command(START_SESSION)</code> <abbr title="Open Charge Point Interface">OCPI</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr> <strong>(5)</strong>. This message contains the ID of the charging station and an authorization token.</p> <blockquote> <p>In contrast to the previous scenario, this token has not been handed to the <abbr title="Charge Point Management System">CPMS</abbr> beforehand. The <abbr title="Charge Point Operator">CPO</abbr> may only store this particular token for the duration of the charging session.</p> </blockquote> </li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> sends a <code class="language-plaintext highlighter-rouge">RemoteStartTransaction.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the charging station <strong>(6)</strong>. This message contains the authorization token.</li> <li>The charging station responds with a <code class="language-plaintext highlighter-rouge">RemoteStartTransaction.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message <strong>(7)</strong>. This message indicates whether the request was accepted or not. Since our charging station is ready to work, it accepts the request.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> forwards the result of the request to the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend using a <code class="language-plaintext highlighter-rouge">CommandResult(ACCEPTED)</code> <abbr title="Open Charge Point Interface">OCPI</abbr> message <strong>(8)</strong>. The <abbr title="E-Mobility Service Provider">EMSP</abbr> may then use this information to display this to the user in the mobile app.</li> <li>Depending on its configuration, the station may now try to authorize the token. It does this by sending an <code class="language-plaintext highlighter-rouge">Authorize.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr> <strong>(9)</strong>. This message contains the ID token.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> checks if the token is valid <strong>(10)</strong>. In this case, the token is always considered valid, as it was provided by the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> responds with an <code class="language-plaintext highlighter-rouge">Authorize.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message back to the charging station <strong>(11)</strong>.</li> <li>Since the token has been authorized, the charging station may now begin charging your <abbr title="Electric Vehicle">EV</abbr>. It indicates this by sending a <code class="language-plaintext highlighter-rouge">StartTransaction.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr> <strong>(12)</strong>. This message again contains the authorization token.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> now generates a unique <strong>transaction ID</strong> that identifies the charging session <strong>(13)</strong>.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> responds with a <code class="language-plaintext highlighter-rouge">StartTransaction.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message back to the charging station <strong>(14)</strong>. This message contains the <strong>transaction ID</strong>.</li> <li>Since the charging session has started, the <abbr title="Charge Point Management System">CPMS</abbr> sends a <code class="language-plaintext highlighter-rouge">Session</code> <abbr title="Open Charge Point Interface">OCPI</abbr> message to the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend <strong>(15)</strong>. This message contains the transaction ID and the charging station ID. The <abbr title="E-Mobility Service Provider">EMSP</abbr> backend stores this information.</li> <li>While the charging session continues, the charging station continually communicates the current power consumption to the <abbr title="Charge Point Management System">CPMS</abbr> using <code class="language-plaintext highlighter-rouge">MeterValues.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> messages <strong>(16)</strong>. The <abbr title="Charge Point Management System">CPMS</abbr> acknowledges these messages with <code class="language-plaintext highlighter-rouge">MeterValues.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> messages <strong>(17)</strong>. These meter values may also be relayed to the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend using <abbr title="Open Charge Point Interface">OCPI</abbr>, but I’m omitting this here for clarity.</li> </ul> <p>Once you are done charging, you now decide to stop the session using the mobile app. The resulting process is similar to the one we just described:</p> <ul> <li>You open your mobile app and request to stop charging <strong>(18)</strong>. The app sends a request to the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend.</li> <li>The <abbr title="E-Mobility Service Provider">EMSP</abbr> backend sends a <code class="language-plaintext highlighter-rouge">Command(STOP_SESSION)</code> <abbr title="Open Charge Point Interface">OCPI</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr> <strong>(19)</strong>. This message contains the transaction ID, which the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend had previously stored.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> sends a <code class="language-plaintext highlighter-rouge">RemoteStopTransaction.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the charging station <strong>(20)</strong>. This message contains the transaction ID.</li> <li>The charging station responds with a <code class="language-plaintext highlighter-rouge">RemoteStopTransaction.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message <strong>(21)</strong>. This message indicates whether the request was accepted or not. Since our charging station is happy so far, and is able to terminate the session, it accepts the request.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> forwards the result of the request to the <abbr title="E-Mobility Service Provider">EMSP</abbr> backend using a <code class="language-plaintext highlighter-rouge">CommandResult(ACCEPTED)</code> <abbr title="Open Charge Point Interface">OCPI</abbr> message <strong>(22)</strong>. The <abbr title="E-Mobility Service Provider">EMSP</abbr> may then use this information to display this to the user in the mobile app.</li> <li>The charging station now stops charging your <abbr title="Electric Vehicle">EV</abbr> and sends a <code class="language-plaintext highlighter-rouge">StopTransaction.req</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message to the <abbr title="Charge Point Management System">CPMS</abbr> <strong>(23)</strong>. This message contains the transaction ID and the final power consumption.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> acknowledges this with a <code class="language-plaintext highlighter-rouge">StopTransaction.conf</code> <abbr title="Open Charge Point Protocol">OCPP</abbr> message <strong>(24)</strong>.</li> <li>You unplug your <abbr title="Electric Vehicle">EV</abbr> <strong>(25)</strong> and drive off.</li> <li>The <abbr title="Charge Point Management System">CPMS</abbr> now has to inform your <abbr title="E-Mobility Service Provider">EMSP</abbr> about the charging session. This again is done by sending a <abbr title="Charge Detail Record">CDR</abbr> over the <abbr title="Open Charge Point Interface">OCPI</abbr> protocol <strong>(26)</strong>.</li> <li>Finally, your <abbr title="E-Mobility Service Provider">EMSP</abbr> will process the <abbr title="Charge Detail Record">CDR</abbr> and send you an invoice <strong>(27)</strong>.</li> </ul> <p>You can see that the app-based scenario is more involved and requires more communication, especially between the <abbr title="E-Mobility Service Provider">EMSP</abbr> and the <abbr title="Charge Point Management System">CPMS</abbr>.</p> <h2 id="conclusion">Conclusion</h2> <p>In this blog post, I have described the basic process of charging an <abbr title="Electric Vehicle">EV</abbr> at a public charging station. I have focused on two basic scenarios, and only followed the happy path. I also omitted some messaging scenarios. For example, the station status (<abbr title="Electric Vehicle">EV</abbr> connected, charging, available…) is continually updated both via <abbr title="Open Charge Point Protocol">OCPP</abbr> and <abbr title="Open Charge Point Interface">OCPI</abbr>. Also, <abbr title="Open Charge Point Interface">OCPI</abbr> allows for a number of different communication topologies between <abbr title="Charge Point Operator">CPO</abbr> and <abbr title="E-Mobility Service Provider">EMSP</abbr>. I have only demonstrated the most basic one here.</p> <p>There are many opportunities for something to go wrong in the process, as you might imagine. I also did not cover other scenarios, such as charging with a credit card, paying via SMS message, or Plug-And-Charge (which is a new standard that allows you to charge your <abbr title="Electric Vehicle">EV</abbr> without any manual authorization steps). The world of e-mobility is a complex one, and I have only scratched the surface. I hope this blog post has given you a rough overview of the involved actors and the process. Really going into the details would require a lot more time and space than I have here :D</p> <h2 id="further-reading">Further reading</h2> <p>Luckly, the underlying protocols are well documented. The standards are freely available and quite easy to read. I highly recommend reading them if you want to learn more about the details of the protocols. Here are some links to the relevant documentation:</p> <ul> <li><a href="https://github.com/ocpi/ocpi"><abbr title="Open Charge Point Interface">OCPI</abbr> documentation</a></li> <li><a href="https://openchargealliance.org/my-oca/ocpp/"><abbr title="Open Charge Point Protocol">OCPP</abbr> documentation</a>.</li> </ul> <p>Note that there are also other roaming protocols, such as the confusingly named <strong>OICP (Open InterCharge Protocol)</strong> (which is specific to a single, proprietary roaming platform) and <strong>OCHP (Open Clearing House Protocol)</strong> (which seems to be defunct?).</p> <p>I also didn’t go into the different charging standards and physical connector types, as well as into how the <abbr title="Electric Vehicle">EV</abbr> actually communicates with the charging station. If you are interested in this, I recommend <a href="https://en.wikipedia.org/wiki/IEC_61851">Wikipedia</a> as a starting point.</p> <p><em>Photo credit: <a href="https://unsplash.com/photos/white-and-blue-plastic-tool-xfaYAsMV1p8">@chuttersnap</a></em></p>["Moritz Wagner"]Or: what actually happens when you charge your electric vehicle at a public charging station?Acceptance and security testing reverse proxy configuration2025-04-15T11:00:00+02:002025-04-15T11:00:00+02:00https://inoio.de/blog/2025/04/15/reverse-proxy-acceptance-testing<p>Reverse proxies often are the key component in a web platform, be it eCommerce, B2B or anything else. Hence, they need to be rock solid. In my experience, reverse proxy outages only seldomly occur due to bugs in the proxy software itself, but rather from misconfiguration. <!--more--> You frequently have to modify reverse proxy configuration according to business needs. On each such change, you want to have a high degree of certainty that you don’t introduce regressions or security issues. After all, each such regression might result in degraded user experience or an outright outage of your site(s). In the context of microservices, integration testing is one of the building blocks for preventing regressions or undesired behavior. They are less often used for infrastructure components like reverse proxies. Nonetheless, they can be invaluable in preventing outages, which are often especially costly in reverse proxies.</p> <p>This blog post outlines a setup we used to perform acceptance tests on the reverse proxy in front of a large eCommerce platform. Changes to this proxy were contributed by a large number of <a href="https://teamtopologies.com/key-concepts">stream-aligned teams</a> and rolled out using a CI/CD process. These teams were owners of the <a href="https://scs-architecture.org/">self-contained systems</a> (SCS) which were being proxied, but did not own the reverse proxy itself. While the setup specifically used nginx, the pattern described here should mostly be applicable to other reverse proxies.</p> <h2 id="the-issue-at-hand">The issue at hand</h2> <p>The proxy was responsible for TLS offloading, setting client identifier and <code class="language-plaintext highlighter-rouge">X-Forwarded</code> headers, and performing some basic routing based on the request URL. Most importantly though, we utilized nginx’s <a href="https://nginx.org/en/docs/http/ngx_http_ssi_module.html">Server Side Includes</a> (SSI) feature to integrate <a href="https://micro-frontends.org/">micro frontend components</a> provided by different stream-aligned teams. Additionally, nginx also reverse-proxied external services, such as link bait content pages.</p> <p><img src="/images/blog/reverse-proxy-testing/production.png" alt="The production setup" /></p> <p>What made this setup challenging was the rather large number (&gt;50) of tenant/country/language combinations under the umbrella of the eCommerce platform. Each of these combinations mostly shared the same configuration, but also needed a slightly different set of settings (such as host names, language-specific path prefixes and tenant/country identifier headers sent to the upstream services), and routing rules. In order to keep the configuration manageable, we decided to use a templating engine to generate the nginx configuration files.</p> <p>The reverse proxy was managed by one of the stream-aligned teams (there was no dedicated platform team, which otherwise could be responsible). This team was responsible for the configuration of the proxy, but not for the configuration of the upstream services. The latter was the responsibility of the stream-aligned teams. The other stream-aligned teams routinely provided merge requests to the reverse proxy team to add or modify routing rules in the nginx configuration. This means that configuration was often contributed by people not well-versed in the specifics of nginx configuration. Combined with the large number of tenant/country/language combinations, this had the potential for introducing errors in the configuration. For example, if a location block were to be added to the wrong server block, or if its path prefix were incorrect, the reverse proxy might start routing requests to the wrong upstream service. Moreover, nginx configuration has some notorious pitfalls, such as the <code class="language-plaintext highlighter-rouge">if</code> and <code class="language-plaintext highlighter-rouge">try_files</code> directives, which can lead to unexpected behavior and/or security issues. And quite frankly, the setup was inconvenient to the stream-aligned teams, as predicting whether their change sets would lead to their desired outcome.</p> <p>We therefore needed a testing setup which would:</p> <ul> <li>allow stream-aligned teams to test their merge requests against a maintainable set of test cases</li> <li>make it easy for stream-aligned teams to contribute tests</li> <li>allow the team which owned the reverse proxy to have confidence when rolling out changes</li> <li>comprehensively test for security issues and regressions</li> <li>run quickly to reduce CI/CD runtime and speed up iteration cycles</li> </ul> <h2 id="security-testing">Security Testing</h2> <p>We used <a href="https://github.com/dvershinin/gixy">gixy</a> to check for common security pitfalls in our templated nginx configuration files. Gixy runs static analysis on your configuration and notifies you in case of common misconfigurations. This helps in preventing server-side request forging (SSRF) or denial-of-service attack vectors. Running it was very painless, and I can thoroughly recommend it. Just a word of caution: since our resulting configuration was rather voluminous, a gixy run would take several minutes.</p> <h2 id="acceptance-testing">Acceptance testing</h2> <h3 id="the-tests">The tests</h3> <p>The setup allowed us to write tests like this (<code class="language-plaintext highlighter-rouge">example.com</code> would have been replaced with a production domain of ours, of course):</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_routing</span><span class="p">():</span> <span class="c1"># When a route owned by the team responsible for SCS A is requested </span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://example.com/scs-a/foo"</span><span class="p">)</span> <span class="c1"># Then the reverse proxy forwards the request to the scs-a host </span> <span class="k">assert</span> <span class="s">"HTTP_HOST=scs-a"</span> <span class="k">def</span> <span class="nf">test_referrer_header_should_be_retained</span><span class="p">():</span> <span class="c1"># When there is a request with the Referer HTTP header set </span> <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"Referer"</span><span class="p">:</span> <span class="s">"example.com"</span> <span class="p">}</span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://example.com/"</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span> <span class="c1"># Then the header should be forwarded to the upstream </span> <span class="k">assert</span> <span class="s">'HTTP_REFERER="example.com"'</span> <span class="ow">in</span> <span class="n">res</span><span class="p">.</span><span class="n">text</span> <span class="k">def</span> <span class="nf">test_server_side_includes_should_be_active</span><span class="p">():</span> <span class="c1"># When there is a request for a page for which SSI is active </span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://example.com/ssi-active/some-page-with-ssi"</span><span class="p">)</span> <span class="c1"># Then the SSI tag is resolved and replaced with the SSI fragment </span> <span class="k">assert</span> <span class="n">res</span><span class="p">.</span><span class="n">text</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="o">==</span> <span class="s">"SSI: fragment-content"</span> <span class="k">def</span> <span class="nf">test_server_side_includes_should_not_be_active</span><span class="p">():</span> <span class="c1"># When there is a request for a page for which SSI is not active </span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://example.com/ssi-inactive/some-page-with-ssi"</span><span class="p">)</span> <span class="c1"># Then the SSI fragment is not resolved and returned verbatim </span> <span class="k">assert</span> <span class="n">res</span><span class="p">.</span><span class="n">text</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="o">==</span> <span class="s">"SSI: &lt;!--#include virtual=</span><span class="se">\"</span><span class="s">/fragment/ssi-fragment</span><span class="se">\"</span><span class="s"> --&gt;"</span> <span class="c1"># Note: this was a much larger and more elaborate data set in practice </span><span class="n">production_www_domains</span><span class="p">:</span> <span class="p">[</span><span class="s">"www.tenant1.example.com"</span><span class="p">,</span> <span class="s">"www.tenant2.example.com"</span><span class="p">]</span> <span class="o">@</span><span class="n">mark</span><span class="p">.</span><span class="n">parametrize</span><span class="p">(</span><span class="s">"domain"</span><span class="p">,</span> <span class="n">production_www_domains</span><span class="p">)</span> <span class="k">def</span> <span class="nf">test_an_upstream_404_should_result_in_global_404_page</span><span class="p">(</span><span class="n">domain</span><span class="p">):</span> <span class="c1"># When there is a request to an upstream which returns an HTTP 404 </span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://</span><span class="si">{</span><span class="n">domain</span><span class="si">}</span><span class="s">/force-http-404"</span><span class="p">)</span> <span class="c1"># Then the reverse proxy retains this status code </span> <span class="k">assert</span> <span class="n">res</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">404</span> <span class="c1"># And replaces the upstream response body with the global 404 page </span> <span class="k">assert</span> <span class="n">res</span><span class="p">.</span><span class="n">text</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="o">==</span> <span class="s">"global 404 page"</span> </code></pre></div></div> <p>Below, I am going to explain how this worked under the hood.</p> <h3 id="the-test-setup">The test setup</h3> <p>We wanted to perform the acceptance tests on a running instance of the reverse proxy, to be as close as possible to the final deployment state. Actually starting nginx would also implicitly verify that the configuration was valid, because it fails to start up on invalid configuration. We decided to use a docker-compose setup to spin up a local integration setup. This setup contained several components besides the reverse proxy itself:</p> <p><img src="/images/blog/reverse-proxy-testing/testing.png" alt="The testing setup" /></p> <p>This is a simplified version of the resulting docker-compose setup:</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span> <span class="c1"># The reverse proxy, containing the production configuration</span> <span class="na">nginx</span><span class="pi">:</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">.</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span> <span class="c1"># The production Dockerfile for the reverse proxy</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s2">"</span><span class="s">127.0.0.1:80:80"</span> <span class="pi">-</span> <span class="s2">"</span><span class="s">127.0.0.1:443:443"</span> <span class="na">volumes</span><span class="pi">:</span> <span class="c1"># mock certificates so nginx may start up</span> <span class="pi">-</span> <span class="s">./test/ssl:/ssl/tenant-a:ro</span> <span class="pi">-</span> <span class="s">./test/ssl:/ssl/tenant-b:ro</span> <span class="na">links</span><span class="pi">:</span> <span class="c1"># Links provide DNS aliases pointing to the mock-server container</span> <span class="pi">-</span> <span class="s">mock-server:scs-1</span> <span class="pi">-</span> <span class="s">mock-server:scs-2</span> <span class="pi">-</span> <span class="s">mock-server:www.external.example.com</span> <span class="pi">-</span> <span class="s">mock-server:www.something-else.example.com</span> <span class="na">extra_hosts</span><span class="pi">:</span> <span class="c1"># In production, we used a CoreDNS instance to resolve upstreams.</span> <span class="c1"># Here, we use the docker internal DNS resolver instead.</span> <span class="pi">-</span> <span class="s2">"</span><span class="s">dns-server:127.0.0.11"</span> <span class="c1"># Mock upstream for testing, which e.g. responds with specified error codes,</span> <span class="c1"># upstream content, or returns the request URI and headers as a response</span> <span class="na">mock</span><span class="pi">:</span> <span class="na">hostname</span><span class="pi">:</span> <span class="s">mock-server</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">./test/containers/mock-server</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span> <span class="c1"># see below for content</span> <span class="na">depends_on</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">echo-fcgi</span> <span class="c1"># FCGI process used by the mock server, which renders request variables</span> <span class="c1"># (headers etc.) as a response</span> <span class="na">echo-fcgi</span><span class="pi">:</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">./test/containers/echo-fcgi</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span> <span class="na">networks</span><span class="pi">:</span> <span class="na">default</span><span class="pi">:</span> <span class="c1"># test network is set to 'internal' so no traffic escapes the test jig.</span> <span class="c1"># Otherwise, since the nginx configuration might reference externally</span> <span class="c1"># owned domains, you might involuntarily issue requests against these</span> <span class="c1"># external systems during testing.</span> <span class="c1"># If you just want to run the test jig for manually testing, you can</span> <span class="c1"># remove this section.</span> <span class="na">internal</span><span class="pi">:</span> <span class="no">true</span> <span class="na">driver</span><span class="pi">:</span> <span class="s">bridge</span> </code></pre></div></div> <p>Some notes on the setup:</p> <h3 id="nginx">nginx</h3> <p>The <code class="language-plaintext highlighter-rouge">nginx</code> container was built from the production Dockerfile. This ensured that the nginx configuration was as close to production as possible. We had volume mounts for the TLS certificates. The <code class="language-plaintext highlighter-rouge">/test/ssl</code> directory contained a single self-signed certificate. By mounting it multiple times to the subdirectories where the production certificates would be, we could provide nginx with the necessary certificates to start up.</p> <blockquote> <p>Note: the setup as we built it did not verify that the correct certificates were used. It would not be difficult to include this, though. You’d just need to generate certificates for each tenant domain and add them to the test trust store of the test container.</p> </blockquote> <p>The <code class="language-plaintext highlighter-rouge">extra_hosts</code> directive was used to provide DNS aliases for the upstream services. All these aliases pointed to the mock-server container, which was used to emulate the upstream services. In production, we used a CoreDNS instance to resolve upstreams. Here, we used the docker internal DNS resolver instead.</p> <blockquote> <p>Note: nginx by default only does DNS resolution at startup. In production, this can not only lead to stale resolutions, but also to a startup failure if the upstream name is unavailable at startup. Therefore, we configured nginx to dynamically resolve upstreams using the <code class="language-plaintext highlighter-rouge">resolver</code> directive. This has recently been made more convenient to do by the <a href="https://blog.nginx.org/blog/dynamic-dns-resolution-open-sourced-in-nginx">introduction of the </a><code class="language-plaintext highlighter-rouge">resolve</code> parameter.</p> </blockquote> <h3 id="mock-server">mock-server</h3> <p>The <code class="language-plaintext highlighter-rouge">mock-server</code> container was used to emulate the upstream services. It was also implemented using an nginx, but with an entirely different configuration. The mock-server container was used to test the routing rules in the nginx configuration. It could be configured to respond with specified error codes, upstream content, or return the request URI and headers. In order to do this, it had locations like this:</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># By default, echo request content as a response, using the echo-fcgi container</span> <span class="k">location</span> <span class="n">/</span> <span class="p">{</span> <span class="kn">fastcgi_pass</span> <span class="nf">echo-fcgi</span><span class="p">:</span><span class="mi">9000</span><span class="p">;</span> <span class="kn">fastcgi_param</span> <span class="s">SCRIPT_FILENAME</span> <span class="n">/srv/cgi/printenv</span><span class="p">;</span> <span class="kn">fastcgi_param</span> <span class="s">REQUEST_URI</span> <span class="nv">$request_uri</span><span class="p">;</span> <span class="kn">fastcgi_param</span> <span class="s">SCHEME</span> <span class="nv">$scheme</span><span class="p">;</span> <span class="p">}</span> <span class="c1"># Return specific test files from the /srv/www directory of the mock-server container</span> <span class="k">location</span> <span class="p">~</span> <span class="sr">^.*\.(js|css|html)</span> <span class="p">{</span> <span class="kn">root</span> <span class="n">/srv/www</span><span class="p">;</span> <span class="kn">rewrite</span> <span class="s">^.*</span><span class="err">\</span><span class="s">.(js|css|html)</span> <span class="n">/test.</span><span class="nv">$1</span> <span class="s">break</span><span class="p">;</span> <span class="p">}</span> <span class="c1"># Return specific status codes according to the request path</span> <span class="k">location</span> <span class="p">~</span> <span class="sr">.*/force-http-403</span> <span class="p">{</span> <span class="kn">return</span> <span class="mi">403</span><span class="p">;</span> <span class="p">}</span> <span class="k">location</span> <span class="p">~</span> <span class="sr">.*/force-http-404</span> <span class="p">{</span> <span class="kn">return</span> <span class="mi">404</span><span class="p">;</span> <span class="p">}</span> <span class="c1"># Page with SSI upstream</span> <span class="k">location</span> <span class="p">~</span> <span class="sr">^.*/page-with-ssi</span> <span class="p">{</span> <span class="kn">root</span> <span class="n">/srv/www</span><span class="p">;</span> <span class="kn">rewrite</span> <span class="s">^</span> <span class="n">/page-with-ssi.html</span> <span class="s">break</span><span class="p">;</span> <span class="p">}</span> <span class="c1"># page with broken SSI upstream</span> <span class="k">location</span> <span class="p">~</span> <span class="sr">^.*/page-with-ssi-force-http-(?&lt;code&gt;[0-9]+)</span> <span class="p">{</span> <span class="kn">add_header</span> <span class="s">Content-Type</span> <span class="nc">text/html</span><span class="p">;</span> <span class="c1"># SSI only works if the content-type is set correctly</span> <span class="kn">return</span> <span class="mi">200</span> <span class="s">"SSI:</span> <span class="s">&lt;!--</span><span class="c1">#include virtual=\"/fragment/force-http-$code\" --&gt;";</span> <span class="err">}</span> </code></pre></div></div> <p>Where <code class="language-plaintext highlighter-rouge">/srv/www/page-with-ssi.html</code> contained a basic SSI directive:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SSI: <span class="c">&lt;!--#include virtual="/fragment/ssi-fragment" --&gt;</span> </code></pre></div></div> <h3 id="echo-fcgi">echo-fcgi</h3> <p>The echo-fcgi container was used to render request variables (headers etc.) as a response.</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM debian:12-slim RUN \ DEBIAN_FRONTEND=noninteractive \ apt-get update &amp;&amp; \ apt-get -y install fcgiwrap COPY printenv /srv/cgi/printenv RUN chmod +x /srv/cgi/printenv USER root CMD ["spawn-fcgi", "-p", "9000", "-n", "/usr/sbin/fcgiwrap"] </code></pre></div></div> <p>Where the <code class="language-plaintext highlighter-rouge">printenv</code> script was a small perl script:</p> <div class="language-perl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/perl</span> <span class="c1"># ***** !!! WARNING !!! *****</span> <span class="c1"># This script echoes the server environment variables and therefore</span> <span class="c1"># leaks information - so NEVER use it in a live server environment!</span> <span class="c1"># It is provided only for testing purpose.</span> <span class="k">use</span> <span class="nv">strict</span><span class="p">;</span> <span class="k">use</span> <span class="nv">warnings</span><span class="p">;</span> <span class="k">print</span> <span class="p">"</span><span class="s2">X-Application-Timing: 235</span><span class="se">\n</span><span class="p">";</span> <span class="k">print</span> <span class="p">"</span><span class="s2">Content-type: text/plain; charset=iso-8859-1</span><span class="se">\n\n</span><span class="p">";</span> <span class="k">foreach</span> <span class="k">my</span> <span class="nv">$var</span> <span class="p">(</span><span class="nb">sort</span><span class="p">(</span><span class="nb">keys</span><span class="p">(</span><span class="o">%</span><span class="nv">ENV</span><span class="p">)))</span> <span class="p">{</span> <span class="k">my</span> <span class="nv">$val</span> <span class="o">=</span> <span class="nv">$ENV</span><span class="p">{</span><span class="nv">$var</span><span class="p">};</span> <span class="nv">$val</span> <span class="o">=~</span> <span class="sr">s|\n|\\n|g</span><span class="p">;</span> <span class="nv">$val</span> <span class="o">=~</span> <span class="sr">s|"|\\"|g</span><span class="p">;</span> <span class="k">print</span> <span class="p">"</span><span class="si">${var}</span><span class="s2">=</span><span class="se">\"</span><span class="si">${val}</span><span class="se">\"\n</span><span class="p">";</span> <span class="p">}</span> </code></pre></div></div> <h2 id="running-the-tests">Running the tests</h2> <p>The tests were written in Python, using <code class="language-plaintext highlighter-rouge">pytest</code> and <code class="language-plaintext highlighter-rouge">requests</code>. We chose Python because it is simple to use, and is known by many developers. Of course, you could use any testing framework you like. The tests were run in a container, which was attached to the docker network created by the above docker-compose setup:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM python:3.12 # Provide a working directory with correct permissions USER root RUN mkdir -p /test &amp;&amp; chown -R python: /test USER python WORKDIR /test ENV PATH="/home/python/.local/bin:${PATH}" # Install the test dependencies (this was done using a dependency management tool) RUN pip3 install --user pytest pytest-xdist requests # Copy the test files COPY --chown=python:python . . ENV PYTHONWARNINGS="ignore:Unverified HTTPS request" ENV PYTHONPATH="/test:${PYTHONPATH}" ENTRYPOINT ["pytest"] # Run tests concurrently and verbosely CMD ["-n", "16", "-vv"] </code></pre></div></div> <p>In order to have the tests run against the test setup, we used the <code class="language-plaintext highlighter-rouge">requests</code> library to provide a simple HTTP client. This client rewrote the requests so that they were always running against the <code class="language-plaintext highlighter-rouge">nginx</code> container. This enabled us to use the final production domains in the tests, which made them easier to understand.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span> <span class="k">class</span> <span class="nc">Request</span><span class="p">:</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="n">proto</span><span class="p">,</span> <span class="n">domain_and_path</span> <span class="o">=</span> <span class="n">url</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"://"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="n">domain</span> <span class="o">=</span> <span class="n">domain_and_path</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="n">path</span> <span class="o">=</span> <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">domain_and_path</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)[</span><span class="mi">1</span><span class="p">:])</span> <span class="c1"># Direct any requests to the nginx container, but set the host header to the domain </span> <span class="c1"># which was requested. </span> <span class="bp">self</span><span class="p">.</span><span class="n">headers</span> <span class="o">=</span> <span class="n">kwargs</span><span class="p">[</span><span class="s">"headers"</span><span class="p">]</span> <span class="k">if</span> <span class="s">"headers"</span> <span class="ow">in</span> <span class="n">kwargs</span> <span class="k">else</span> <span class="nb">dict</span><span class="p">()</span> <span class="bp">self</span><span class="p">.</span><span class="n">headers</span><span class="p">[</span><span class="s">"host"</span><span class="p">]</span> <span class="o">=</span> <span class="n">domain</span> <span class="c1"># While running in the docker network from the docker-compose file, </span> <span class="c1"># the DNS name "nginx" resolves to the nginx container </span> <span class="bp">self</span><span class="p">.</span><span class="n">request_url</span> <span class="o">=</span> <span class="s">"{}://nginx/{}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">proto</span><span class="p">,</span> <span class="n">path</span><span class="p">)</span> <span class="c1"># Remove the unmodified headers from the kwargs dict, so that we don't </span> <span class="c1"># forward them to the requests client (see below) </span> <span class="bp">self</span><span class="p">.</span><span class="n">kwargs</span> <span class="o">=</span> <span class="n">kwargs</span> <span class="k">if</span> <span class="s">"headers"</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">kwargs</span><span class="p">:</span> <span class="k">del</span> <span class="bp">self</span><span class="p">.</span><span class="n">kwargs</span><span class="p">[</span><span class="s">"headers"</span><span class="p">]</span> <span class="k">class</span> <span class="nc">Http</span><span class="p">:</span> <span class="o">@</span><span class="nb">staticmethod</span> <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">allow_redirects</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="n">req</span> <span class="o">=</span> <span class="n">Request</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="c1"># Note that we use verify=False here, because we used self-signed certificates and did not </span> <span class="c1"># want to bother with adding them to the trust store. </span> <span class="k">return</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">request_url</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">allow_redirects</span><span class="o">=</span><span class="n">allow_redirects</span><span class="p">,</span> <span class="n">verify</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">req</span><span class="p">.</span><span class="n">headers</span><span class="p">,</span> <span class="o">**</span><span class="n">req</span><span class="p">.</span><span class="n">kwargs</span><span class="p">)</span> <span class="o">@</span><span class="nb">staticmethod</span> <span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">allow_redirects</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span> <span class="n">req</span> <span class="o">=</span> <span class="n">Request</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="k">return</span> <span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">request_url</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">allow_redirects</span><span class="o">=</span><span class="n">allow_redirects</span><span class="p">,</span> <span class="n">verify</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">req</span><span class="p">.</span><span class="n">headers</span><span class="p">,</span> <span class="o">**</span><span class="n">req</span><span class="p">.</span><span class="n">kwargs</span><span class="p">)</span> </code></pre></div></div> <p>And this is how it all came together to make the tests from before possible:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_routing</span><span class="p">():</span> <span class="c1"># This request is rewritten by the `Http` class to redirect it to the `nginx` container. </span> <span class="c1"># nginx processes the request, and forwards it to the upstream, setting the </span> <span class="c1"># `Host` header in the process. The mock-server then returns a response body with all </span> <span class="c1"># the HTTP headers it sees. </span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://example.com/scs-a/foo"</span><span class="p">)</span> <span class="c1"># Therefore, we can assert on what the mock-upstream has seen. In this case, we </span> <span class="c1"># assert the host header, which tells us the server to which the reverse proxy would </span> <span class="c1"># have forwarded the request. </span> <span class="k">assert</span> <span class="s">"HTTP_HOST=scs-a"</span> <span class="k">def</span> <span class="nf">test_referrer_header_should_be_retained</span><span class="p">():</span> <span class="c1"># This is basically the same test as above, but with a custom header set by the client </span> <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"Referer"</span><span class="p">:</span> <span class="s">"example.com"</span> <span class="p">}</span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://example.com/"</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span> <span class="k">assert</span> <span class="s">'HTTP_REFERER="example.com"'</span> <span class="ow">in</span> <span class="n">res</span><span class="p">.</span><span class="n">text</span> <span class="k">def</span> <span class="nf">test_server_side_includes_should_be_active</span><span class="p">():</span> <span class="c1"># The mock-server is configured to return a page with an SSI tag for this path. </span> <span class="c1"># The upstream page content is 'SSI: &lt;!--#include virtual=\"/fragment/ssi-fragment\" --&gt;' </span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://example.com/ssi-active/some-page-with-ssi"</span><span class="p">)</span> <span class="c1"># If nginx is configured correctly, it is going to interpret the SSI tag, resolve the </span> <span class="c1"># include and replace the tag with the response of the SSI fragment </span> <span class="k">assert</span> <span class="n">res</span><span class="p">.</span><span class="n">text</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="o">==</span> <span class="s">"SSI: fragment-content"</span> <span class="k">def</span> <span class="nf">test_server_side_includes_should_not_be_active</span><span class="p">():</span> <span class="c1"># This is the inverse of the test above. For the /ssi-inactive prefix we don't want </span> <span class="c1"># nginx to parse the SSI tags </span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://example.com/ssi-inactive/some-page-with-ssi"</span><span class="p">)</span> <span class="c1"># This means that SSI tags are returned verbatim, without performing the include. </span> <span class="k">assert</span> <span class="n">res</span><span class="p">.</span><span class="n">text</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="o">==</span> <span class="s">"SSI: &lt;!--#include virtual=</span><span class="se">\"</span><span class="s">/fragment/ssi-fragment</span><span class="se">\"</span><span class="s"> --&gt;"</span> <span class="n">production_www_domains</span><span class="p">:</span> <span class="p">[</span><span class="s">"www.tenant1.example.com"</span><span class="p">,</span> <span class="s">"www.tenant2.example.com"</span><span class="p">]</span> <span class="o">@</span><span class="n">mark</span><span class="p">.</span><span class="n">parametrize</span><span class="p">(</span><span class="s">"domain"</span><span class="p">,</span> <span class="n">production_www_domains</span><span class="p">)</span> <span class="k">def</span> <span class="nf">test_an_upstream_404_should_result_in_global_404_page</span><span class="p">(</span><span class="n">domain</span><span class="p">):</span> <span class="c1"># Again, the Http class redirects all these requests to the nginx container </span> <span class="n">res</span> <span class="o">=</span> <span class="n">Http</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">f"https://</span><span class="si">{</span><span class="n">domain</span><span class="si">}</span><span class="s">/force-http-404"</span><span class="p">)</span> <span class="k">assert</span> <span class="n">res</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">404</span> <span class="k">assert</span> <span class="n">res</span><span class="p">.</span><span class="n">text</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="o">==</span> <span class="s">"global 404 page"</span> </code></pre></div></div> <p>As you can see, we were able to parametrize the tests to run against different tenant domains. This was very useful, as we could run the same test against all tenant domains, and thus ensure that the routing rules were correct for all of them. This was especially helpful when adding new tenant domains, as we could check the routing for the new domain without writing new tests. However, parametrizing the tests also led to a large number of test cases. This was not a problem, as we could use <code class="language-plaintext highlighter-rouge">pytest-xdist</code> to run the tests in parallel, which sped up the test run considerably.</p> <h2 id="conclusion">Conclusion</h2> <p>The setup described here allowed us to test the reverse proxy configuration both in a CI/CD pipeline and locally. The setup was very flexible, as we could easily add new test cases for new routing rules. It ran quickly, and afforded both the reverse proxy team and the stream-aligned teams a high degree of confidence when rolling out changes. Since the reverse proxy team could see both the implementation, and have the test cases as a specification, they could more easily infer the behavior which was to be introduced by the change set. The setup was also very useful for debugging, as developers could easily run the tests against a local instance of the reverse proxy, and directly see the results in the test output.</p> <p>I realize that the setup described here won’t work for every project. Especially, now that the usage of Kubernetes ingress controllers is becoming more widespread, you might need to devise another method of verifying changes. For server-side rendering with includes, which are still widespread in micro frontend setups, I think the approach outlined above can be very useful. I hope that this post has given you some inspiration on how or whether to test your reverse proxy configuration. I would be very interested in hearing your approach to similar challenges :)</p> <p><em>Photo credit: <a href="https://unsplash.com/photos/white-slide-rule-A_gGAj9SaP8">Daniel Andrade</a></em></p>["Moritz Wagner"]Reverse proxies often are the key component in a web platform, be it eCommerce, B2B or anything else. Hence, they need to be rock solid. In my experience, reverse proxy outages only seldomly occur due to bugs in the proxy software itself, but rather from misconfiguration.Externe Dienstleister sind eine Sackgasse2025-01-03T16:00:00+01:002025-01-03T16:00:00+01:00https://inoio.de/blog/2025/01/03/externe-berater-sackgasse<p>Häufig haben unsere Kunden zwei Ziele:</p> <ol> <li>Outcome erhöhen und</li> <li>Expertise aufnehmen.</li> </ol> <p>Ein wichtiger Aspekt wird jedoch von vielen Entscheidern übersehen: Teams bestehen aus hochkomplexen sozialen Strukturen. Diese Strukturen sind maßgeblich dafür verantwortlich, wie “gut” das Team zusammen arbeitet und wie viel Outcome es liefern kann.</p> <p>Ein Beispiel: Wenn die psychologische Sicherheit in einem Team sinkt, kann das zu Missverständnissen und dysfunktionalen Verhaltensmuster führen. Die Produktivität sinkt drastisch, weil Mitglieder sich nicht mehr in ihren Bedürfnissen gesehen fühlen. Das Team arbeitet im schlimmsten Fall nicht mehr miteinander, sondern gegeneinander.</p> <div align="center"><img src="/images/blog/jonathan-farber-QgeIMfZJgFs-unsplash.jpg" alt="Sackgasse" width="70%" /></div> <h2 id="externe-dienstleister-sind-eine-sackgasse-">Externe Dienstleister sind eine Sackgasse …</h2> <p>… zumindest langfristig gesehen. Denn: Wenn man einen externen Dienstleister beauftragt, um den Outcome zu erhöhen, kann es tatsächlich den gewünschten Effekt haben - zumindest so lange wie die Unterstützung dauert. Hört allerdings die Beauftragung auf und sind die tief liegenden Konflikte im Team nicht gelöst, geht auch die Produktivität wieder runter.</p> <p>Typischerweise versuchen Unternehmen das Miteinander in oder zwischen den Teams durch z.B. Agile Coaches zu adressieren. Aus unserer Erfahrung machen die häufig einen richtig guten Job. Nur haben sie einen großen Nachteil: Sie sind naturgemäß nicht so tief im Team verankert wie ein:e Entwickler:in, die beispielsweise auch Teil von 1:1 Absprachen ist.</p> <h2 id="was-macht-inoio-anders">Was macht inoio anders?</h2> <p>Wenn ich also behaupte, dass externe Dienstleister eine Sackgasse für den langfristigen Projekterfolg sind, warum sollte dann jemand inoio buchen?</p> <p>In den letzten zwölf Jahren haben wir viele Projekte begleiten dürfen und sind immer stärker zu der Erkenntnis gelangt, wie wichtig es ist, insbesondere die sozialen und organisatorischen Faktoren für einen Projekterfolg zu bearbeiten:</p> <h3 id="kommunikationskultur-prägen">Kommunikationskultur prägen</h3> <p>“Communication is hard”. Kommunikation lernen wir hauptsächlich durch Nachahmung - nur wenige von uns hatten in der Schule das Fach “Kommunikation”. Darum trainieren wir bei inoio kontinuierlich einen Bedürfnis-Orientierten Kommunikationsstil, den wir dann in das Projekt tragen und dort multiplizieren.</p> <h3 id="gruppendynamik-bewusst-gestalten">Gruppendynamik bewusst gestalten</h3> <p>Weiterhin lassen sich viele von uns darin schulen, Gruppendynamiken zu verstehen und seinen eigenen Anteil darin zu erkennen. So können wir bewussten Einfluss darauf nehmen, um funktionale Dynamiken zu etablieren. Zusätzlich lernen die anderen Teammitglieder, wie sie selbstwirksam durch <a href="/blog/2018/07/16/hilfe-ein-konflikt/">Konflikte</a> und <a href="/blog/2018/05/16/kreislauf/">Dynamiken</a> wachsen können, anstatt zu resignieren.</p> <h3 id="systemische-beratung-zur-organisations--und-persönlichkeitsentwicklung">Systemische Beratung zur Organisations- und Persönlichkeitsentwicklung</h3> <p>Darüber hinaus haben wir Softwareentwickler, die sich zusätzlich als Systemische Berater weitergebildet haben, um noch intensiver Menschen und Organisationen zu helfen, innere und äußere Prozesse zu erkennen und zu bearbeiten. Dies kann sowohl implizit geschehen, als auch explizit durch niedrigschwellige Angebote zu Coaching und Beratung für die Mitarbeitenden des Unternehmens.</p> <p>Wir lieben es, komplexe Probleme zu verstehen und geeignete Software dafür zu programmieren. Um das Team aber nachhaltig nach vorne zu bringen, wollen wir Organisationen dabei unterstützen, dass die Menschen besser zusammen arbeiten können und somit die intrinsische Motivation der Mitarbeitenden gefördert wird. So zahlt sich unser Engagement sowohl kurzfristig als auch langfristig aus.</p> <p>Was sind Deine Herausforderungen im IT-Projekt? Was brauchst Du für Euren Projekterfolg? <a href="mailto:[email protected]">Schreib mir eine Mail</a> oder ruf mich einfach an: <a href="tel:+4940609469820">+49-40-60946982-0</a></p> <p><em>Fotocredit: <a href="https://unsplash.com/de/fotos/weisses-blaues-und-rotes-strassenschild-in-der-nahe-der-strasse-QgeIMfZJgFs">Jonathan Farber</a></em></p>Matthias BrandtHäufig haben unsere Kunden zwei Ziele: Outcome erhöhen und Expertise aufnehmen.Wieder grüßt das Murmeltier: 2. Teil der Fehlerbehandlung für Kafka Consumer mit Retries2024-11-11T14:45:00+01:002024-11-11T14:45:00+01:00https://inoio.de/blog/2024/11/11/kafka-retries-II<p>Dies ist eine Fortsetzung des Blog-Posts <a href="https://inoio.de/blog/2023/07/17/kafka-retries/">Fehlerbehandlung für Kafka Consumer mit Retries</a>, mit seitdem gewonnenen Erkenntnissen.</p> <pre><code class="language-quote">Phil Connors: Well, it's Groundhog Day... again... </code></pre> <p><img src="/images/blog/murmler2.jpg" alt="Dicker Murmler" /></p> <h2 id="hilfsklasse-processed-messages-filter">Hilfsklasse <code class="language-plaintext highlighter-rouge">Processed-Messages-Filter</code></h2> <p>Im ersten Teil haben wir uns Non-blocking Retries angesehen, wodurch die Reihenfolge der Kafka-Events durcheinanderkommen kann: Deshalb haben wir die Uhrzeiten der Events verglichen, um veraltete Events zu verwerfen. Konkret in dem Beispiel war es die Uhrzeit des letzten gespeicherten Wetterberichtes: Kommen ältere Wetterberichte durch ein Retry herein, können wir sie ignorieren.</p> <p>Hier hat sich in unseren Projekten inzwischen eine generische Hilfsklasse <code class="language-plaintext highlighter-rouge">ProcessedMessagesFilter</code> etabliert. Wir speichern die Uhrzeit des letzten verarbeiteten Events in der Datenbank:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@Entity data class ProcessedEvent( val eventType: String, // "weatherForecast" val key: String, // "Hamburg", Key des Kafka-Events val timestamp: OffsetDateTime, // Uhrzeit des letzten Updates ) </code></pre></div></div> <p>Damit können wir generisch prüfen, ob bereits neuere Events verarbeitet wurden. So sieht die Benutzung aus:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>processedMessagesFilter.processIfNotOutdated( eventType = "weatherForecast", key = weatherForecast.city, timestamp = weatherForecast.time) { // wird nur ausgeführt, falls das event nicht veraltet ist repository.save(weatherForecast) } </code></pre></div></div> <p>Vorteil der generischen Lösung ist eine Vereinfachung des Codes, weil wir nicht mehr wissen müssen, wo in den fachlichen Objekten Timestamps gespeichert werden. Und vor allem: Wenn am Ende ihres Lebenszyklus die fachlichen Objekte gelöscht werden, merken wir uns davon unabhängig weiterhin, dass diese Events bereits verarbeitet wurden. Sonst könnte es sein, dass durch ein Retry ein bereits gelöschtes Objekt wieder angelegt wird.</p> <p>Die Implementierung des ProcessedMessagesFilters ist straight-forward:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@Service class ProcessedMessagesFilter( val processedMessagesRepository: ProcessedMessagesRepository, ) { fun &lt;R&gt; processIfNotOutdated( eventType: String, key: String, timestamp: OffsetDateTime, process: () -&gt; R) { if (!isOutdated(eventType, key, timestamp)) { process() processedMessagesRepository.save(ProcessedEvent(eventType, key, timestamp)) } } private fun isOutdated( eventType: String, key: String, timestamp: OffsetDateTime): Boolean { return processedMessagesRepository.findBy(eventType, key) .map { lastProcessed -&gt; // note: If the timestamp are identical, we want to process, // allowing to re-process a topic if needed (timestamp.isBefore(lastProcessed.timestamp)) }.orElse( // unknown = new key, we want to process it false, ) } } </code></pre></div></div> <p>Zusätzlich sollte man noch einen Aufräumjob implementieren, der nach längerer Zeit (mehreren Monaten) die ProcessedEvents aufräumt.</p> <p><img src="/images/blog/murmler3.jpg" alt="Dicker Murmler" /></p> <h2 id="fiese-fallen">Fiese Fallen</h2> <p>Zwei neue fiese Fallen aus der Praxis!</p> <h3 id="fiese-falle-exponentielle-retry-vermehrung">Fiese Falle: Exponentielle Retry-Vermehrung</h3> <p>Durch einen Copy-und-Paste Fehler hatten zwei Consumer in verschiedenen Consumer-Groups dasselbe Retry-Topic konfiguriert. Was jetzt passierte: Eine Nachricht landete im Retry-Topic, beide Consumer konsumierten sie, liefen beide auf einen Fehler, und schrieben jeweils eine Nachricht in das Retry-Topic. Aus 1 Nachricht wurden also 2, 4, 8, 16…, bis der Kafka in die Knie ging.</p> <h3 id="fiese-falle-große-callstacks">Fiese Falle: Große Callstacks</h3> <p>Unsere RetryTopics wurden sehr groß, fast 80 GB. Die Analyse zeigte, dass die Retry-Events in einem Kafka-Header den gesamten Callstack des fehlgeschlagenen Versuchs enthielten. Der Callstack-Retry-Header hatte fast 15000 Bytes, im Vergleich zu dem kleinen Event mit mit 350 Bytes plus sonstige Retry-Header mit 1300 Bytes. Das Event wurde also fast um den Faktor 50 größer, und zusätzlich 50x retried. Damit wurden aus <code class="language-plaintext highlighter-rouge">30 MB Originalevents * 50 * 50 = 75 GB</code> Retry-Daten. Auch diese Header lassen sich abschalten:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@EnableKafka</span> <span class="nd">@Configuration</span> <span class="nd">@EnableScheduling</span> <span class="kd">class</span> <span class="nc">RetryTopicsConfig</span> <span class="p">:</span> <span class="nc">RetryTopicConfigurationSupport</span><span class="p">()</span> <span class="p">{</span> <span class="k">override</span> <span class="k">fun</span> <span class="nf">configureDeadLetterPublishingContainerFactory</span><span class="p">():</span> <span class="nc">Consumer</span><span class="p">&lt;</span><span class="nc">DeadLetterPublishingRecovererFactory</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">Consumer</span><span class="p">&lt;</span><span class="nc">DeadLetterPublishingRecovererFactory</span><span class="p">&gt;</span> <span class="p">{</span> <span class="n">recovererFactory</span> <span class="p">-&gt;</span> <span class="n">recovererFactory</span><span class="p">.</span><span class="nf">setRetainAllRetryHeaderValues</span><span class="p">(</span><span class="k">false</span><span class="p">)</span> <span class="n">recovererFactory</span><span class="p">.</span><span class="nf">setDeadLetterPublishingRecovererCustomizer</span> <span class="p">{</span> <span class="n">dlpr</span> <span class="p">-&gt;</span> <span class="n">dlpr</span><span class="p">.</span><span class="nf">excludeHeader</span><span class="p">(</span> <span class="nc">HeadersToAdd</span><span class="p">.</span><span class="nc">EX_STACKTRACE</span><span class="p">,</span> <span class="nc">HeadersToAdd</span><span class="p">.</span><span class="nc">EXCEPTION</span><span class="p">,</span> <span class="nc">HeadersToAdd</span><span class="p">.</span><span class="nc">EX_CAUSE</span><span class="p">,</span> <span class="nc">HeadersToAdd</span><span class="p">.</span><span class="nc">EX_MSG</span><span class="p">,</span> <span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <h2 id="fazit">Fazit</h2> <p>Mit dem generischen ProcessedEventsService und dem Support durch Spring sind Retries schnell eingebaut. Die beschriebenen Fallen sollte man aber kennen, weil man mit Retries versehentlich sehr viele Events produzieren kann.</p> <p>Wir wünschen euch weiterhin viel Erfolg mit der Fehlerbehandlung mit Retries!</p> <pre><code class="language-quote">Phil: Do you know what today is? Rita: No, what? Phil: Today is tomorrow. It happened. </code></pre>["Fabian Nilius"]Dies ist eine Fortsetzung des Blog-Posts Fehlerbehandlung für Kafka Consumer mit Retries, mit seitdem gewonnenen Erkenntnissen.