<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="de"><link href="https://www.manuel-holzrichter.de/de/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.manuel-holzrichter.de/de/" rel="alternate" type="text/html" /><updated>2026-09-30T16:00:00.000Z</updated><id>https://www.manuel-holzrichter.de/de/feed.xml</id><title type="html">Manuel Holzrichter</title><subtitle>Pragmatische Softwareentwicklung — Architektur, klare Verantwortlichkeiten und was Entwickler wirklich effektiv macht. Aus über 15 Jahren Praxis.</subtitle><author><name>Manuel Holzrichter</name></author><entry><title type="html">Die versteckten Kosten von nicht wartbarem Code</title><link href="https://www.manuel-holzrichter.de/de/2026/09/30/die-versteckten-kosten-von-nicht-wartbarem-code/" rel="alternate" type="text/html" title="Die versteckten Kosten von nicht wartbarem Code" /><published>2026-09-30T16:00:00.000Z</published><updated>2026-09-30T16:00:00.000Z</updated><id>https://www.manuel-holzrichter.de/de/2026/09/30/die-versteckten-kosten-von-nicht-wartbarem-code</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2026/09/30/die-versteckten-kosten-von-nicht-wartbarem-code/"><![CDATA[<p>Montagmorgen. Ein Entwickler, der vor drei Wochen ins Team gekommen ist, nimmt sich ein Ticket aus dem Backlog: „Bestellungen aus dem Dezember können bis zum 31. Januar zurückgegeben werden.“ Der Product Owner hat es auf eine Stunde geschätzt. Eine kleine Änderung. Ein Datum, eine Bedingung, fertig vor dem Mittagessen.</p>
<p>Drei Tage später ist die Änderung immer noch nicht deployt. Der Entwickler hat vier Stellen in der Codebasis gefunden, die sich mit der Rückgabefrist beschäftigen. Sie widersprechen sich. Niemand im Team kann sagen, welche richtig ist. Und der erste Versuch, die Änderung umzusetzen, hat die Zahlungserinnerungen für alle Dezember-Bestellungen kaputt gemacht.</p>
<p>Nichts an dieser Geschichte ist ungewöhnlich. Ich habe sie in fast jedem Legacy-System erlebt, das ich übernommen habe. Die Änderung selbst war trivial. Der Code drumherum war es nicht.</p>
<h2 id="die-kosten-die-auf-keiner-rechnung-stehen">Die Kosten, die auf keiner Rechnung stehen</h2>
<p>Nicht wartbarer Code scheitert selten laut. Er stürzt nicht am ersten Tag ab. Er macht nur jede Änderung ein bisschen langsamer, ein bisschen riskanter und ein bisschen beängstigender als die davor. Die Kosten sind real, aber sie tauchen nie als eigener Posten auf. Sie zeigen sich als Angst.</p>
<p><strong>Angst vor dem Deployment in Produktion.</strong> Auf jedes Release folgt die Frage: Was haben wir diesmal kaputt gemacht? Teams reagieren darauf, indem sie seltener releasen, mehr Änderungen in jedes Release packen und manuelle Testphasen einführen. Und das macht jedes Release noch riskanter.</p>
<p><strong>Angst vor Updates von Abhängigkeiten.</strong> Ein Framework oder eine Bibliothek zu aktualisieren heißt, Code anzufassen, den niemand ganz versteht. Also wird das Update verschoben. Und wieder verschoben. Sicherheitspatches stapeln sich, und irgendwann ist das Update keine Aufgabe mehr, sondern ein Projekt.</p>
<p><strong>Angst vor Produktionsvorfällen.</strong> Wenn etwas schiefgeht, weiß niemand, wo er suchen soll. Die Ursache könnte überall liegen. Ich habe Bugs gesehen, bei denen die Suche nach der Ursache Wochen gedauert hat und die Behebung zehn Minuten.</p>
<p>Hinter diesen Ängsten stecken Kosten, die schwerer zu sehen sind:</p>
<ul>
<li><strong>Schätzungen werden unzuverlässig.</strong> Wenn niemand weiß, was eine Änderung alles berührt, ist jede Schätzung geraten. Das Vertrauen zwischen Entwicklung und Fachbereich bröckelt, und die Antwort darauf ist meist mehr Kontrolle, mehr Meetings und mehr Puffer.</li>
<li><strong>Wissen konzentriert sich in wenigen Köpfen.</strong> Nur ein oder zwei Leute verstehen bestimmte Teile des Systems. Sie werden zum Engpass – und zum Risiko, wenn sie krank sind oder gehen.</li>
<li><strong>Das Onboarding dauert Monate statt Wochen.</strong> Ein neuer Entwickler kann kein System lernen, das sich nicht selbst erklärt. Er muss es von Menschen lernen.</li>
<li><strong>Gute Entwickler gehen.</strong> In einer Codebasis zu arbeiten, in der jede Änderung ein Kampf ist, ist anstrengend. Wer Angebote hat, nimmt sie an.</li>
<li><strong>Die Kosten summieren sich.</strong> Jede Abkürzung macht die nächste Änderung teurer. Irgendwann sind die Zinsen so hoch, dass keine Kapazität mehr bleibt, um die Schulden abzuzahlen.</li>
</ul>
<p>Nichts davon ist in einem Sprint-Report zu sehen. Alles davon sieht man daran, wie ein Team über seinen eigenen Code denkt.</p>
<h2 id="wo-ich-anfange">Wo ich anfange</h2>
<p>Ich habe über die Jahre viele Legacy-Systeme übernommen und sie in Codebasen verwandelt, in denen Teams wieder gerne arbeiten. Der Ablauf ist immer derselbe, und er beginnt nicht mit Code.</p>
<p>Der erste Schritt ist Verstehen. Welches Problem löst dieses System? Welchen Wert soll es schaffen? Wie erreicht es dieses Ziel, mit welchen Konzepten? Welche Akteure bewegen sich durch das System, und wofür ist jeder von ihnen verantwortlich?</p>
<p>Hätten diese Systeme Tests, wäre das oft eine leichte Aufgabe. Tests beschreiben, wie sich das System verhalten soll. Aber in keinem Legacy-System, an dem ich gearbeitet habe, konnte man ernsthaft von Testabdeckung sprechen. Also besteht der zweite Schritt darin, das bestehende Verhalten in Tests zu gießen, mit so wenigen Änderungen am Code wie möglich.</p>
<p>Dabei passieren die ersten Refactorings. Eine Komponente mit vielen Abhängigkeiten braucht ein riesiges Test-Setup. Also wäge ich ständig zwei Dinge gegeneinander ab: Wie stark ändere ich die Struktur des Codes, und wie komplex darf mein Test-Setup werden? Kompromisse mache ich hier selten. Kompromisse rächen sich früher, als man denkt. In der Praxis heißt das: eine klare Domäne ohne technische Details einführen, eine Anwendungsschicht, die die Akteure der Domäne orchestriert, eine Persistenzschicht, deren einzige Aufgabe es ist, Zustand zu speichern und wiederherzustellen, und eine Humble UI, sauber vom Rest getrennt.</p>
<p>Während ich auf die Testabdeckung hinarbeite, baue ich mein Verständnis der Codebasis auf. Ich lerne ihre Konzepte kennen. Ich finde implizite Konzepte und schreibe sie auf, um sie später explizit zu machen. Ich finde Entscheidungen, die an mehreren Stellen getroffen werden. Alles, was nicht ganz ins Bild passt, aber irgendwie trotzdem funktioniert, kommt auf die Liste. Diese Notizen sind oft Hinweise auf ein strukturelles Problem. Oder anders gesagt: auf ein Konzept, das extrem kompliziert umgesetzt wurde.</p>
<p>Dann refactore ich, eine Notiz nach der anderen. Das Ziel ist einfacher, langweiliger Code, der tut, was er soll.</p>
<h2 id="der-entwickler-ist-der-kunde-des-codes">Der Entwickler ist der Kunde des Codes</h2>
<p>Hinter all dem steht eine Idee. Normalerweise denken wir bei den Kunden unserer Software an die Nutzer. Aber auch der Code selbst hat einen Kunden: den Entwickler, der ihn als Nächstes ändern muss.</p>
<p>Alles, was ein Entwickler bei einer Änderung im Kopf behalten muss, ist kognitive Last. Implizite Regeln, Stellen, die sich gemeinsam ändern müssen, Seiteneffekte in Features, die nichts miteinander zu tun haben, die Frage, wo man überhaupt suchen soll. Je mehr von dieser Last der Code dem Entwickler aufbürdet, desto langsamer und riskanter wird jede Änderung. Nicht weil der Entwickler schlecht ist, sondern weil das menschliche Arbeitsgedächtnis begrenzt ist.</p>
<p>Wartbarer Code nimmt diese Last weg. Er erklärt sich selbst, er hat für jede Entscheidung genau einen Ort, und er hat Tests, die dir sofort sagen, wenn du etwas kaputt gemacht hast. Dem Entwickler die Arbeit leicht zu machen ist kein Luxus. Genau daher kommt Effizienz.</p>
<p>Gehen wir zurück zu dem Entwickler und dem Dezember-Ticket und schauen wir uns an, worauf er gestoßen ist.</p>
<h2 id="schritt-1-zuerst-das-verhalten-festnageln">Schritt 1: Zuerst das Verhalten festnageln</h2>
<p>Der Entwickler fängt nicht damit an, Code zu ändern. Er fängt damit an, einen Test für das aktuelle Verhalten zu schreiben: Eine Bestellung, die am 1. März geliefert wurde, kann am 15. März zurückgegeben werden, aber nicht am 16. März.</p>
<p>Selbst das ist schwieriger als gedacht. Die Rückgabelogik steckt in einem <code>OrderService</code>, der direkt mit der Datenbank, dem Mailserver und einem PDF-Renderer spricht und die aktuelle Zeit mit <code>LocalDateTime.now()</code> liest. Um eine Regel zu testen, muss man das halbe System aufbauen. Also passieren die ersten kleinen Refactorings, bevor überhaupt an einem Feature gearbeitet wird: Die Uhr wird hineingereicht statt ausgelesen, die Regel wird aus dem Datenbankaufruf herausgelöst.</p>
<p>Der erste Test kann sogar falsches Verhalten festschreiben. Das ist in Ordnung. Jede Definition ist besser als keine Definition. Sie wird in Gesprächen mit den Leuten korrigiert, die das Geschäft kennen. Wichtig ist, dass das Verhalten als automatisierter Test aufgeschrieben ist. Ohne Tests musst du langsam und vorsichtig gehen. Mit Tests kannst du rennen, weil du in dem Moment Bescheid bekommst, in dem du etwas kaputt machst.</p>
<p>Vertiefung: Legacy-Code ohne Angst refactoren</p>
<h2 id="schritt-2-das-konzept-das-es-nicht-gibt">Schritt 2: Das Konzept, das es nicht gibt</h2>
<p>Der Entwickler durchsucht die Codebasis nach „Rückgabefrist“. Nichts. Das Konzept, über das der Fachbereich jeden Tag spricht, existiert im Code nicht. Was existiert, ist das hier:</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="kotlin"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">private</span><span style="color:#D73A49;--shiki-dark:#F97583"> fun</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> withinDeadline</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(order: </span><span style="color:#6F42C1;--shiki-dark:#B392F0">Order</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    order.deliveredAt</span><span style="color:#D73A49;--shiki-dark:#F97583">!!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">plusDays</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">14</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">>=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> LocalDateTime.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">now</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre>
<p>Jemand musste wissen, dass „Deadline“ hier die Rückgabefrist meint und dass <code>14</code> eine geschäftliche Entscheidung ist und keine technische Konstante. Dieses Wissen steckte im Kopf des ursprünglichen Autors. Es ist weg.</p>
<p>Wenn ein Konzept implizit ist, muss jeder Entwickler es aus Arithmetik rekonstruieren. Wenn es explizit ist – eine <code>ReturnPeriod</code> mit einem Namen, einem Ort und einer klaren Regel –, lässt sich der Code in der Sprache des Fachbereichs lesen, und die Dezember-Regel hat einen offensichtlichen Platz.</p>
<p>Vertiefung: Implizite Konzepte explizit machen</p>
<h2 id="schritt-3-vier-stellen-drei-antworten">Schritt 3: Vier Stellen, drei Antworten</h2>
<p>Beim Schreiben der Tests findet der Entwickler die Rückgabefrist an vier Stellen:</p>
<ol>
<li>Das Backend zählt 14 Tage ab Lieferung, in Serverzeit.</li>
<li>Das Frontend zählt 14 Tage ab Bestelldatum.</li>
<li>Ein SQL-Report für den Kundenservice zählt zwei Wochen ab Lieferung, in UTC.</li>
<li>Die Bestellbestätigung per E-Mail sagt „innerhalb von 14 Tagen“, fest einprogrammiert.</li>
</ol>
<p>Keine davon war falsch, als sie geschrieben wurde. Jede war eine vernünftige Auslegung einer Regel, die nie an einer Stelle aufgeschrieben wurde. Aber sie sind auseinandergedriftet, und der Kunde sieht im Shop „rückgabefähig“, während das Backend die Rückgabe ablehnt.</p>
<p>Dazu führen implizite Konzepte. Dieselbe Entscheidung wird an mehreren Stellen getroffen, und mehrere Kopien einer Entscheidung driften auseinander. Die Frage ist nicht, ob, sondern wann. Die Dezember-Änderung hat diesen Bug nicht verursacht. Sie hat ihn nur sichtbar gemacht.</p>
<p>Die Lösung ist nicht, vier Stellen anzupassen. Die Lösung ist, die Entscheidung einmal zu treffen und alles andere nach dem Ergebnis fragen zu lassen.</p>
<p>Vertiefung: Eine Entscheidung, ein Ort</p>
<h2 id="schritt-4-die-änderung-die-die-rechnungen-kaputt-machte">Schritt 4: Die Änderung, die die Rechnungen kaputt machte</h2>
<p>Der erste Versuch des Entwicklers war der naheliegende: die Dezember-Regel in <code>withinDeadline</code> einbauen. Die Rückgabetests liefen durch. Am nächsten Morgen meldete die Buchhaltung, dass für Dezember-Bestellungen keine Zahlungserinnerungen verschickt worden waren.</p>
<p>Derselbe <code>OrderService</code> verschickt auch Zahlungserinnerungen an Kunden, die auf Rechnung kaufen. Deren Zahlungsziel liegt zufällig ebenfalls bei 14 Tagen nach Lieferung, also hat jemand <code>withinDeadline</code> dafür wiederverwendet. Zwei Geschäftsregeln, die nichts miteinander zu tun haben und zwei verschiedenen Abteilungen gehören, teilten sich eine Implementierung, weil sie zufällig dieselbe Zahl hatten.</p>
<p>Eine Komponente, die gleichzeitig dem Kundenservice, der Buchhaltung und dem Marketing dient, hat drei Gründe, sich zu ändern. Jede Änderung für einen davon riskiert, die anderen kaputt zu machen.</p>
<p>Vertiefung: Eine Verantwortung pro Komponente</p>
<h2 id="schritt-5-die-regel-am-falschen-ort">Schritt 5: Die Regel am falschen Ort</h2>
<p>Schließlich stellt der Entwickler fest, dass die Rückgaberegel auch in einer SQL-Abfrage steckt und dass die Domänenlogik von der Systemuhr abhängt. Die Datumsformatierung für den Shop wird im Backend berechnet. Die Datenbankabfrage kennt Geschäftsregeln. Die Geschäftsregel kennt die Uhr.</p>
<p>Wenn Verantwortungen in der falschen Schicht liegen, breiten sich Änderungen über Schichten aus, die das nichts angehen sollte. Die UI sollte nur entscheiden, wie die Dinge aussehen. Die Persistenz sollte nur Zustand speichern und wiederherstellen. Die Domäne sollte die Geschäftsregeln kennen und nichts über Technik wissen. Und die Anwendungsschicht sollte die Domäne orchestrieren und externe Abhängigkeiten hinter Ports halten.</p>
<p>Vertiefung: Verantwortung in der richtigen Schicht</p>
<h2 id="dieselbe-änderung-noch-einmal">Dieselbe Änderung, noch einmal</h2>
<p>Nach dem Refactoring ist die Rückgabefrist ein Konzept in der Domäne. Sie wird an einer Stelle berechnet. Das Frontend, die E-Mail und der Report holen sie alle von dort. Zahlungsziele sind ein eigenes Konzept. Die Uhr wird hineingereicht.</p>
<p>Jetzt sieht das Dezember-Ticket so aus:</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="kotlin"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">class</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> HolidayReturnPolicy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#D73A49;--shiki-dark:#F97583">private</span><span style="color:#D73A49;--shiki-dark:#F97583"> val</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> standardPolicy: </span><span style="color:#6F42C1;--shiki-dark:#B392F0">ReturnPolicy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) : </span><span style="color:#6F42C1;--shiki-dark:#B392F0">ReturnPolicy</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    override</span><span style="color:#D73A49;--shiki-dark:#F97583"> fun</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> returnPeriodFor</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(orderedOn: </span><span style="color:#6F42C1;--shiki-dark:#B392F0">LocalDate</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, deliveredOn: </span><span style="color:#6F42C1;--shiki-dark:#B392F0">LocalDate</span><span style="color:#24292E;--shiki-dark:#E1E4E8">): </span><span style="color:#6F42C1;--shiki-dark:#B392F0">ReturnPeriod</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        val</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> standardPeriod </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> standardPolicy.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">returnPeriodFor</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(orderedOn, deliveredOn)</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (orderedOn.month </span><span style="color:#D73A49;--shiki-dark:#F97583">!=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Month.DECEMBER) </span><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> standardPeriod</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> standardPeriod.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">extendedTo</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(LocalDate.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">of</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(orderedOn.year </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">31</span><span style="color:#24292E;--shiki-dark:#E1E4E8">))</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="kotlin"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">@Test</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">fun</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> `december orders can be returned until january 31`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    val</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> policy </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> HolidayReturnPolicy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#6F42C1;--shiki-dark:#B392F0">StandardReturnPolicy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    val</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> period </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> policy.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">returnPeriodFor</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        orderedOn </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> LocalDate.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">of</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">2026</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">12</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">10</span><span style="color:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        deliveredOn </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> LocalDate.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">of</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">2026</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">12</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">12</span><span style="color:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    )</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    assertEquals</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(LocalDate.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">of</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">2027</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">31</span><span style="color:#24292E;--shiki-dark:#E1E4E8">), period.endsOn)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>Eine neue Klasse, ein neuer Test, eine Zeile Verdrahtung. Derselbe Entwickler setzt es in weniger als der Stunde um, die der Product Owner geschätzt hat. Nicht weil er das System jetzt besser kennt, sondern weil er es nicht muss.</p>
<h2 id="warum-mir-diese-arbeit-spaß-macht">Warum mir diese Arbeit Spaß macht</h2>
<p>Ich bin ehrlich: Software wartbar zu machen ist einer der befriedigendsten Teile meiner Arbeit. Ein implizites Konzept finden, darüber nachdenken, wie man es in der Struktur des Codes ausdrückt, und zusehen, wie aus kompliziertem Code etwas Einfaches, Schönes und Stabiles wird – davon bekomme ich nie genug.</p>
<p>Und es gibt eine ganz bestimmte Art von Ruhe, wenn man eine doppelte Entscheidung an einem Ort zusammenführt. Ich weiß, dass der nächste Entwickler, der sie ändert, dort keinen Bug produzieren wird. Nicht weil er vorsichtig ist, sondern weil der Code keinen Raum dafür lässt.</p>
<p>Das bedeutet Wartbarkeit für mich. Sie ist kein Anliegen für Puristen. Sie ist das, was Software erlaubt, sich weiter zu verändern, und was den Menschen, die daran arbeiten, wieder Freude an ihrer Arbeit gibt. Keine Angst mehr vor dem Deployment am Freitag. Keine verschobenen Updates mehr. Keine Vorfälle mehr, deren Ursache man wochenlang sucht.</p>
<p>Das Ziel ist einfacher, langweiliger Code, der tut, was er soll.</p>
<h2 id="die-serie">Die Serie</h2>
<p>Dieser Beitrag ist die Übersicht einer Serie. Jede Vertiefung greift einen Schritt aus der Geschichte auf und geht ein konkretes Refactoring durch:</p>
<ol>
<li>Legacy-Code ohne Angst refactoren – folgt bald</li>
<li>Implizite Konzepte explizit machen – folgt bald</li>
<li>Eine Entscheidung, ein Ort – folgt bald</li>
<li>Eine Verantwortung pro Komponente – folgt bald</li>
<li>Verantwortung in der richtigen Schicht – folgt bald</li>
</ol>]]></content><author><name>Manuel Holzrichter</name></author><category term="software-architecture" /><category term="legacy-code" /><category term="refactoring" /><category term="maintainability" /><category term="clean-code" /><summary type="html"><![CDATA[Nicht wartbarer Code scheitert nicht laut. Er macht jede Änderung langsamer, riskanter und beängstigender. Was er wirklich kostet und wie aus Legacy-Code wieder einfacher, langweiliger Code wird.]]></summary></entry><entry><title type="html">Warum ich der KI nie sage, was sie tun soll</title><link href="https://www.manuel-holzrichter.de/de/2026/04/26/warum-ich-der-ki-nie-sage-was-sie-tun-soll/" rel="alternate" type="text/html" title="Warum ich der KI nie sage, was sie tun soll" /><published>2026-04-26T08:00:00.000Z</published><updated>2026-04-26T08:00:00.000Z</updated><id>https://www.manuel-holzrichter.de/de/2026/04/26/warum-ich-der-ki-nie-sage-was-sie-tun-soll</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2026/04/26/warum-ich-der-ki-nie-sage-was-sie-tun-soll/"><![CDATA[<p>Vor ein paar Wochen hatte ich ein Problem, das ich zu verstehen glaubte. Ich hatte eine Lösung im Kopf, sauber und vollständig. Ich setzte mich hin, öffnete die KI und beschrieb die Aufgabe sorgfältig und im Detail. Eingaben, Ausgaben, Struktur, Randfälle. Ich bat sie, umzusetzen, was ich beschrieben hatte. Eine Minute später hatte ich eine selbstbewusste, gut strukturierte Antwort. Die Namen waren gut. Tests waren da. Die Form sah richtig aus.</p>
<p>Ich fing an zu lesen. Nach der Hälfte fiel mir etwas auf. Eine kleine Annahme der KI, völlig vernünftig, die still und leise einer Randbedingung widersprach, die ich nie aufgeschrieben hatte. Für mich war diese Randbedingung offensichtlich. Für niemanden sonst, und ich hatte sie nicht hingeschrieben. Ich versuchte, das Ergebnis zu flicken. Der Flicken machte das Nächste kaputt. Am Ende des Nachmittags hatte ich das meiste weggeworfen und von vorn angefangen.</p>
<p>Das Ergebnis war nicht das Problem. Das Problem war, dass ich der KI gesagt hatte, was sie tun soll, bevor ich geprüft hatte, ob wir uns einig waren, worin das Problem überhaupt besteht. Ich hatte den Teil übersprungen, in dem ich herausfinde, ob mein Gegenüber die Situation wirklich versteht. An diesem Nachmittag habe ich geändert, wie ich mit KI arbeite. Ich habe aufgehört, Anweisungen zu geben. Ich habe angefangen, Fragen zu stellen.</p>
<h2 id="die-umkehr">Die Umkehr</h2>
<p>Die Änderung ist im Moment klein und über eine Arbeitswoche groß. Statt mit „Mach X“ einzusteigen, beschreibe ich das Problem und frage die KI, welche Ansätze sie in Betracht ziehen würde. Ich lese die Antwort. Ich widerspreche, wo mein Kontext Widerspruch verlangt. Ich stelle Rückfragen zu den Teilen, denen ich noch nicht traue. Erst wenn wir uns über die Richtung einig sind, lasse ich sie sich auf etwas Größeres festlegen.</p>
<p>Die ersten Minuten wirken langsamer. Der Nachmittag wirkt schneller. Die Woche wirkt viel schneller. Gleiche Aufgabe, gleiches Werkzeug, anderer erster Zug.</p>
<h2 id="die-zwei-annahmen">Die zwei Annahmen</h2>
<p>Die Methode funktioniert nur, wenn du zwei Dinge gleichzeitig im Kopf behältst, und die meisten lassen eins davon fallen.</p>
<p>Erstens: <strong>Die KI weiß mehr als ich.</strong> Über die gesamte Breite der Softwareentwicklung hat sie mehr Code gelesen, mehr Muster gesehen und mehr Abwägungen kennengelernt, als mir in meinem ganzen Berufsleben begegnen werden. Bei jedem Thema, mit dem ich mich nicht ernsthaft beschäftigt habe, ist ihr Urteil wahrscheinlich breiter als meins.</p>
<p>Zweitens: <strong>Ich weiß mehr über meinen Kontext, als die KI je wissen wird.</strong> Die Randbedingungen, die ich nicht aufgeschrieben habe. Die Teamabsprachen, die in alten Chatverläufen stecken. Die historisch gewachsenen Entscheidungen, die seltsam aussehen, bis man weiß, wer sie getroffen hat und warum. Der Kunde, der auf eine bestimmte Formulierung schlecht reagiert. Nichts davon steckt in den Trainingsdaten des Modells, und nichts davon kommt hinein, solange ich es nicht selbst einbringe.</p>
<p>Beides stimmt fast immer gleichzeitig, und beides zieht in unterschiedliche Richtungen. Wer die KI wie eine Suchmaschine behandelt, verschenkt das Erste. Wer sie wie ein Orakel behandelt, ignoriert das Zweite. Das Arbeitsmuster, bei dem ich gelandet bin, ist darauf ausgelegt, beidem gerecht zu werden. Ich verlasse mich auf die KI bei dem, was sie weiß. Ich verlasse mich nicht auf sie bei dem, was nur ich weiß.</p>
<h2 id="fragen-zeigen-verständnis-anweisungen-setzen-es-voraus">Fragen zeigen Verständnis, Anweisungen setzen es voraus</h2>
<p>Der beste Vergleich, den ich dafür habe, ist die Arbeit mit einem Azubi. Wenn ich wissen will, was ein Azubi wirklich verstanden hat, gebe ich ihm keine Anweisung und schaue mir dann das Ergebnis an. Bis das Ergebnis vor mir liegt, ist das Signal verwaschen. Hat er es richtig gemacht, weil er es verstanden hat, oder weil die Anweisung so eng war, dass jeder sorgfältige Mensch dasselbe abgeliefert hätte? Hat er es falsch gemacht, weil er die Aufgabe missverstanden hat, oder weil er ein bestimmtes Wort missverstanden hat, das ich benutzt habe?</p>
<p>Also stelle ich stattdessen Fragen. Aus verschiedenen Richtungen. „Wie würdest du das angehen?“ „Worauf würdest du achten?“ „Wo siehst du das Risiko?“ Seine Antworten zeigen mir sehr schnell, wo die Lücke ist und welchen Kontext ich teilen muss, bevor er loslegt.</p>
<p>Bei einer KI ist es derselbe Zug, nur läuft die Asymmetrie andersherum. Ich prüfe nicht, ob die KI das Fachgebiet kennt. Ich prüfe, ob ihr Verständnis <em>meiner Situation</em> gut genug ist, dass ich mich gefahrlos auf ihr breiteres Wissen verlassen kann. Mit Fragen finde ich das heraus. Anweisungen überspringen diesen Schritt und hoffen das Beste. Hoffen ist keine Arbeitsmethode.</p>
<h2 id="richtung-vor-detail">Richtung vor Detail</h2>
<p>Der teuerste Fehler, den ich mit KI mache, ist keine falsche Antwort. Es ist, eine detaillierte Antwort zu prüfen, während die Richtung noch offen ist.</p>
<p>Wenn die Richtung feststeht, geht die Detailprüfung schnell. Du weißt, wonach du suchst. Du weißt, was da sein sollte und was nicht. Der Blick bleibt schnell an den falschen Dingen hängen, weil die richtigen eine klare Form haben.</p>
<p>Wenn die Richtung noch offen ist, ist die Detailprüfung eine Falle. Wichtige, prägende Entscheidungen verstecken sich in etwas, das wie Rauschen aussieht. Ein Standardwert, eine Formulierung, eine gewählte Abstraktion, ein stillschweigend übergangener Aspekt. Jede davon ist klein genug, um jemandem durchzurutschen, der gerade damit beschäftigt ist, ob die Oberfläche richtig aussieht. Und sobald eine davon durchrutscht, baut alles Weitere darauf auf. Wenn das Missverständnis ans Licht kommt, korrigierst du keine Zeile mehr. Du wirfst das Ergebnis weg.</p>
<p>Also lasse ich die KI nicht in die Tiefe gehen, bis die Richtung feststeht. Ich forme das übergeordnete Konzept mit Fragen. Ich prüfe, ob die KI dorthin steuert, wo ich hinwill. Ich korrigiere früh, solange Korrigieren billig und das Ergebnis noch klein ist. Erst dann werfe ich sie an. Eine Detailprüfung, nachdem die Richtung feststeht, ist etwas anderes als eine davor. Das Erste ist Verifikation. Das Zweite ist Archäologie.</p>
<h2 id="schlechte-ergebnisse-sind-ein-spiegel">Schlechte Ergebnisse sind ein Spiegel</h2>
<p>Am schwersten fiel es mir, meine Reaktion auf schlechte KI-Ergebnisse umzutrainieren. Früher war mein erster Impuls Frust über das Modell. Das Ergebnis war falsch, die KI hatte es erzeugt, die Ursache schien offensichtlich.</p>
<p>Sie war fast nie die Ursache.</p>
<p>Wenn die KI etwas sichtbar Danebenliegendes produziert, liegt es selten an fehlenden Fähigkeiten. Viel öfter ist es eine Kontextlücke auf meiner Seite. Ich habe die Situation schlecht beschrieben, weil ich sie selbst nicht gut genug verstanden hatte. Die ungeschriebene Randbedingung blieb ungeschrieben. Die Abwägung, die ich still in meinem Kopf entschieden hatte, landete nie im Prompt. Die KI hat dort, wo ich eine Lücke gelassen hatte, vernünftig geraten, und sie lag falsch, weil genau in dieser Lücke die wichtigste Information hätte stehen sollen.</p>
<p>Heute nutze ich schlechte KI-Ergebnisse zur Diagnose. Wenn ich aus der KI keine brauchbare Antwort herausbekomme, könnte ich mit ziemlicher Sicherheit auch selbst keine saubere Antwort liefern. Das Problem so gut zu beschreiben, dass die KI damit arbeiten kann, ist dieselbe Übung, wie das Problem so gut zu verstehen, dass man es lösen kann. Wenn diese Übung scheitert, ist das Scheitern eine Information. Es zeigt mir, worüber ich nachdenken muss.</p>
<h2 id="hör-auf-anzuweisen-fang-an-zu-fragen">Hör auf anzuweisen. Fang an zu fragen.</h2>
<p>Das Muster passt in einen Satz. Hör auf, der KI zu sagen, was sie tun soll. Fang an, sie zu fragen, was sie tun würde.</p>
<p>Was sich ändert, ist nicht die KI. Die KI ist vor und nach der Umstellung dieselbe. Was sich ändert, ist, welchen deiner beiden Vorteile du nutzt. Anweisungen stützen sich auf deinen Kontext und verschenken das Wissen der KI. Fragen stützen sich auf das Wissen der KI und zwingen dich, ehrlich mit deinem Kontext umzugehen. Die Ergebnisse werden besser. Und ganz nebenbei auch dein eigenes Denken – denn zu einem Problem, das du nicht verstehst, kannst du keine gute Frage stellen.</p>
<p>Wenn du dich das nächste Mal hinsetzt, um KI für echte Arbeit zu nutzen, widersteh dem Drang, Anweisungen zu geben. Beschreib das Problem. Frag, was sie tun würde. Lies die Antwort, wie du den ersten Entwurf eines Kollegen lesen würdest. Widersprich, wo dein Kontext Widerspruch verlangt. Einige dich mit ihr auf die Richtung, bevor du dich auf Details einigst.</p>
<p>Dann, erst dann, lass sie von der Leine.</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="ai" /><category term="productivity" /><category term="mindset" /><category term="software-development" /><category term="collaboration" /><summary type="html"><![CDATA[Hör auf, der KI Anweisungen zu geben. Wer zuerst das Problem beschreibt und Fragen stellt, bekommt bessere Ergebnisse – die KI kennt die Muster, du kennst deinen Kontext.]]></summary></entry><entry><title type="html">Fremdsysteme werden sich ändern – so bist du vorbereitet</title><link href="https://www.manuel-holzrichter.de/de/2026/03/29/fremdsysteme-werden-sich-aendern/" rel="alternate" type="text/html" title="Fremdsysteme werden sich ändern – so bist du vorbereitet" /><published>2026-03-29T08:00:00.000Z</published><updated>2026-03-29T08:00:00.000Z</updated><id>https://www.manuel-holzrichter.de/de/2026/03/29/fremdsysteme-werden-sich-aendern</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2026/03/29/fremdsysteme-werden-sich-aendern/"><![CDATA[<p>Freitagnachmittag, 16</p><div></div> Uhr. Ein Zahlungsanbieter spielt ein „kleines“ API-Update aus. Niemand bemerkt es. Am Montagmorgen kommen die ersten Kundenbeschwerden. Bestellungen scheitern im Checkout. Der Entwickler in Rufbereitschaft fängt an zu graben. Der Zahlungsanbieter hat die Struktur seiner Antwort geändert – ein verschachteltes Objekt, das vorher flach war, ein Feld, das von <code>transaction_id</code> in <code>txn_id</code> umbenannt wurde. Kleinkram. Aber die alte Struktur war in vierzehn Dateien über drei Services hinweg durchgesickert. Domänenlogik, Validierungsregeln, sogar E-Mail-Templates haben direkt auf die Feldnamen des Anbieters verwiesen. Der Fix hat drei Tage gedauert. Die Änderung selbst war einfach. Aber das Fremdsystem hatte sich überall ausgebreitet.<p></p>
<p>Ich habe dieses Muster öfter gesehen, als mir lieb ist. Ein Team bindet ein externes System an und nutzt dessen Datenstrukturen direkt im eigenen Domänencode. Alles funktioniert, bis sich das externe System ändert. Dann beginnt das Gerangel.</p>
<p>Der Fachbegriff dafür ist Kopplung. Aber ich glaube, es gibt eine nützlichere Sichtweise. Fremdsysteme sprechen eine andere Sprache als deine Anwendung. Wenn du diese Sprache in deine Domäne durchsickern lässt, verschmutzt du genau den Ort in deiner Codebasis, der glasklar sein sollte: das Modell deines Geschäfts.</p>
<h2 id="das-problem-wenn-fremde-konzepte-in-deine-domäne-eindringen">Das Problem: wenn fremde Konzepte in deine Domäne eindringen</h2>
<p>Jedes Fremdsystem bringt sein eigenes Vokabular mit. Ein Zahlungsanbieter spricht von <code>charges</code>, <code>intents</code> und <code>disputes</code>. Ein Versanddienst spricht von <code>parcels</code>, <code>carriers</code> und <code>tracking_events</code>. Deine Domäne muss vielleicht nur wissen, dass eine Bestellung bezahlt ist und ein Paket unterwegs ist.</p>
<p>Wenn du die Strukturen des Fremdsystems direkt in deinem Domänencode verwendest, wird deine Domäne unscharf. Entwickler, die den Code lesen, müssen nicht nur die Geschäftslogik verstehen, sondern auch das Vokabular jedes externen Systems, das ihr anbindet. Konzepte, die selbsterklärend sein sollten, werden mit fremden Begriffen zugemüllt. Das sind konzeptionelle Schulden, und sie wachsen, ohne dass es jemand merkt.</p>
<p>Dazu kommt: Änderungen im Fremdsystem ziehen sich durch deine ganze Codebasis. Ein umbenanntes Feld, eine umgebaute Antwort, ein abgekündigter Endpunkt – jede dieser Änderungen kann Anpassungen in Domänenlogik erzwingen, die mit der Entscheidung des externen Systems, sich weiterzuentwickeln, nichts zu tun hat. Und wenn tatsächlich etwas kaputtgeht, muss der Betrieb raten. War es der Zahlungsanbieter? Der Versanddienst? Welcher Endpunkt? Welches Feld? Ohne klare Grenzen wird Fehlersuche zur Archäologie.</p>
<h2 id="die-lösung-ports-adapter-und-wrapper">Die Lösung: Ports, Adapter und Wrapper</h2>
<p>Mein Ansatz stützt sich auf hexagonale Architektur, ergänzt sie aber um eine praktische Schicht für den Umgang mit Fremdsystemen im Entwicklungsalltag. Vier Bausteine:</p>
<p><strong>Port</strong>: ein Interface, das beschreibt, was deine Anwendung braucht, in der Sprache deiner Anwendung. Der Port weiß nichts über das Fremdsystem.</p>
<p><strong>Wrapper</strong>: eine kleine Klasse, die für genau eine Fähigkeit des Fremdsystems verantwortlich ist. Ein Endpunkt, ein Wrapper.</p>
<p><strong>Adapter</strong>: die Übersetzungsschicht. Er implementiert den Port, indem er Wrapper orchestriert und fremde Konzepte auf Domänenkonzepte abbildet.</p>
<p><strong>Smoke-Tests und ein APITester</strong>: Prüfwerkzeuge, die die Verbindung ehrlich halten.</p>
<p>Ich gehe jeden dieser Bausteine an einem konkreten Beispiel durch. Angenommen, deine Anwendung muss Zahlungen abwickeln, und du bindest einen Anbieter namens PayCorp an.</p>
<h2 id="ports-definiere-was-du-brauchst-nicht-was-sie-anbieten">Ports: definiere, was du brauchst, nicht was sie anbieten</h2>
<p>Der Port ist ein Vertrag, geschrieben aus der Perspektive deiner Anwendung. Er erwähnt PayCorp nicht. Er erwähnt ihre API nicht. Er beschreibt, was deine Domäne braucht.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">class</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> PaymentGateway</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">ABC</span><span style="color:#24292E;--shiki-dark:#E1E4E8">):</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    @abstractmethod</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> charge</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, amount: Money, reference: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) -> PaymentResult:</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    @abstractmethod</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> get_status</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, payment_id: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) -> PaymentStatus:</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    @abstractmethod</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> refund</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, payment_id: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) -> RefundResult:</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        ...</span></span></code></pre>
<p><code>Money</code>, <code>PaymentResult</code>, <code>PaymentStatus</code>, <code>RefundResult</code> – das sind alles Typen deiner Domäne. Deine Geschäftslogik hängt von diesem Interface ab und von nichts anderem. Wenn du PayCorp morgen gegen einen anderen Anbieter austauschst, ändert sich der Domänencode nicht. Nur der Adapter.</p>
<h2 id="wrapper-ein-endpunkt-eine-klasse">Wrapper: ein Endpunkt, eine Klasse</h2>
<p>Ein Wrapper macht genau eine Fähigkeit des Fremdsystems für die Codebasis verfügbar. Hat PayCorp einen Endpunkt <code>POST /v2/charges</code> und einen Endpunkt <code>GET /v2/charges/{id}</code>, bekommst du zwei Wrapper. Nicht einen. Zwei.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">class</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> CreatePayCorpCharge</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> __init__</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, http_client: HttpClient, config: PayCorpConfig):</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.http_client </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> http_client</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.config </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> config</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> execute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, payload: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">dict</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) -> </span><span style="color:#005CC5;--shiki-dark:#79B8FF">dict</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        response </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.http_client.post(</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">            f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.config.base_url</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/v2/charges"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">            json</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">payload,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">            headers</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF">self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.config.auth_headers,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        response.raise_for_status()</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> response.json()</span></span></code></pre>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">class</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> GetPayCorpCharge</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> __init__</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, http_client: HttpClient, config: PayCorpConfig):</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.http_client </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> http_client</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.config </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> config</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> execute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, charge_id: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) -> </span><span style="color:#005CC5;--shiki-dark:#79B8FF">dict</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        response </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.http_client.get(</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">            f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.config.base_url</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/v2/charges/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">charge_id</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">            headers</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF">self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.config.auth_headers,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        response.raise_for_status()</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> response.json()</span></span></code></pre>
<p>Warum fasst man die beiden nicht in einer einzigen Klasse <code>PayCorpClient</code> zusammen? Weil eine Komponente umso komplexer wird, je mehr Verantwortungen sie hat. Wir wollen einfachen, langweiligen, wartbaren Code. Jeder Wrapper macht genau eine Sache. Du kannst ihn in dreißig Sekunden lesen. Du kannst ihn isoliert testen. Du kannst ihn ersetzen, ohne irgendetwas anderes anzufassen. Wenn PayCorp den Endpunkt zum Anlegen von Charges ändert, passt du einen Wrapper an. Der Rest des Systems bekommt davon nicht einmal etwas mit.</p>
<p>Beim ersten Aufsetzen fühlt sich das vielleicht übertrieben an. Aber spätestens wenn ein Fremdsystem zum dritten Mal einen Endpunkt ändert und du das an einer einzigen, offensichtlichen Stelle behebst, willst du nicht mehr zurück.</p>
<h2 id="adapter-wo-zwei-welten-aufeinandertreffen">Adapter: wo zwei Welten aufeinandertreffen</h2>
<p>Im Adapter passiert die Übersetzung. Er implementiert deinen Port, nutzt intern Wrapper und bildet die Antworten des Fremdsystems auf deine Domänentypen ab.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">class</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> PayCorpPaymentAdapter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#6F42C1;--shiki-dark:#B392F0">PaymentGateway</span><span style="color:#24292E;--shiki-dark:#E1E4E8">):</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> __init__</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        self,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        create_charge: CreatePayCorpCharge,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        get_charge: GetPayCorpCharge,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    ):</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.create_charge </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> create_charge</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.get_charge </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> get_charge</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> charge</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, amount: Money, reference: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) -> PaymentResult:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        response </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.create_charge.execute({</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "amount_cents"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: amount.to_cents(),</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "currency"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: amount.currency,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "external_ref"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: reference,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        })</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> PaymentResult(</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">            payment_id</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">response[</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"txn_id"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">            status</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF">self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">._map_status(response[</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"state"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        )</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> get_status</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, payment_id: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) -> PaymentStatus:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        response </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.get_charge.execute(payment_id)</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">._map_status(response[</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"state"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">])</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> _map_status</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self, paycorp_state: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">str</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) -> PaymentStatus:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        mapping </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "COMPLETED"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: PaymentStatus.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">PAID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "PENDING"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: PaymentStatus.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">PROCESSING</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "FAILED"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: PaymentStatus.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">FAILED</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> mapping.get(</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">            paycorp_state, PaymentStatus.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">UNKNOWN</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        )</span></span></code></pre>
<p>Achte darauf, wo die fremden Konzepte leben. <code>txn_id</code>, <code>state</code>, <code>amount_cents</code>, <code>COMPLETED</code> – das gesamte Vokabular von PayCorp steckt in diesem Adapter. Die Domäne sieht es nie. Wenn PayCorp <code>txn_id</code> wieder in <code>transaction_id</code> umbenennt, änderst du eine Zeile in einer Methode eines Adapters. Dein Domänencode, deine Tests, deine Geschäftsregeln – nichts davon ist betroffen.</p>
<p>Das ist die Grenze. Auf der einen Seite die Welt von PayCorp. Auf der anderen deine. Der Adapter ist der einzige Ort, an dem sich beide treffen.</p>
<h2 id="smoke-tests-vertrauen-ist-gut-kontrolle-ist-besser">Smoke-Tests: Vertrauen ist gut, Kontrolle ist besser</h2>
<p>Jeder Wrapper bekommt einen Smoke-Test. Ein Smoke-Test ist eine winzige ausführbare Main. Sie baut die nötige Infrastruktur auf, ruft den Wrapper auf und gibt das Ergebnis aus. Mehr nicht.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> __name__</span><span style="color:#D73A49;--shiki-dark:#F97583"> ==</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "__main__"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    http_client </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> HttpClient()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    config </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> PayCorpConfig.from_env()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    wrapper </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> CreatePayCorpCharge(http_client, config)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    result </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> wrapper.execute({</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        "amount_cents"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">100</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        "currency"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"EUR"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        "external_ref"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"smoke-test-001"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    print</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#D73A49;--shiki-dark:#F97583">f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"txn_id: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">result.get(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'txn_id'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    print</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#D73A49;--shiki-dark:#F97583">f</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"state:  </span><span style="color:#005CC5;--shiki-dark:#79B8FF">{</span><span style="color:#24292E;--shiki-dark:#E1E4E8">result.get(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'state'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span><span style="color:#005CC5;--shiki-dark:#79B8FF">}</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<p>Du führst ihn aus, du siehst die Ausgabe, du weißt, ob es funktioniert. Genau darum geht es.</p>
<p>Warum manuelle Smoke-Tests statt automatisierter Integrationstests? Weil Integrationstests einen definierten Zustand brauchen. Du brauchst einen bekannten Ausgangspunkt, vorhersagbares Verhalten und konsistente Antworten. Bei Fremdsystemen hast du diesen Luxus selten. Sandbox-Umgebungen sind instabil. Testkonten laufen ab. Rate Limits kommen dazwischen. Stabile automatisierte Tests gegen ein System zu pflegen, das du nicht kontrollierst, ist ein Kampf, den du nicht gewinnst.</p>
<p>Smoke-Tests haben einen anderen Zweck. In der ersten Entwicklungsphase geben sie dir einen schnellen Weg zu prüfen, ob dein Wrapper wirklich korrekt mit dem Fremdsystem spricht. Wenn in Produktion etwas kaputtgeht, hast du ein Werkzeug, um das Problem nachzustellen und zu analysieren. Und wenn das Fremdsystem eine neue API-Version ankündigt, kannst du damit die Kompatibilität prüfen, bevor du deployst.</p>
<p>Sie sind ein Werkzeug für Entwickler, um Sicherheit zu gewinnen, kein Werkzeug für die CI-Pipeline, um Releases freizugeben oder zu blockieren.</p>
<h2 id="der-apitester-ein-sicherheitsnetz-für-den-betrieb">Der APITester: ein Sicherheitsnetz für den Betrieb</h2>
<p>Der APITester ist ein eigenes Thema, getrennt von den Smoke-Tests. Smoke-Tests sind ein Werkzeug für Entwickler, der APITester dagegen wird zusammen mit deiner Anwendung in Produktion deployt. Er orchestriert die Wrapper-Klassen direkt und prüft, ob die Endpunkte des Fremdsystems erreichbar und kompatibel sind.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">class</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> PayCorpAPITester</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> __init__</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        self,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        create_charge: CreatePayCorpCharge,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        get_charge: GetPayCorpCharge,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    ):</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.create_charge </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> create_charge</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.get_charge </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> get_charge</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    def</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> check_connectivity</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(self):</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.create_charge.execute({</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "amount_cents"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "currency"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"EUR"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "external_ref"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"connectivity-check"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        })</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        print</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"CreateCharge: OK"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.get_charge.execute(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"connectivity-check"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">        print</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"GetCharge: OK"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
<p>Stell das als CLI-Befehl namens <code>check-connectivity</code> bereit, und der Betrieb kann die Kompatibilität jederzeit selbst prüfen. Kein Raten, kein Wühlen in Logs, kein Warten darauf, dass die Entwickler aufwachen.</p>
<p>Angenommen, PayCorp spielt an einem Samstag eine inkompatible Änderung aus. Das Monitoring meldet steigende Fehlerraten. Der Techniker in Rufbereitschaft führt <code>check-connectivity</code> aus und sieht:</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>CreateCharge: FAILED — Missing txn_id in response</span></span>
<span class="line"><span>GetCharge:    OK</span></span></code></pre>
<p>Innerhalb von Sekunden weiß der Betrieb genau, welche Fähigkeit kaputt ist. Er kann mit präzisen Informationen eskalieren und prüfen, ob es einen Workaround gibt, ohne den Domänencode oder die Logik des Adapters verstehen zu müssen. Der APITester gibt ihm eine klare, ehrliche Antwort.</p>
<h2 id="zieh-die-grenze-am-adapter">Zieh die Grenze am Adapter</h2>
<p>Fremdsysteme werden sich ändern. Neue API-Versionen, umbenannte Felder, abgekündigte Endpunkte, geänderte Authentifizierung – all das wird passieren, und zwar nach dem Zeitplan von jemand anderem. Das Einzige, was du kontrollierst, ist, wie tief du es in deinen Code lässt, bevor du dich damit beschäftigen musst.</p>
<p>Wenn du das nächste Mal ein Fremdsystem anbindest, zieh die Grenze am Adapter. Lass die Wrapper die rohe Kommunikation übernehmen. Lass den Adapter übersetzen. Lass deine Domäne sauber. Und gib deinem Betriebsteam eine Möglichkeit zu prüfen, ob die Welt außerhalb deiner Anwendung noch die Sprache spricht, die du erwartest.</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="software-architecture" /><category term="hexagonal-architecture" /><category term="integration" /><category term="adapters" /><category term="clean-code" /><summary type="html"><![CDATA[Externe APIs ändern sich ohne Vorwarnung. Wie Adapter und hexagonale Architektur Fremdsysteme an der Grenze deiner Codebasis halten.]]></summary></entry><entry><title type="html">Warum dein Softwareprojekt hinter dem Zeitplan liegt (und über dem Budget)</title><link href="https://www.manuel-holzrichter.de/de/2026/03/06/warum-softwareprojekte-zu-spaet-und-zu-teuer-sind/" rel="alternate" type="text/html" title="Warum dein Softwareprojekt hinter dem Zeitplan liegt (und über dem Budget)" /><published>2026-03-06T11:00:00.000Z</published><updated>2026-03-06T11:00:00.000Z</updated><id>https://www.manuel-holzrichter.de/de/2026/03/06/warum-softwareprojekte-zu-spaet-und-zu-teuer-sind</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2026/03/06/warum-softwareprojekte-zu-spaet-und-zu-teuer-sind/"><![CDATA[<p>Mittwochnachmittag, drei Wochen vor der Deadline. Das Projekt ist vor sechs Monaten gestartet, mit einem aufgeräumten Backlog und einem Team, das ehrlich daran geglaubt hat, pünktlich zu liefern. Jetzt weicht im Statusmeeting jeder den Blicken der anderen aus. Die Demo letzte Woche hat ein grundlegendes Missverständnis bei einem zentralen Feature aufgedeckt. Die Hälfte der Arbeit aus dem Sprint muss nachgebessert werden. Die Restschätzung hat sich still und leise verdoppelt. Niemand ist überrascht. Aber kommen sehen hat es auch niemand.</p>
<p>Ich habe schon auf beiden Seiten dieses Tisches gesessen. Als Entwickler, der sich fragt, wie wir hier gelandet sind, und als Lead, der einem Stakeholder erklären muss, warum aus drei Monaten sechs geworden sind. Nach mehr als 15 Jahren Softwareentwicklung bin ich überzeugt: Die Antwort ist fast nie „wir waren faul“ oder „wir konnten nicht programmieren“. Die technische Arbeit bringt ein Projekt selten aus der Spur. Was es aus der Spur bringt, sind <strong>lange Feedback-Schleifen</strong>, <strong>ungeprüftes Vertrauen</strong> und <strong>Schätzungen nach Bauchgefühl</strong>.</p>
<p>Die <a href="https://opencommons.org/CHAOS_Report_on_IT_Project_Outcomes">Standish Group hat über 50.000 Technologieprojekte analysiert</a> und festgestellt, dass 66 % ganz oder teilweise scheitern. Nur 31 % werden pünktlich, im Budget und mit dem geplanten Umfang geliefert. Das sind schlechte Aussichten. Aber die Ursachen lassen sich überraschend gut beheben.</p>
<h2 id="die-wahren-kosten-langer-feedback-schleifen">Die wahren Kosten langer Feedback-Schleifen</h2>
<p>Ich habe einmal geschrieben, dass <a href="https://www.manuel-holzrichter.de/de/2024/01/17/die-rolle-des-iterierens/">Softwareentwicklung wie ein Spaziergang im Nebel ist</a>. Du siehst das Ziel nicht. Du machst kleine Schritte, prüfst, was du vorfindest, und entscheidest, in welche Richtung es weitergeht. Lange Feedback-Schleifen entstehen, wenn du damit aufhörst. Du läufst selbstbewusst Hunderte Meter, ohne dich umzusehen, und wenn sich der Nebel lichtet, merkst du, dass du die ganze Zeit in die falsche Richtung gelaufen bist.</p>
<p>Eine Feedback-Schleife ist die Zeit zwischen einer Entscheidung und dem Moment, in dem du erfährst, ob sie richtig war. Jeder Tag ohne Validierung ist ein Tag, an dem du vielleicht das Falsche baust. <a href="https://www.functionize.com/blog/the-cost-of-finding-bugs-later-in-the-sdlc">Die Forschung von IBM</a> hat das in Zahlen gefasst: Einen Bug zu beheben, der während der Implementierung gefunden wird, kostet ungefähr sechsmal so viel, wie ihn schon im Design zu erwischen. Je weiter sich ein Fehler von seinem Ursprung entfernt, desto teurer wird es, ihn rückgängig zu machen.</p>
<h2 id="erst-validieren-dann-bauen">Erst validieren, dann bauen</h2>
<p>Am Anfang meiner Laufbahn habe ich drei Wochen an einem aufwendigen Reporting-Modul gebaut. Schöne Diagramme. Konfigurierbare Zeiträume. PDF-Export. Das volle Programm. Als wir es dem Kunden gezeigt haben, hat er zehn Sekunden draufgestarrt und gesagt: „Das ist schön, aber wir brauchten eigentlich nur eine einzige Zahl auf einem Dashboard.“ Drei Wochen Arbeit für etwas, das an einem Tag erledigt gewesen wäre. Der Code war in Ordnung. Wir haben einfach das Falsche gebaut.</p>
<p>Dieses Muster sehe ich überall. Teams behandeln alle Einträge im Backlog als Bauaufgaben, obwohl viele davon eigentlich Validierungsaufgaben sind. Die Frage ist selten „Können wir das bauen?“. Sie lautet fast immer „Sollten wir das bauen, und passt unser Verständnis zur Realität?“. Bau die kleinstmögliche Version und stell sie einem echten Nutzer hin. Keinen Prototyp, den jemand stellvertretend abgenickt hat. Sondern das tatsächlich kleinste Ding, das deine riskanteste Annahme testet.</p>
<p><a href="https://digitaloctopusgroup.com/breaking-down-the-70-failure-rate-in-software-development/">Studien zeigen</a>, dass 70 % der gescheiterten Digitalisierungsprojekte auf Probleme mit den Anforderungen zurückgehen. Nicht weil die Leute nachlässig waren, sondern weil niemand die Anforderungen früh genug validiert hat.</p>
<p>Eine falsche Annahme zu korrigieren, auf der du monatelang aufgebaut hast, ist wie <a href="https://www.manuel-holzrichter.de/de/2025/05/05/die-rolle-des-schreibens/">das Fundament eines Hauses auszutauschen, während du schon die Dachziegel verlegst</a>. Nicht unmöglich. Aber Spaß macht es niemandem.</p>
<h2 id="missverständnisse">Missverständnisse</h2>
<p>Hier ist eins, das ein Team, mit dem ich gearbeitet habe, zwei komplette Sprints Nacharbeit gekostet hat. Der Stakeholder wollte „Benutzergruppen“. Die Entwickler haben „Berechtigungsmodell“ verstanden – Rollen, Zugriffskontrolle, Hierarchien. Gemeint hatte der Stakeholder aber eine Möglichkeit, Nutzer zu markieren, um ihnen gesammelt E-Mail-Benachrichtigungen zu schicken. Dieselben Worte. Komplett unterschiedliche Features. Aufgefallen ist es erst in der Demo.</p>
<p>Das ist kein Problem der Menschen. Es ist ein Problem des Prozesses. Wenn zwischen „das haben wir besprochen“ und „wir haben geprüft, was wir gemeint haben“ Wochen liegen, haben kleine Missverständnisse genug Zeit, zu großen Lücken in der Umsetzung zu werden.</p>
<p>Das <a href="https://www.pmi.org/learning/library/communication-method-content-in-project-9937">Project Management Institute</a> hat herausgefunden, dass schlechte Kommunikation bei 56 % der gescheiterten Projekte eine Rolle spielt. Projekte mit guter Kommunikation liefern doppelt so oft den geplanten Umfang. Der Unterschied ist nicht Talent. Es geht darum, wie oft und wie früh ihr die Schleife zwischen Annahme und Bestätigung schließt.</p>
<p>Verkürze also den Abstand zwischen Gespräch und Vorführung. Schreib auf, was du verstanden hast. Skizziere es. Bau an einem Nachmittag einen Wegwerf-Prototyp. Mach aus der abstrakten Einigung etwas Konkretes, das sich beide Seiten ansehen können, um dann zu sagen „ja, genau das meinte ich“ – oder besser noch „nein, nicht ganz“.</p>
<p>Ich habe über <a href="https://www.manuel-holzrichter.de/de/2025/05/05/die-rolle-des-schreibens/">die Rolle des Schreibens</a> beim Debuggen des eigenen Denkens geschrieben. Hier gilt dasselbe. Wenn du nicht in einfachen Worten aufschreiben kannst, was du baust, hast du es noch nicht verstanden.</p>
<h2 id="deine-testsuite-ist-eine-feedback-schleife">Deine Testsuite ist eine Feedback-Schleife</h2>
<p>Bei den Problemen oben sind andere Menschen beteiligt. Dieses hier liegt ganz bei uns.</p>
<p>Eine fehlende oder lückenhafte Testsuite ist die Feedback-Schleife, die wir selbst in der Hand haben – und die Teams am meisten vernachlässigen. Ohne automatisierte Tests kannst du nur herausfinden, ob deine Änderung etwas kaputt gemacht hat, indem du dich von Hand durch die Anwendung klickst. Das kostet Zeit. Also lässt du es bei „kleinen Änderungen“ weg. Dann macht eine kleine Änderung etwas drei Bildschirme weiter kaputt, und du erfährst es in der nächsten Demo. Oder in Produktion.</p>
<p>Tests sind nicht nur Qualitätsschranken. Sie sind Feedback-Schleifen im Schnelldurchlauf. Eine solide Testsuite sagt dir innerhalb von Sekunden, ob deine letzte Änderung zu allen Annahmen passt, auf denen das System gebaut wurde. Sekunden, nicht Wochen.</p>
<p><a href="https://codesuite.org/blogs/identifying-bugs-early-the-way-to-cutting-software-costs-by-50/">Untersuchungen zeigen</a>, dass frühes und kontinuierliches Testen rund 40 % der gesamten Entwicklungszeit spart. Tests zu schreiben ist nicht umsonst, aber gegen die Kosten, Probleme spät zu finden, ist dieser Aufwand nichts.</p>
<p>Zurück zum Nebel: Deine Testsuite ist der Boden unter deinen Füßen. Du siehst vielleicht nicht weit, aber du weißt zumindest, dass der Schritt, den du gerade gemacht hast, auf festem Grund stand.</p>
<h2 id="ungeprüftes-vertrauen">Ungeprüftes Vertrauen</h2>
<p>Wenn ein Stakeholder sagt „wir brauchen das“, liegt es nahe, der Aussage zu vertrauen und loszulegen. Vertrauen fühlt sich professionell an. Nachfragen fühlt sich an, als würde man im Weg stehen. Also landet das Feature im Backlog, wird geschätzt, gebaut, ausgeliefert. Und niemand nutzt es.</p>
<p>Vertrauen ist in Ordnung. <strong>Ungeprüftes</strong> Vertrauen ist das Problem. Jede Aussage darüber, was Nutzer brauchen, ist eine Annahme, bis sie durch Belege gestützt ist. Kunden beschreiben Lösungen statt Probleme. Kollegen geben Informationen durch ihre eigene Interpretation gefiltert weiter. Niemand lügt. Aber beide filtern die Realität durch ihre Perspektive. Der Entwickler, der hört „der Kunde will ein Benachrichtigungssystem“, weiß vielleicht nicht, dass der Kunde eigentlich gesagt hat „Ich verpasse manchmal wichtige Neuigkeiten“ – ein Problem, das sich auf ein Dutzend Arten lösen lässt, die meisten davon einfacher als ein Benachrichtigungssystem.</p>
<p><a href="https://www.mountaingoatsoftware.com/blog/are-64-of-features-really-rarely-or-never-used">Die Standish Group hat festgestellt</a>, dass 64 % der Software-Features selten oder nie genutzt werden. Nur 20 % liefern hohen Nutzen. Jedes dieser ungenutzten Features wurde irgendwann „gebraucht“. Jemand hat es gesagt, und das Team hat ihm vertraut.</p>
<p>Der <a href="https://www.researchgate.net/publication/235430372_Confirmation_Bias_in_Software_Development_and_Testing_An_Analysis_of_the_Effects_of_Company_Size_Experience_and_Reasoning_Skills">Bestätigungsfehler (Confirmation Bias)</a> macht es noch schlimmer. Sobald ein Team eine Behauptung als Tatsache akzeptiert hat, sucht es nach Belegen, die die Entscheidung stützen, und ignoriert Signale, die ihr widersprechen. Das Feature wächst im Umfang. Niemand fragt noch einmal: „Wissen wir eigentlich, dass Nutzer genau das brauchen?“ Die ursprüngliche Aussage ist zur Gewissheit erstarrt, ohne je getestet worden zu sein.</p>
<p>Behandle jedes „wir brauchen“ als Hypothese. Woher wissen wir das? Wer hat es gesagt? Können wir das Verhalten beobachten? Können wir es mit dem kleinstmöglichen Experiment testen, bevor wir Wochen an Entwicklungszeit investieren?</p>
<h2 id="bauchgefühl-statt-schätzung">Bauchgefühl statt Schätzung</h2>
<p>Lange Feedback-Schleifen erklären, warum Projekte vom Kurs abkommen. Aber bei der Schätzung werden viele Projekte schon zum Scheitern verurteilt, bevor die erste Zeile Code geschrieben ist.</p>
<p>Du sitzt in einem Planungsmeeting. Der Product Owner beschreibt ein Feature. Jemand sagt „wahrscheinlich eine Woche“. Jemand anderes sagt „eher drei Wochen“. Das Team einigt sich auf zwei, weil das in der Mitte liegt und sich vernünftig anfühlt. Niemand hat die Arbeit heruntergebrochen. Niemand hat die Unbekannten benannt. Die Schätzung ist ein sozialer Konsens, keine Analyse.</p>
<p>Daniel Kahneman und Amos Tversky haben das 1979 beschrieben und <a href="https://en.wikipedia.org/wiki/Planning_fallacy">Planungsfehlschluss (Planning Fallacy)</a> genannt. Wenn Menschen Aufgaben schätzen, spielen sie instinktiv den besten Fall durch. Alles läuft glatt. Keine Krankheitstage, keine überraschenden Abhängigkeiten, keine Anforderungen, die sich mitten im Sprint ändern. Sie schätzen danach, wie es laufen <em>könnte</em>, nicht danach, wie es normalerweise läuft.</p>
<p>Erste Projektschätzungen können um den Faktor vier schwanken. Selbst die besten Schätzmodelle in der Forschung haben einen mittleren Fehler von 39 %. Wir schätzen nicht schlecht, weil wir nachlässig sind. Wir schätzen schlecht, weil unser Gehirn darauf gepolt ist, bei den eigenen Plänen optimistisch zu sein.</p>
<p>Bevor du also eine Zahl nennst, bring Licht in die dunklen Ecken. Welche Teile hast du schon einmal gebaut? Welche Teile sind wirklich neu? Wo gibt es Abhängigkeiten, die du nicht kontrollierst? In den Unbekannten steckt das Risiko, und das Risiko macht aus einer Schätzung von zwei Wochen eine Realität von zwei Monaten.</p>
<p><a href="https://opencommons.org/CHAOS_Report_on_IT_Project_Outcomes">Daten der Standish Group</a> bestätigen das: Kleine Projekte sind in rund 90 % der Fälle erfolgreich. Große Projekte in weniger als 10 %. Der Unterschied liegt nicht nur in der Komplexität. Große Projekte haben mehr Unbekannte, mehr Annahmen und mehr Stellen, an denen sich optimistische Schätzungen gegenseitig aufschaukeln.</p>
<p>Kahneman hat als Gegenmittel die sogenannte Referenzklassenprognose vorgeschlagen. Statt „Wie lange wird das dauern?“ fragst du „Wie lange haben ähnliche Dinge in der Vergangenheit gedauert?“. Schau dir an, was dein Team tatsächlich geliefert hat, nicht seine optimistischen Schätzungen. Nicht aufregend. Aber es funktioniert.</p>
<h2 id="kleine-schritte-offene-augen">Kleine Schritte, offene Augen</h2>
<p>Erinnerst du dich an das Statusmeeting am Mittwochnachmittag? Jemand hätte es kommen sehen können – wenn die Feedback-Schleifen kürzer gewesen wären und die Schätzungen auf Belegen statt auf Hoffnung beruht hätten.</p>
<p>Lange Feedback-Schleifen lassen kleine Probleme zu großen werden. Ungeprüftes Vertrauen macht aus Meinungen wochenlang verschwendete Arbeit. Schätzungen nach Bauchgefühl machen den Zeitplan vom ersten Tag an zur Fiktion. Diese Muster erklären die meisten Terminüberschreitungen, die ich in 15 Jahren gesehen habe. Und die Antwort ist keine neue Methodik und kein besseres Tool. Sie heißt Disziplin. Validiere, bevor du baust. Teste kontinuierlich. Schätze auf Basis dessen, was tatsächlich passiert ist, nicht dessen, was du dir erhoffst.</p>
<p>Softwareentwicklung wird immer ein Spaziergang im Nebel bleiben. Aber wir können kleinere Schritte machen und öfter prüfen, ob wir festen Boden unter den Füßen haben. Die Projekte, die pünktlich fertig werden, sind nicht die mit perfekten Plänen. Es sind die, die gelernt haben, den Kurs zu korrigieren, bevor es zu spät war.</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="software-development" /><category term="project-management" /><category term="estimation" /><category term="communication" /><category term="feedback-loops" /><summary type="html"><![CDATA[Softwareprojekte scheitern selten an der technischen Arbeit. Sie scheitern an langen Feedback-Schleifen, ungeprüftem Vertrauen und Schätzungen aus dem Bauch heraus – alle drei lassen sich beheben.]]></summary></entry><entry><title type="html">Micro Frontends mit Import Maps: die leichtgewichtige Alternative zu Module Federation – Teil 2</title><link href="https://www.manuel-holzrichter.de/de/2026/02/28/vom-monolithen-zu-micro-frontends-teil-2/" rel="alternate" type="text/html" title="Micro Frontends mit Import Maps: die leichtgewichtige Alternative zu Module Federation – Teil 2" /><published>2026-02-28T11:00:00.000Z</published><updated>2026-02-28T11:00:00.000Z</updated><id>https://www.manuel-holzrichter.de/de/2026/02/28/vom-monolithen-zu-micro-frontends-teil-2</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2026/02/28/vom-monolithen-zu-micro-frontends-teil-2/"><![CDATA[<p>In Teil eins haben wir ein monolithisches Firmenportal gebaut, einen Shared Kernel extrahiert und das News-Modul als eigenständige SPA abgespalten. Das hat funktioniert. Jedes Team konnte unabhängig deployen. Aber jede Navigation von einer App zur anderen hat die ganze Seite neu geladen. Shell und Remote fühlten sich an wie zwei verschiedene Websites.</p>
<p>Jetzt gehen wir das schwierigere Problem an: ein Micro Frontend zur Laufzeit in die Host-Anwendung einbetten. Kein Neuladen der Seite. Keine iframes. Kein frameworkspezifischer Klebecode. Nur ein natives Browser-Feature namens Import Maps.</p>
<h2 id="import-maps-in-60-sekunden">Import Maps in 60 Sekunden</h2>
<p>Eine Import Map ist ein JSON-Block in einem <code>&#x3C;script type="importmap"></code>-Tag. Sie sagt dem Browser: Wenn im JavaScript-Code <code>import('remote-time')</code> steht, lade das Modul von dieser URL, statt es als relativen Pfad aufzulösen.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="html"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"importmap"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    "imports": {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      "remote-time": "http://localhost:9002/remote-time.js"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<p>Das ist der gesamte Mechanismus. Kein Bundler-Plugin. Kein Build-Schritt. Keine Laufzeitbibliothek. Der Browser liest die Map, und jeder Bare Module Specifier in deinem Code wird zu der URL aufgelöst, die du angegeben hast.</p>
<p>Die Browserunterstützung lag Ende 2025 weltweit bei etwa 94,5 %. Chrome, Edge, Firefox und Safari unterstützen Import Maps nativ. Die verbleibende Lücke sind ältere mobile Browser, die du bei Bedarf mit Polyfills wie <a href="https://github.com/guybedford/es-module-shims">es-module-shims</a> abdecken kannst.</p>
<p>Warum ist das wichtig? Import Maps sind ein <strong>Grundbaustein der Webplattform</strong>. Sie sind kein Framework, keine Bibliothek und kein Produkt eines Build-Tool-Herstellers. Sie sind Teil der HTML-Spezifikation. Kein Lock-in. Kein Laufzeit-Overhead. Der Browser erledigt die Arbeit.</p>
<h2 id="ein-micro-frontend-mit-mountunmount-bauen">Ein Micro Frontend mit Mount/Unmount bauen</h2>
<p>Der <a href="https://github.com/Krippke/monolith-to-micro-frontends/commit/41a232f">fünfte Commit</a> extrahiert das Zeiterfassungsmodul nach <code>remote-time/</code>. Anders als das News-Modul (das zu einer vollständigen SPA wurde) wird die Zeiterfassung aber als <strong>Bibliothek</strong> im Library Mode von Vite gebaut. Das Ergebnis ist ein einzelnes ES-Modul: <code>remote-time.js</code>.</p>
<p>Der Einstiegspunkt definiert den gesamten öffentlichen Vertrag – zwei Funktionen:</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> app </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> mount</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">el</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  app </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createApp</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(TimeTrackingPage)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(Quasar)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">mount</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(el)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> unmount</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (app) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">unmount</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    app </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p><code>mount</code> nimmt ein DOM-Element entgegen, erzeugt eine Vue-App und rendert sie hinein. <code>unmount</code> baut sie wieder ab. Der Orchestrator weiß nie, welches Framework das Remote verwendet. Er kennt nur den Vertrag.</p>
<p>Auf der Host-Seite ist <code>RemoteTimeHost.vue</code> ein dünner Wrapper:</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="vue"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">template</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">q-page</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> padding</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"containerRef"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">q-page</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">template</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> setup</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { ref, onMounted, onUnmounted } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'vue'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> containerRef</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> ref</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> unmountRemote </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">onMounted</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">mount</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">unmount</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#D73A49;--shiki-dark:#F97583"> import</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'remote-time'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  mount</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(containerRef.value)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  unmountRemote </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> unmount</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">onUnmounted</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (unmountRemote) </span><span style="color:#6F42C1;--shiki-dark:#B392F0">unmountRemote</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre>
<p>Wenn der Nutzer zu <code>/time</code> navigiert, mountet Vue <code>RemoteTimeHost</code>. Die Komponente importiert <code>remote-time</code> dynamisch (aufgelöst über die Import Map), ruft <code>mount</code> mit einem Container-Element auf und merkt sich die <code>unmount</code>-Funktion zum Aufräumen. Der Nutzer sieht, wie die Zeiterfassungsseite in der Shell des Orchestrators erscheint – gleiche Sidebar, gleicher Header, kein Neuladen.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>+--------------------------------------------------+</span></span>
<span class="line"><span>|              Orchestrator Shell                  |</span></span>
<span class="line"><span>|  (layout, navigation, routing)                   |</span></span>
<span class="line"><span>|                                                  |</span></span>
<span class="line"><span>|  /          -> Dashboard (local)                 |</span></span>
<span class="line"><span>|  /directory -> Employee Directory (local)        |</span></span>
<span class="line"><span>|  /time      -> RemoteTimeHost.vue                |</span></span>
<span class="line"><span>|                  |                               |</span></span>
<span class="line"><span>|                  | import('remote-time')         |</span></span>
<span class="line"><span>|                  |   resolved via import map     |</span></span>
<span class="line"><span>|                  v                               |</span></span>
<span class="line"><span>|         +------------------+                     |</span></span>
<span class="line"><span>|         | remote-time.js   |                     |</span></span>
<span class="line"><span>|         | mount(el)        |                     |</span></span>
<span class="line"><span>|         | unmount()        |                     |</span></span>
<span class="line"><span>|         +------------------+                     |</span></span>
<span class="line"><span>+--------------------------------------------------+</span></span></code></pre>
<p><strong>Der Knackpunkt ist Entwicklung gegenüber Produktion.</strong> In der Entwicklung zeigt die Import Map in <code>index.html</code> direkt auf <code>http://localhost:9002/remote-time.js</code> – den Vite-Dev-Server, der das Remote auf einem anderen Port ausführt. Der Dependency Optimizer von Vite versteht aber keine Import Maps. Er würde versuchen, <code>remote-time</code> zu bundeln, und scheitern. Deshalb nutzt der Orchestrator ein eigenes Vite-Plugin, das den Bare Specifier <code>remote-time</code> abfängt, ihn als extern markiert und stattdessen auf die URL des Dev-Servers zeigen lässt:</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'import-map-externals'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  enforce</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'pre'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  resolveId</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(source) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (source </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'remote-time'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">      return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { id: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'http://localhost:9002/src/main.js'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, external: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p>In der Produktion externalisiert Rollup den Specifier beim Build, und die native Import Map des Browsers übernimmt. Dieselbe <code>import('remote-time')</code>-Anweisung im Quellcode, zwei völlig unterschiedliche Auflösungsmechanismen, je nach Umgebung.</p>
<h2 id="produktiv-deployment-mit-docker-compose">Produktiv-Deployment mit Docker Compose</h2>
<p>Der <a href="https://github.com/Krippke/monolith-to-micro-frontends/commit/0a5bafb">sechste Commit</a> ergänzt ein vollständiges Produktions-Setup: vier Docker-Container, orchestriert von Docker Compose, mit einem nginx-Reverse-Proxy davor.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>                Browser :8080</span></span>
<span class="line"><span>                     |</span></span>
<span class="line"><span>               nginx proxy</span></span>
<span class="line"><span>                     |</span></span>
<span class="line"><span>        +------------+------------+</span></span>
<span class="line"><span>        |            |            |</span></span>
<span class="line"><span>        v            v            v</span></span>
<span class="line"><span>  orchestrator  remote-news  remote-time</span></span>
<span class="line"><span>     (:80)        (:80)        (:80)</span></span></code></pre>
<p>Jedes Micro Frontend hat ein eigenes Multi-Stage-Dockerfile: einen Node-22-alpine-Container zum Bauen und einen nginx-alpine-Container zum Ausliefern. Jedes lässt sich unabhängig bauen und deployen.</p>
<p>Der nginx-Reverse-Proxy übernimmt das Routing, und hier wird es subtil. Schau dir die <code>default.conf</code> an:</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="nginx"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">location</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#DBEDFF"> /time </span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    proxy_pass </span><span style="color:#24292E;--shiki-dark:#E1E4E8">http://orchestrator:80;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">location</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> /time/ </span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    proxy_pass </span><span style="color:#24292E;--shiki-dark:#E1E4E8">http://remote-time:80;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<p><code>/time</code> (exakter Treffer, ohne abschließenden Slash) geht an den <strong>Orchestrator</strong>. Warum? Weil <code>/time</code> eine Route im Vue Router des Orchestrators ist. Der Browser lädt die Shell des Orchestrators, die <code>RemoteTimeHost.vue</code> rendert, das wiederum das remote-time-Modul dynamisch importiert.</p>
<p><code>/time/</code> (Präfix-Treffer, mit abschließendem Slash) geht an den <strong>remote-time-Container</strong>. Warum? Weil die Import Map <code>remote-time</code> zu <code>/time/remote-time.js</code> auflöst. Dieser Request passt auf das Präfix <code>/time/</code> und landet bei dem Container, der die JavaScript-Datei tatsächlich ausliefert.</p>
<p>Ein Zeichen. Völlig anderes Verhalten. Das ist subtil, aber korrekt, und genau solche Details entscheiden darüber, ob ein Micro-Frontend-Deployment funktioniert oder scheitert.</p>
<p><strong>Noch etwas zur Produktion.</strong> Das Dockerfile des Orchestrators enthält diese Zeile:</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">sed</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> -i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 's|http://localhost:9002/remote-time.js|/time/remote-time.js|g'</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  orchestrator/dist/spa/index.html</span></span></code></pre>
<p>Sie schreibt die URL in der Import Map von der localhost-Adresse der Entwicklung auf den relativen Pfad der Produktion um. Für eine Demo reicht das. Aber es ist fragil – es bricht stillschweigend, wenn sich das HTML-Format ändert, und es skaliert schlecht auf mehrere Remotes. In einem echten System würdest du die Import Map beim Deployment über Umgebungsvariablen oder einen Konfigurations-Endpoint einspeisen.</p>
<h2 id="zwei-ansätze-im-vergleich">Zwei Ansätze im Vergleich</h2>
<p>Wir haben jetzt beide Micro-Frontend-Muster in Aktion gesehen. So schneiden sie im Vergleich ab:</p>













































<table><thead><tr><th></th><th>Separate SPA (News)</th><th>Import Map (Time)</th></tr></thead><tbody><tr><td>Navigation</td><td>Neuladen der ganzen Seite</td><td>Wie in einer SPA, nahtlos</td></tr><tr><td>Gemeinsame Shell</td><td>Nein (eigenes Layout)</td><td>Ja (Layout des Hosts)</td></tr><tr><td>Kopplung zur Laufzeit</td><td>Keine</td><td>mount/unmount</td></tr><tr><td>Unabhängiges Deployment</td><td>Vollständig</td><td>Vollständig</td></tr><tr><td>Nutzererlebnis</td><td>Zusammengestückelt</td><td>Nahtlos</td></tr><tr><td>Komplexität</td><td>Gering</td><td>Mittel</td></tr><tr><td>Fehlerisolation</td><td>Vollständig</td><td>Teilweise (gemeinsames DOM)</td></tr></tbody></table>
<p>Keiner der beiden Ansätze ist grundsätzlich besser. Die richtige Wahl hängt von der Beziehung zwischen Modul und Host ab.</p>
<p><strong>Separate SPAs</strong> passen am besten, wenn das Modul ein wirklich eigenständiger Ablauf ist. Der Nutzer wechselt beim Betreten gedanklich den Kontext. Denk an Einstellungen, einen Admin-Bereich oder ein Hilfe-Center. Das Neuladen der ganzen Seite ist hier akzeptabel, weil der Nutzer eine andere Umgebung erwartet.</p>
<p><strong>Einbetten per Import Map</strong> passt am besten, wenn das Modul ein Bereich innerhalb einer größeren Shell ist. Der Nutzer erwartet eine gemeinsame Navigation, ein einheitliches Layout und einen nahtlosen Ablauf. Denk an ein Dashboard-Widget, einen Tab in einem Portal oder einen Schritt in einem mehrstufigen Workflow.</p>
<h2 id="die-ehrliche-wahrheit-wann-du-auf-micro-frontends-verzichten-solltest">Die ehrliche Wahrheit: Wann du auf Micro Frontends verzichten solltest</h2>
<p>Hier ist eine Zahl, bei der du innehalten solltest. Die Verbreitung von Micro Frontends in der Branche ist zwischen dem Höhepunkt des Hypes und heute von 75,4 % auf 23,6 % gefallen. Das ist keine Geschichte des Scheiterns. Es ist eine gesunde Marktkorrektur. Teams haben das Muster begeistert ausprobiert, den Overhead bemerkt und sich auf einen pragmatischen Einsatz zurückgezogen.</p>
<p>Aus eigener Erfahrung sind die zwei größten Gewinne von Micro Frontends <strong>unabhängige Deploybarkeit</strong> und <strong>Teamautonomie</strong>. Wenn jedes Team nach eigenem Zeitplan ausliefern kann, ohne Releases abzustimmen, und wenn jedes Team seinen Stack von oben bis unten selbst verantwortet, dann ist der organisatorische Nutzen echt und greifbar.</p>
<p>Das gilt aber auch für die Kosten. <strong>Infrastruktur-Overhead</strong> ist der stille Killer. Nginx-Routingregeln, Docker-Orchestrierung, Verwaltung der Import Maps, Versionierung des Shared Kernels, CI/CD mit mehreren Pipelines – all das ist operativer Ballast, den ein Monolith schlicht nicht mit sich herumträgt. Und dann ist da noch die Falle der <strong>verfrühten Einführung</strong>: Teams greifen zu Micro Frontends, bevor sie überhaupt die Probleme haben, die das Muster löst. Wenn ihr ein einzelnes Team mit einer einzigen Deploy-Pipeline seid, fügt ihr Komplexität hinzu, ohne irgendetwas zu gewinnen.</p>
<p>Bevor du Micro Frontends einführst, beantworte diese Fragen ehrlich:</p>
<ol>
<li>Arbeitet mehr als ein Team am Frontend?</li>
<li>Blockieren sich diese Teams gegenseitig mit ihren Deploy-Zeitplänen?</li>
<li>Haben die Module wirklich klar getrennte fachliche Grenzen?</li>
<li>Seid ihr bereit, in die nötige Infrastruktur und operative Reife zu investieren?</li>
</ol>
<p>Wenn die Antwort auf eine dieser Fragen „nein“ lautet, fährst du mit einem gut strukturierten Monolithen mit klaren Modulgrenzen und einer gemeinsamen Komponentenbibliothek besser – bei einem Bruchteil der Komplexität.</p>
<p>Noch eine letzte Sache. Conway’s Law wirkt in beide Richtungen. Micro Frontends lösen keine organisatorischen Probleme. Sie zementieren sie. Wenn eure Teams keine klaren fachlichen Grenzen haben, entstehen diese Grenzen nicht dadurch, dass ihr das Frontend aufteilt – ihr verteilt die Verwirrung nur auf mehr Repositories.</p>
<h2 id="fazit">Fazit</h2>
<p>In diesen zwei Beiträgen sind wir den ganzen Weg gegangen: Monolith, Shared Kernel, separate SPA, Einbetten per Import Map, Produktiv-Deployment. Jeder Schritt im <a href="https://github.com/Krippke/monolith-to-micro-frontends">begleitenden Repository</a> ist ein einzelner Commit, den du auschecken und ausführen kannst.</p>
<p>Die wichtigste Erkenntnis: Micro Frontends sind zuerst ein organisatorisches Werkzeug und erst danach ein technisches. Die Architektur sollte der Teamstruktur folgen, nicht umgekehrt. Wenn eure Teams getrennte Domänen, unabhängige Release-Rhythmen und die operative Reife für verteilte Infrastruktur haben, bringen Micro Frontends echtes Tempo. Wenn nicht, ist ein sauberer Monolith kein Kompromiss – er ist die richtige Antwort.</p>
<p>Klon das Repo. Führ <code>docker compose up -d</code> aus. Schau dich um. Und bevor du zum Micro-Frontend-Hammer greifst, stell sicher, dass du wirklich einen Nagel vor dir hast.</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="micro-frontends" /><category term="import-maps" /><category term="module-federation" /><category term="runtime-composition" /><category term="micro-frontend-tutorial" /><category term="docker-deployment" /><summary type="html"><![CDATA[Micro Frontends zur Laufzeit mit Import Maps zusammensetzen – eine leichtgewichtige, browsernative Alternative zu Module Federation. Kein Neuladen der Seite, keine iframes.]]></summary></entry><entry><title type="html">Warum du immer falschliegen musst</title><link href="https://www.manuel-holzrichter.de/de/2026/02/19/warum-du-immer-falschliegen-musst/" rel="alternate" type="text/html" title="Warum du immer falschliegen musst" /><published>2026-02-19T11:00:00.000Z</published><updated>2026-02-19T11:00:00.000Z</updated><id>https://www.manuel-holzrichter.de/de/2026/02/19/warum-du-immer-falschliegen-musst</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2026/02/19/warum-du-immer-falschliegen-musst/"><![CDATA[<p>Vor zwölf Jahren hatte ich eine Aufgabe, an die ich bis heute regelmäßig denke. Wir mussten eine Reihe manueller, repetitiver Änderungen durch eine Codebasis ziehen. Nichts Besonderes. Jede Stelle finden, an der ein bestimmtes Muster vorkam, die Änderung anwenden, weiter. Die Feedback-Schleife war brutal. Fünfzehn bis zwanzig Minuten zwischen dem Deployment und dem Moment, in dem ich sah, ob ich etwas übersehen hatte. Also tat ich, was jeder sorgfältige Mensch tun würde. Ich ging den Code langsam und methodisch durch und prüfte jede gefundene Stelle dreifach. Ich war konzentriert und ich war mir sicher.</p>
<p>Ich deployte auf die Dev-Umgebung. Und da war es. Ich hatte Stellen übersehen. Keine obskuren, tief verschachtelten. Offensichtliche. Solche, bei denen ich auf den Bildschirm starrte und dachte: Wie konnte ich einfach daran vorbeilaufen?</p>
<p>Diese Frage hat mich länger beschäftigt als der Bug selbst. Ich hatte aufgepasst. Ich war nicht schlampig. Was war also passiert?</p>
<p>Nachdem ich eine Weile darüber nachgedacht hatte, wurde mir klar: Das Problem lag nicht in meinem Vorgehen. Es lag in meiner Annahme. Ich war davon ausgegangen, dass ich richtiglag. Ich hatte den Code nach einer Bestätigung abgesucht, dass ich alles erwischt hatte, statt aktiv nach dem zu suchen, was ich übersehen haben könnte. Mein Gehirn hatte schon entschieden, dass die Arbeit erledigt war. Es suchte nur noch nach der Erlaubnis, weiterzumachen.</p>
<p>An diesem Tag habe ich die Grundannahme geändert. Ich ging nicht mehr davon aus, dass ich richtiglag. Ich ging davon aus, dass ich falschlag. Und der Unterschied ist wie Tag und Nacht.</p>
<h2 id="die-falle-des-rechthabens">Die Falle des Rechthabens</h2>
<p>Für das, was mir passiert ist, gibt es einen Namen: <a href="https://en.wikipedia.org/wiki/Confirmation_bias">Bestätigungsfehler (Confirmation Bias)</a>.</p>
<p>1960 führte der Psychologe Peter Wason ein Experiment durch, das zu einem Grundpfeiler der Kognitionswissenschaft wurde. Er bat Teilnehmer, die Regel hinter einer Zahlenfolge herauszufinden. Ihre Hypothese konnten sie testen, indem sie neue Folgen vorschlugen. Was Wason herausfand, war aufschlussreich: Die Leute testeten fast ausschließlich Folgen, die ihre Vermutung bestätigen würden. Folgen, die sie hätten widerlegen können, probierten sie nicht aus. Dadurch wurden sie sich ihrer falschen Hypothesen immer sicherer.</p>
<p>Genau das macht dein Gehirn, wenn du davon ausgehst, dass du richtigliegst. Es hört auf, nach Belegen dafür zu suchen, dass du falschliegst. Es filtert selektiv. Es überspringt die Teile, die nicht passen. Dein Gehirn versucht, Energie zu sparen. Jede Information unvoreingenommen zu verarbeiten ist teuer, also nimmt es Abkürzungen. Dazu kommt: Falschliegen ist unangenehm. Es erzeugt kognitive Dissonanz und stellt dein Selbstbild infrage. Also weicht dein Gehirn dem aus.</p>
<p>Der <a href="https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect">Dunning-Kruger-Effekt</a> fügt eine weitere Ebene hinzu. Die Fähigkeiten, die du brauchst, um etwas gut zu machen, sind oft dieselben, die du brauchst, um zu beurteilen, ob du es gut gemacht hast. So entsteht ein blinder Fleck genau dort, wo du am dringendsten sehen müsstest. Du kannst nicht sehen, was du nicht sehen kannst.</p>
<p>Im großen Maßstab sind die Folgen hässlich. Ingenieure der NASA wussten <a href="https://en.wikipedia.org/wiki/Space_Shuttle_Challenger_disaster">neun Jahre lang</a> vor der Katastrophe vom O-Ring-Problem der Raumfähre Challenger. Die Annahme, dass der redundante zweite Ring sie sicher genug machte, wurde nie ernsthaft hinterfragt. Sieben Menschen starben. Die Führung von Boeing <a href="https://ethicsunwrapped.utexas.edu/engineering-ethics-and-the-boeing-scandal">kam zu dem Schluss, die 737 MAX sei sicher</a>, selbst nach zwei Abstürzen, bei denen 346 Menschen ums Leben kamen. Die Titanic wurde als unsinkbar beworben, bevor sie auf ihrer Jungfernfahrt sank.</p>
<p>Das sind extreme Beispiele. Aber der Mechanismus ist derselbe, der mich bei diesen Code-Änderungen erwischt hat. Selbstsicherheit ersetzte Überprüfung. Niemand bemerkte es, bis es zu spät war.</p>
<h2 id="dreh-die-grundannahme-um">Dreh die Grundannahme um</h2>
<p>Karl Popper hat eine ganze <a href="https://plato.stanford.edu/entries/popper/">Wissenschaftsphilosophie</a> auf der Idee aufgebaut, dass es bei der wissenschaftlichen Methode nicht darum geht, Theorien als richtig zu beweisen. Es geht darum, sie zu widerlegen. Eine Theorie hat nur dann Wert, wenn sie widerlegbar ist. Du sammelst keine Belege, die deine Schlussfolgerung stützen. Du suchst nach Belegen, die sie zerstören. Was diesen Prozess übersteht, dem kannst du vertrauen.</p>
<p>Piloten haben das schon vor langer Zeit verstanden. Sie nutzen <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC9246552/">Checklisten</a> nicht, weil sie die Schritte vergessen hätten. Sie nutzen sie, weil „Ich bin sicher, dass ich an alles gedacht habe“ genau die Art von Annahme ist, die Menschen das Leben kostet. Berührungskontrollen, gegenseitige Bestätigung durch beide Piloten und eine Fehlerkultur ohne Bestrafung ersetzten die alte Denkweise vom „unvermeidbaren menschlichen Versagen“. Die Unfallraten sanken deutlich.</p>
<p>Die Forschung zu <a href="https://greatergood.berkeley.edu/article/item/five_reasons_why_intellectual_humility_is_good_for_you">intellektueller Bescheidenheit</a> erzählt aus Sicht der Psychologie eine ähnliche Geschichte. Menschen, die anerkennen, dass sie falschliegen könnten, treffen bessere Entscheidungen, lernen schneller und können viel besser einschätzen, was sie wissen und was nicht. Das hat <a href="https://www.nature.com/articles/s44159-022-00081-9">nichts mit Intelligenz zu tun</a>. Es ist eine eigene, trainierbare Fähigkeit. Kluge Menschen sind genauso anfällig für Selbstüberschätzung wie alle anderen.</p>
<p>Gary Kleins <a href="https://en.wikipedia.org/wiki/Pre-mortem">Pre-Mortem-Technik</a> überträgt das auf die Teamebene. Bevor ein Projekt startet, stellt sich das Team vor, es sei bereits gescheitert. Dann arbeiten alle rückwärts, um herauszufinden, was schiefgelaufen ist. Indem Teams das Scheitern vorab annehmen, bringen sie Risiken ans Licht, die eine optimistische Planung begraben hätte. Das durchbricht Gruppendenken. Es erlaubt, Zweifel auszusprechen.</p>
<h2 id="validieren-bevor-du-umsetzt">Validieren, bevor du umsetzt</h2>
<p>Nach diesem Tag vor zwölf Jahren habe ich meine Arbeitsweise umgebaut. Die Änderung war klein, aber sie hat alles verändert: Bevor ich darüber nachdenke, wie ich eine Aufgabe erledige, überlege ich, wie ich das Ergebnis validiere.</p>
<p>Der Prüfschritt kommt zuerst. Nicht als nachträglicher Gedanke, nicht als Häkchen, das ich setze, wenn ich fertig bin. Er ist das Erste, was ich entwerfe. Wenn ich nicht herausfinde, wie ich überprüfen kann, dass das Ergebnis korrekt ist, habe ich die Aufgabe noch nicht vollständig verstanden.</p>
<p>Das hat mein ganzes Verhältnis zu Fehlern umgekehrt. Ich fürchte sie nicht mehr. Ich erwarte sie. Jedes Arbeitsergebnis behandle ich als schuldig, bis seine Unschuld bewiesen ist. Ich suche gezielt nach den Stellen, an denen ich Mist gebaut habe, weil ich weiß, dass es sie gibt. Es gibt sie immer.</p>
<p>Meine Kollegen beschreiben mich manchmal als jemanden, der fehlerfrei arbeitet. Darüber muss ich schmunzeln, denn das Geheimnis ist genau das Gegenteil. Ich gehe jedes einzelne Mal davon aus, dass ich irgendwo einen Fehler gemacht habe. Dann suche ich ihn, bevor es jemand anderes tut. Es geht nicht darum, Angst vor Fehlern zu haben. Es geht darum, zu akzeptieren, dass du schon einen gemacht hast, und ihn zu suchen.</p>
<p>Und ich mache immer noch Fehler. Ein paarmal im Jahr rutscht etwas durch. Ehrlich gesagt beruhigt mich das. Es beweist, dass das System nicht auf der Illusion übermenschlicher Beständigkeit beruht. Es verschiebt nur die Chancen deutlich zugunsten davon, Fehler früh zu finden.</p>
<p>Damit das klar ist: Das ist kein Selbstzweifel. Ich zweifle nicht daran, ob ich die Arbeit kann. Ich hinterfrage das Ergebnis. Selbstzweifel lähmt. Systematischer Zweifel ist produktiv. Der eine sagt: „Vielleicht kann ich das nicht.“ Der andere sagt: „Wahrscheinlich habe ich irgendwo einen Fehler gemacht, also suche ich ihn.“</p>
<h2 id="falschliegen-um-richtigzuliegen">Falschliegen, um richtigzuliegen</h2>
<p>Hier steckt ein kleines Paradox. Die Menschen, die davon ausgehen, dass sie falschliegen, liegen am Ende am häufigsten richtig. Sie überprüfen. Sie kontrollieren. Sie suchen den Fehler, statt zu hoffen, dass es ihn nicht gibt.</p>
<p>Der Wechsel der Denkweise passt in einen Satz. Hör auf zu fragen „Habe ich das richtig gemacht?“ und frag stattdessen „Wo habe ich Mist gebaut?“</p>
<p>Wenn du das nächste Mal eine Arbeit abschließt, widersteh dem Drang, gleich weiterzumachen. Nimm dir fünf Minuten und versuch, kaputtzumachen, was du gerade gebaut hast. Such den Grenzfall, an den du nicht gedacht hast. Lies die Anforderung noch einmal, die du verstanden zu haben glaubst. Hinterfrage die Annahme, die du auf Autopilot getroffen hast.</p>
<p>Dort liegt die echte Qualität. Nicht im Rechthaben. Sondern darin, dir selbst nachzuweisen, dass du falschliegst.</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="software-development" /><category term="mindset" /><category term="productivity" /><category term="quality" /><category term="career" /><summary type="html"><![CDATA[Geh standardmäßig davon aus, dass du falschliegst: warum eine fest eingebaute Überprüfung mehr bringt als Selbstsicherheit und wie kurze Feedback-Schleifen finden, was du selbst nicht siehst.]]></summary></entry><entry><title type="html">Vom Monolithen zu Micro Frontends: eine Schritt-für-Schritt-Anleitung – Teil 1</title><link href="https://www.manuel-holzrichter.de/de/2026/02/12/vom-monolithen-zu-micro-frontends-teil-1/" rel="alternate" type="text/html" title="Vom Monolithen zu Micro Frontends: eine Schritt-für-Schritt-Anleitung – Teil 1" /><published>2026-02-12T11:00:00.000Z</published><updated>2026-02-12T11:00:00.000Z</updated><id>https://www.manuel-holzrichter.de/de/2026/02/12/vom-monolithen-zu-micro-frontends-teil-1</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2026/02/12/vom-monolithen-zu-micro-frontends-teil-1/"><![CDATA[<p>Stell dir Folgendes vor. Du arbeitest in einem Team, das ein internes Firmenportal baut. Vier Features: News, Zeiterfassung, ein Mitarbeiterverzeichnis und ein Dashboard, das Daten aus allen dreien zusammenzieht. Drei Teams verantworten verschiedene Teile. Ein Repository. Jeder Pull Request ist ein Minenfeld. Du willst einen CSS-Fix für die News-Seite ausliefern, aber das Zeiterfassungs-Team hat gerade ein halbfertiges Feature gepusht und der Build ist rot. Deine Deployment-Pipeline braucht fünfzehn Minuten und alle stehen Schlange. Der Release-Tag fühlt sich an wie eine Geiselverhandlung.</p>
<p>Jetzt stell dir vor, jedes Team könnte seinen Teil des Portals unabhängig bauen, testen und deployen. Keine Abstimmung. Kein Warten. Keine gemeinsame Build-Warteschlange. Genau das versprechen Micro Frontends.</p>
<p>Das ist Teil eins einer zweiteiligen Serie. In diesem Beitrag bauen wir den Monolithen und gehen den ersten Schritt der Herauslösung: Wir spalten ein Modul in eine eigenständige Anwendung ab. In Teil zwei gehen wir tiefer, mit Komposition zur Laufzeit über browsernative Import Maps, Deployment in Produktion mit Docker und einem ehrlichen Blick darauf, wann sich dieser Ansatz die Komplexität nicht lohnt.</p>
<p>Alles in dieser Serie stützt sich auf ein <a href="https://github.com/Krippke/monolith-to-micro-frontends">begleitendes Repository</a>, in dem jeder Commit einen Architekturschritt abbildet. Klon es, schau dir die Commits an und mach mit.</p>
<h2 id="was-sind-micro-frontends-überhaupt">Was sind Micro Frontends überhaupt?</h2>
<p>Der Begriff „Micro Frontends“ tauchte zum ersten Mal im November 2016 im <a href="https://www.thoughtworks.com/radar/techniques/micro-frontends">ThoughtWorks Technology Radar</a> auf. Drei Jahre später veröffentlichte das Team von Martin Fowler den <a href="https://martinfowler.com/articles/micro-frontends.html">grundlegenden Artikel</a>, der dem Konzept seine maßgebliche Definition gab. Die Kernidee ist einfach: Man überträgt die Prinzipien von Microservices auf das Frontend. Statt einer monolithischen Frontend-Anwendung baust du unabhängig entwickelte, getestete und deployte UI-Module, die sich zu einem einzigen Produkt für den Nutzer zusammensetzen.</p>
<p>Stell es dir wie eine Zeitung vor. Sportteil, Wirtschaftsteil und Meinungsseite werden von getrennten Redaktionen geschrieben. Sie folgen gemeinsamen Gestaltungsrichtlinien – gleiche Schriften, gleiche Spaltenbreiten, gleicher Zeitungskopf. Aber jedes Ressort erscheint nach seinem eigenen Zeitplan. Der Leser sieht eine Zeitung.</p>
<p>Es gibt mehrere Wege, Micro Frontends zusammenzusetzen: Server-Side Includes, Integration zur Build-Zeit über npm-Pakete, iframes, Integration zur Laufzeit per JavaScript oder Web Components. Jeder hat seine Vor- und Nachteile. In dieser Serie konzentrieren wir uns auf zwei: <strong>getrennte SPAs mit nginx-Routing</strong> (der einfachste Ansatz) und <strong>Einbettung zur Laufzeit über Import Maps</strong> (der nahtloseste).</p>
<p>Bevor es um das Wie geht, ein Wort zum Warum. Conway’s Law besagt, dass Organisationen Systeme entwerfen, die ihre Kommunikationsstrukturen widerspiegeln. Micro Frontends sind da keine Ausnahme. Wenn deine Organisation drei Teams mit klar getrennten fachlichen Verantwortungen hat, lassen Micro Frontends jedes Team seinen Teil von Anfang bis Ende verantworten. Wenn deine Organisation aus einem einzigen Team besteht, sind Micro Frontends vielleicht eine Lösung auf der Suche nach einem Problem. Die Forschung von McKinsey bestätigt das: Das Betriebsmodell – wie Teams aufgestellt sind, wie Governance gelebt wird – zählt genauso viel wie die Wahl der Technologie. Architektur folgt den Menschen, nicht umgekehrt.</p>
<h2 id="zuerst-den-monolithen-bauen">Zuerst den Monolithen bauen</h2>
<p>Jedes gute Refactoring beginnt mit einem funktionierenden System. Der <a href="https://github.com/Krippke/monolith-to-micro-frontends/commit/88e84e6">erste</a> und der <a href="https://github.com/Krippke/monolith-to-micro-frontends/commit/622d68e">zweite</a> Commit im begleitenden Repository setzen das Acme Portal als klassischen Monolithen auf: eine einzige Vue-3-+-Quasar-+-Vite-Anwendung mit vier Seitenkomponenten, einer Vue-Router-Konfiguration und lokalen Datendateien, die eine Backend-API simulieren.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>+--------------------------------------------------+</span></span>
<span class="line"><span>|              Acme Portal (Monolith)              |</span></span>
<span class="line"><span>|                                                  |</span></span>
<span class="line"><span>|  +----------+  +------------+  +--------------+  |</span></span>
<span class="line"><span>|  |   News   |  |    Time    |  |  Directory   |  |</span></span>
<span class="line"><span>|  |          |  |  Tracking  |  |              |  |</span></span>
<span class="line"><span>|  +----------+  +------------+  +--------------+  |</span></span>
<span class="line"><span>|                                                  |</span></span>
<span class="line"><span>|  +--------------------------------------------+  |</span></span>
<span class="line"><span>|  |                Dashboard                   |  |</span></span>
<span class="line"><span>|  |   (aggregates News + Time + Directory)     |  |</span></span>
<span class="line"><span>|  +--------------------------------------------+  |</span></span>
<span class="line"><span>|                                                  |</span></span>
<span class="line"><span>|  Shared: employees.js, currentUser.js,           |</span></span>
<span class="line"><span>|          formatDate.js, portal.css               |</span></span>
<span class="line"><span>+--------------------------------------------------+</span></span>
<span class="line"><span>           Single build  |  Single deploy</span></span></code></pre>
<p>Das ist ein bewusster Monolith, kein versehentlicher. Alle vier Module leben unter einem Dach, weil wir absichtlich hier anfangen. Aber stell dir vor, was passiert, wenn drei Teams beginnen mitzuarbeiten: Merge-Konflikte vervielfachen sich, gekoppelte Deploy-Zyklen bedeuten, dass der Bug eines Teams alle blockiert, und der Build wird mit jedem Feature langsamer.</p>
<p>Genau diese Schmerzpunkte adressieren Micro Frontends. Nicht jedes Projekt erreicht diese Schwelle. Aber wenn es so weit ist, fühlt sich der Monolith immer weniger wie ein Fundament an und immer mehr wie ein Flaschenhals.</p>
<h2 id="schritt-eins-einen-shared-kernel-herauslösen">Schritt eins: einen Shared Kernel herauslösen</h2>
<p>Bevor wir irgendetwas aufteilen, müssen wir herausfinden, was wirklich von allen Modulen gemeinsam genutzt wird. Im Acme Portal sind das das Datenmodell für Mitarbeiter, der Kontext des aktuellen Nutzers, Hilfsfunktionen zur Datumsformatierung und das Basis-Stylesheet.</p>
<p>Der <a href="https://github.com/Krippke/monolith-to-micro-frontends/commit/5bdedd2">dritte Commit</a> verschiebt diese Teile nach <code>@acme/common</code>, ein Yarn-Workspace-Paket. Das Monorepo hat jetzt eine Root-<code>package.json</code>, die Workspaces für <code>common/</code>, <code>orchestrator/</code> und die später folgenden Remotes definiert. Jedes Micro Frontend hängt zur Build-Zeit von diesem Paket ab.</p>
<p>Der Barrel-Export in <code>common/src/index.js</code> legt die öffentliche API fest: employees, currentUser, formatDate und Navigationslinks. Alles, was fachspezifisch ist – News-Artikel, Zeiteinträge –, bleibt im jeweiligen Modul.</p>
<p>Eine zentrale Designentscheidung an dieser Stelle: Der Shared Kernel ist eine <strong>Abhängigkeit zur Build-Zeit</strong>, kein Service zur Laufzeit. Jedes Micro Frontend bündelt seine eigene Kopie. Das vermeidet Kopplung zur Laufzeit, auf Kosten von etwas Duplizierung. In einem produktiven System würdest du dieses Paket mit Semver versionieren und Breaking Changes über deinen normalen Release-Prozess steuern.</p>
<h2 id="der-ansatz-mit-getrennten-spas-news-herauslösen">Der Ansatz mit getrennten SPAs: News herauslösen</h2>
<p>Das einfachste Micro-Frontend-Muster ist, jedem Team seine eigene Single-Page-Application zu geben. Der <a href="https://github.com/Krippke/monolith-to-micro-frontends/commit/09b3ebf">vierte Commit</a> macht genau das: Das News-Modul wird zu <code>remote-news/</code>, einer eigenständigen Vue-+-Vite-App mit eigener <code>App.vue</code>, eigenem Router, eigenen Seiten und eigenen Daten.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>                    Browser</span></span>
<span class="line"><span>                       |</span></span>
<span class="line"><span>             +---------+---------+</span></span>
<span class="line"><span>             |                   |</span></span>
<span class="line"><span>        /news/*           everything else</span></span>
<span class="line"><span>             |                   |</span></span>
<span class="line"><span>  +----------------+  +------------------+</span></span>
<span class="line"><span>  |  remote-news   |  |   orchestrator   |</span></span>
<span class="line"><span>  |  (standalone   |  |   (Dashboard,    |</span></span>
<span class="line"><span>  |   SPA)         |  |    Time,         |</span></span>
<span class="line"><span>  |                |  |    Directory)    |</span></span>
<span class="line"><span>  +----------------+  +------------------+</span></span>
<span class="line"><span>             |                   |</span></span>
<span class="line"><span>             +---------+---------+</span></span>
<span class="line"><span>                       |</span></span>
<span class="line"><span>                @acme/common</span></span>
<span class="line"><span>           (build-time dependency)</span></span></code></pre>
<p>Die Navigation zwischen dem Orchestrator und der News-SPA läuft über einfache <code>&#x3C;a href></code>-Links. Ein Klick auf „News“ löst ein komplettes Neuladen der Seite aus. Die News-App rendert ihr eigenes Layout, ihre eigene Seitenleisten-Navigation, einfach alles selbst. Der einzige Integrationspunkt ist die URL.</p>
<p>Damit das funktioniert, führt der Orchestrator eine schlaue <code>NavigationLink</code>-Komponente ein. Sie prüft, ob eine Route zur aktuellen App gehört oder zu einer externen. Lokale Routen bekommen einen <code>&#x3C;router-link></code> für sofortige SPA-Navigation. Externe Routen bekommen einen <code>&#x3C;a href></code> für ein komplettes Neuladen der Seite. Gesteuert wird das über die Navigationskonfiguration in <code>@acme/common</code>, mit einem <code>app</code>-Feld an jedem Link.</p>
<p><strong>Die Vor- und Nachteile sind real.</strong> Auf der Habenseite: völlige Unabhängigkeit. Jede SPA kann bei Bedarf ein anderes Framework nutzen. Deploys sind vollständig entkoppelt. Ein Absturz in News betrifft den Rest des Portals nicht. Auf der Sollseite: komplettes Neuladen der Seite bei jeder Navigation zwischen Apps. Kein gemeinsamer Anwendungszustand zur Laufzeit. Jede SPA rendert das gesamte Layout selbst, also flackert die Seitenleiste bei jedem Übergang. Die User Experience wirkt zusammengeflickt statt nahtlos.</p>
<p>Noch etwas ist erwähnenswert: <code>news.js</code> existiert jetzt sowohl im Orchestrator (für die Zusammenfassungs-Widgets im Dashboard) als auch in remote-news (für die vollständigen News-Seiten). Das Dashboard braucht zusammengefasste Daten, kann aber zur Laufzeit nichts aus einem Remote importieren. In einem echten System würden beide eine gemeinsame API aufrufen. In dieser Demo ist die Duplizierung beabsichtigt – und sie ist eine Design-Einschränkung, der du in der Praxis begegnen wirst.</p>
<h2 id="was-wir-bisher-haben--und-was-noch-fehlt">Was wir bisher haben – und was noch fehlt</h2>
<p>Fassen wir zusammen. Wir haben mit einem Monolithen angefangen, gemeinsamen Code in ein Common-Paket herausgelöst und das News-Modul in eine eigene, unabhängig deployte SPA abgespalten. Der Ansatz mit getrennten SPAs löst das Problem der Deploy-Unabhängigkeit. Jedes Team kann nach seinem eigenen Zeitplan ausliefern. Aber er reißt eine Lücke in die User Experience. Jede Navigation zwischen Apps lädt die Seite komplett neu. Es gibt keinen gemeinsamen Zustand zur Laufzeit. Shell und Remote fühlen sich an wie zwei verschiedene Websites, die dasselbe CSS tragen.</p>
<p>Was wäre, wenn wir ein Micro Frontend direkt in die Shell des Orchestrators einbetten könnten? Dynamisch laden. In einen DOM-Container mounten. Wieder unmounten, wenn der Nutzer weiternavigiert. Alles ohne iframes, ohne Bündelung zur Build-Zeit, nur mit Funktionen, die jeder moderne Browser von Haus aus mitbringt.</p>
<p>Genau das bauen wir in Teil zwei: Import Maps, Komposition zur Laufzeit, Deployment mit Docker und das ehrliche Gespräch darüber, wann du Micro Frontends überhaupt nicht einsetzen solltest.</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="micro-frontends" /><category term="frontend-architecture" /><category term="monolith-migration" /><category term="independent-deployment" /><category term="micro-frontend-tutorial" /><category term="conways-law" /><summary type="html"><![CDATA[Eine Schritt-für-Schritt-Anleitung für die Migration eines Frontend-Monolithen zu Micro Frontends: einen Shared Kernel herauslösen und dann das erste Modul in eine eigenständige SPA aufteilen.]]></summary></entry><entry><title type="html">Warum jeder Entwickler eine eigene Website braucht</title><link href="https://www.manuel-holzrichter.de/de/2026/02/08/warum-jeder-entwickler-eine-eigene-website-braucht/" rel="alternate" type="text/html" title="Warum jeder Entwickler eine eigene Website braucht" /><published>2026-02-08T11:00:00.000Z</published><updated>2026-02-08T21:52:58.000Z</updated><id>https://www.manuel-holzrichter.de/de/2026/02/08/warum-jeder-entwickler-eine-eigene-website-braucht</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2026/02/08/warum-jeder-entwickler-eine-eigene-website-braucht/"><![CDATA[<p>Stell dir folgende Szene vor. Du hast gerade drei Wochen lang eine elegante eventgetriebene Architektur gebaut. Saubere Trennung der Zuständigkeiten, ordentliche Domänengrenzen, das volle Programm. Jetzt bittet dich dein Product Owner, das Ganze einem Stakeholder zu erklären, der in seinem Leben noch keine Zeile Code geschrieben hat. Du machst den Mund auf und … heraus kommt etwas wie ein Stacktrace. Technisch. Dicht. Bedeutungslos für jeden außerhalb deiner Blase.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>  +------------------+                  +------------------+</span></span>
<span class="line"><span>  |   if (event) {   |                  |                  |</span></span>
<span class="line"><span>  |     publish(msg) |  -- explain -->  |    ... what?     |</span></span>
<span class="line"><span>  |     await sub()  |                  |                  |</span></span>
<span class="line"><span>  +------------------+                  +------------------+</span></span>
<span class="line"><span>       Developer                           Stakeholder</span></span></code></pre>
<p>Das kenne ich. Mehr als einmal. Und die unbequeme Wahrheit, die ich nach über 15 Jahren Softwareentwicklung akzeptieren musste, lautet: <strong>Der Code war nie der schwierige Teil. Die Kommunikation war es.</strong></p>
<h2 id="die-fähigkeit-die-dir-kein-tutorial-beibringt">Die Fähigkeit, die dir kein Tutorial beibringt</h2>
<p>Als Entwickler verbringen wir Tausende Stunden damit, unsere technischen Fähigkeiten zu schärfen. Neue Frameworks, Entwurfsmuster, Architekturprinzipien. Wir lernen das alles. Aber die Fähigkeit, die tatsächlich darüber entscheidet, ob unsere Projekte gelingen? Die Kommunikation mit den Menschen, die keine Entwickler sind.</p>
<p>Fachexperten. Stakeholder. Endanwender. Die Menschen, die wissen, <em>was</em> gebaut werden muss, aber keine Ahnung haben, <em>wie</em>. Unser Job ist nicht nur, Code zu schreiben. Er besteht darin, zwischen ihrer Welt und unserer zu übersetzen. Und dieses Übersetzen braucht Übung.</p>
<p>Etwas, das ich gern früher gelernt hätte: Sich in die Lage des Zuhörers zu versetzen, ist ein Muskel. Er wird schwächer, wenn du ihn nicht benutzt. Und auf einer eigenen Website zu bloggen, ist eine der wirksamsten Methoden, ihn zu trainieren.</p>
<h2 id="warum-bloggen-verkapptes-kommunikationstraining-ist">Warum Bloggen verkapptes Kommunikationstraining ist</h2>
<p>Wenn du einen Blogartikel schreibst, zwingt dich das zu etwas Bemerkenswertem: Du musst ein Konzept, das als Intuition in deinem Kopf lebt, in einen strukturierten, verständlichen Text verwandeln. Du musst die richtige Abstraktionsebene wählen. Du musst vorhersehen, was dein Leser nicht weiß. Du musst Analogien finden, die die Lücke zwischen deinem Fachwissen und seinem Verständnis überbrücken.</p>
<p>Genau <em>das</em> passiert in einem Meeting mit einem Stakeholder. Nur dass du in einem Blogartikel iterieren kannst. Du kannst einen Absatz fünfmal umschreiben, bis er sitzt. Du kannst ihn über Nacht liegen lassen und merken, dass deine Erklärung einen blinden Fleck hatte. In einem Live-Gespräch geht das nicht, aber die Übung überträgt sich.</p>
<p>Jeder Artikel, den du veröffentlichst, ist ein Probelauf für die Gespräche, auf die es wirklich ankommt. Die, in denen ein Missverständnis Wochen an Nacharbeit kostet und in denen eine gut gewählte Metapher ein Projekt davor bewahrt, aus dem Ruder zu laufen.</p>
<p>Ich habe schon über <a href="https://www.manuel-holzrichter.de/de/2025/05/05/die-rolle-des-schreibens/">die Rolle des Schreibens</a> geschrieben, darüber, wie Schreiben dein Denken debuggt. Eine eigene Website nimmt dieses Prinzip und macht daraus eine Gewohnheit. Keine einmalige Übung, sondern eine kontinuierliche Praxis der Klarheit.</p>
<h2 id="im-zeitalter-der-ki-ist-kommunikation-dein-multiplikator">Im Zeitalter der KI ist Kommunikation dein Multiplikator</h2>
<p>Jetzt wird es interessant. Wir leben in einer Zeit, in der KI Code generieren, Dokumentation schreiben und ganze Anwendungen aufsetzen kann. Der Engpass hat sich verschoben. Es geht nicht mehr um <em>Tippgeschwindigkeit</em> oder <em>Syntaxwissen</em>. Es geht darum, wie klar du ausdrücken kannst, was du willst.</p>
<p>Prompt Engineering – die Fähigkeit, über die alle reden – ist eigentlich nur Kommunikationsfähigkeit, angewandt auf Maschinen. Die Entwickler, die mit KI-Werkzeugen die besten Ergebnisse erzielen, sind diejenigen, die ihre Absicht klar ausdrücken können. Sie wissen, wie man Kontext liefert, Rahmenbedingungen festlegt und gewünschte Ergebnisse klar beschreibt. Kommt dir das bekannt vor? Es ist dieselbe Fähigkeit, die du bei Stakeholdern brauchst, nur auf ein anderes Publikum gerichtet.</p>
<p>Klare Kommunikation ist zu einem Multiplikator für alles geworden, was du tust. Sie macht deine Meetings produktiver, deine Dokumentation nützlicher, deine Arbeit mit KI effektiver und deine Code Reviews konstruktiver. Eine eigene Website, auf der du diese Fähigkeit regelmäßig übst, ist kein Nice-to-have mehr. Sie ist ein Wettbewerbsvorteil.</p>
<h2 id="mit-github-pages-alle-hürden-beseitigen">Mit GitHub Pages alle Hürden beseitigen</h2>
<p>Ich weiß, was du jetzt denkst. „Ich hatte mal eine Website. Dann musste ich WordPress aktualisieren, mein SSL-Zertifikat erneuern, meine Datenbank migrieren und eine Sicherheitslücke patchen. Als ich damit fertig war, hatte ich null Motivation mehr, tatsächlich etwas zu schreiben.“</p>
<p>Verstehe ich. Klassisches Webhosting ist eine Wartungssteuer, die jede Motivation killt. Deshalb betreibe ich diese Seite auf GitHub Pages – und deshalb halte ich es für das perfekte Setup für Entwickler.</p>
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>  +-----------------------------------------------+</span></span>
<span class="line"><span>  |  ~ Terminal                            _ [] x |</span></span>
<span class="line"><span>  +-----------------------------------------------+</span></span>
<span class="line"><span>  |                                               |</span></span>
<span class="line"><span>  |  $ vim why-caching-matters.md                 |</span></span>
<span class="line"><span>  |  $ git add .                                  |</span></span>
<span class="line"><span>  |  $ git commit -m "New post"                   |</span></span>
<span class="line"><span>  |  $ git push                                   |</span></span>
<span class="line"><span>  |                                               |</span></span>
<span class="line"><span>  |  remote: Your site is published.              |</span></span>
<span class="line"><span>  |                                               |</span></span>
<span class="line"><span>  +-----------------------------------------------+</span></span></code></pre>
<p>Die Developer Experience ist denkbar einfach: Markdown-Datei schreiben, <code>git push</code>, fertig. GitHub kümmert sich um den Build, das Hosting, SSL, um alles. Keine Server zum Patchen. Keine Datenbanken zum Sichern. Keine Hosting-Rechnungen. Null Wartung. Deine Inhalte liegen in einem Git-Repository – versioniert, portabel und für immer deine.</p>
<p>Es ist derselbe Workflow, den du ohnehin jeden Tag nutzt. Kein Kontextwechsel, keine neuen Werkzeuge zu lernen. Nur dein Editor, dein Terminal und deine Gedanken. Das beste Werkzeug ist das, das du tatsächlich benutzt, und GitHub Pages nimmt dir jede Ausrede.</p>
<h2 id="fang-an-zu-schreiben-statt-dich-zu-präsentieren">Fang an zu schreiben, statt dich zu präsentieren</h2>
<p>Hier ist meine Herausforderung an dich: Bau deine eigene Website nicht als polierte Portfolio-Visitenkarte. Verbring nicht Wochen damit, am CSS zu feilen oder das perfekte Farbschema auszusuchen. Das ist Prokrastination, die sich als Produktivität verkleidet.</p>
<p>Fang stattdessen an zu schreiben. Such dir ein Thema aus, das du letzte Woche jemandem erklärt hast – eine Designentscheidung, eine Debugging-Strategie, eine Lektion, die du gelernt hast. Schreib es so auf, als würdest du es einem klugen Kollegen erklären, der in einer anderen Domäne arbeitet. Veröffentliche es. Und dann mach es nächsten Monat wieder.</p>
<p>Nach ein paar Artikeln wirst du etwas bemerken: Deine Fähigkeit, komplexe Ideen zu vermitteln, wird besser. Nicht nur beim Schreiben, sondern auch in Meetings, in Code Reviews, in jedem Gespräch, in dem du dich verständlich machen musst. Die eigene Website ist nur der Trainingsplatz. Der eigentliche Gewinn zeigt sich überall sonst.</p>
<p>Deine zukünftigen Stakeholder werden es dir danken. Und deine KI-Werkzeuge auch.</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="personal-website" /><category term="software-development" /><category term="communication" /><category term="github-pages" /><category term="writing" /><category term="ai" /><summary type="html"><![CDATA[Warum jeder Entwickler eine eigene Website bauen sollte: Du übst, technische Arbeit für Nicht-Techniker zu erklären, behältst deine Inhalte und bringst deine Karriere voran.]]></summary></entry><entry><title type="html">Die Rolle des Schreibens</title><link href="https://www.manuel-holzrichter.de/de/2025/05/05/die-rolle-des-schreibens/" rel="alternate" type="text/html" title="Die Rolle des Schreibens" /><published>2025-05-05T12:48:49.000Z</published><updated>2025-05-05T18:13:47.000Z</updated><id>https://www.manuel-holzrichter.de/de/2025/05/05/die-rolle-des-schreibens</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2025/05/05/die-rolle-des-schreibens/"><![CDATA[<p>Hallo liebe Coder und Tastatur-Ninjas!</p>
<p>Wir kennen das alle, oder? Mittendrin im Code-Gefecht, die fünfte Tasse Kaffee wirkt, und der Bildschirm ist ein hypnotisches Kaleidoskop aus Klammern und Semikolons. Wir <em>lieben</em> es, Probleme mit Code zu lösen. So sehr, dass wir manchmal … na ja, alles andere vergessen. Vor allem das Schreiben.</p>
<img src="/assets/images/typewriter.jpg">
<p>Lange Zeit fühlte sich Schreiben an wie diese nervige Linter-Warnung, die man einfach mit <code>// eslint-disable-next-line</code> wegdrücken will. „Warum soll ich aufschreiben, was ich tue? Der Code ist die ultimative Wahrheit! Kommentare sind was für Anfänger, und Doku liest sowieso keiner!“ Kommt dir bekannt vor? Ich war dem „Code dokumentiert sich selbst“-Kult voll und ganz verfallen. Spoiler: Tut er nicht. Und das hat mich Zeit gekostet. Viel Zeit.</p>
<h2 id="der-bug-im-denkprozess-wenn-die-formulierung-hakt">Der Bug im Denkprozess: Wenn die Formulierung hakt</h2>
<p>Mir ist etwas aufgefallen: Immer wenn ich versucht habe, eine Idee oder ein Konzept in Worte zu fassen (auch nur für mich selbst), und dabei ins Stolpern kam, lag das nicht nur an fehlendem Vokabular. Es war ein Bug-Report für meinen eigenen Denkprozess! Eine holprige Formulierung, ein Satz, der sich wie Spaghetti-Code anfühlte – das war ein klares Zeichen: <strong>Der Gedanke selbst war noch nicht fertig kompiliert.</strong></p>
<p>Dieses Ringen um die richtigen Worte zwingt dich, die Idee zu debuggen. Du musst Variablen (Begriffe) klären, Funktionen (Zusammenhänge) definieren und die Architektur (die Struktur des Gedankens) überdenken. So lange, bis es sich „richtig“ anfühlt. Und siehe da: Wenn du eine Idee klar und verständlich <em>aufschreiben</em> kannst, stehen die Chancen verdammt gut, dass sie konzeptionell tragfähig ist. Das ist wie ein grüner Unit-Test für dein Gehirn, <em>bevor</em> du auch nur eine einzige Zeile Code committest.</p>
<h2 id="vom-code-first-junkie-zum-konzept-schreiber-eine-bekehrungsgeschichte">Vom Code-First-Junkie zum Konzept-Schreiber (eine Bekehrungsgeschichte)</h2>
<p>Mein alter Workflow: Problem -> Koffein -> vage Idee im Kopf -> Hände auf die Tastatur -> draufloshämmern, bis es (angeblich) funktioniert. Das Problem? Oft habe ich erst gemerkt, dass meine grundlegende Annahme falsch war, als ich schon tief in der Dependency-Hölle steckte und versuchte, das letzte obskure Edge-Case-Monster zu bändigen. Solche konzeptionellen Fehler spät im Prozess zu finden und zu beheben, fühlte sich an, als wolle man das Fundament eines Hauses austauschen, während man schon die Dachziegel verlegt. Zeitaufwand: enorm. Frustlevel: <code>Integer.MAX_VALUE</code>.</p>
<p>Heute mache ich das anders. Problem -> Koffein -> <strong>Konzept in Textform!</strong> Ich versuche, die Lösung zuerst auf einer halben Seite Fließtext zu skizzieren. Und genau hier passiert die Magie: Wo kann ich etwas nicht klar formulieren? <em>Das</em> sind die Schwachstellen. Ich iteriere über diesen Text und feile an den Formulierungen, bis er sich logisch und vollständig liest. Eine halbe Seite Text refactorst du in wenigen Minuten. Eine komplexe Codebasis? Frag lieber nicht …</p>
<h2 id="llms-mein-neuer-pair-programming-partner-der-schnell-tippen-kann">LLMs: Mein neuer Pair-Programming-Partner (der schnell tippen kann)</h2>
<p>Dieses „Write-First“-Prinzip hat sich als unbezahlbar erwiesen, besonders im Umgang mit unseren neuen Freunden, den Large Language Models. LLMs sind keine magischen Kristallkugeln, die Code aus dem Nichts zaubern (auch wenn es sich manchmal so anfühlt). Sie sind eher wie ein extrem belesener, blitzschneller Praktikant. Was sie brillant können: relevantes Wissen aus einem riesigen Datenbestand (im Grunde dem Internet) herausziehen und auf unsere <em>konkreten</em> Anfragen zuschneiden.</p>
<p>Mein Ansatz: Ich <em>schreibe</em> das übergeordnete Konzept, die Architektur, die Kernlogik – im Grunde den Bauplan und die wichtige Statik. Dann gebe ich diesen klaren, durchdachten Plan dem LLM und sage: „Okay, jetzt mal die Details aus. Generiere den Boilerplate-Code, recherchiere diese API-Einzelheiten, entwirf die Struktur der Dokumentation.“ Das „Ausmalen“ ist oft der zeitaufwendige Teil. Indem ich die klaren Strukturen vorgebe, überlasse ich dem LLM die Fleißarbeit. Effizienzgewinn? Auf jeden Fall! Aber nur, weil die Vorarbeit – das klare Denken und Schreiben – schon erledigt war. Ohne klaren Prompt bekommst du oft nur eloquenten Unsinn zurück. Garbage In, Garbage Out gilt auch für KI.</p>
<h2 id="warum-dein-systemoutprintlnmeeting-outcome-captured-nicht-reicht">Warum dein <code>System.out.println("Meeting outcome captured!");</code> nicht reicht</h2>
<p>Seien wir ehrlich: Was in einem Meeting besprochen wird, ist oft schon Schnee von gestern, sobald der letzte Teilnehmer den Raum (oder den Zoom-Call) verlässt. „Hatten wir nicht vereinbart, das anders zu machen?“ Kommt dir bekannt vor? Das gesprochene Wort ist flüchtig, wie ein ungespeicherter Buffer. Und es skaliert miserabel. Versuch mal, zehn Leute auf denselben Stand zu bringen, indem du jedem einzeln die Geschichte erzählst.</p>
<p>Schreiben löst das. Ein gut formuliertes Dokument, ein klares Konzept, eine Architekturskizze in Textform – das bleibt. Es lässt sich teilen. Es ermöglicht asynchrone Zusammenarbeit. Neue Teammitglieder können sich einarbeiten. Entscheidungen sind nachvollziehbar. Das ist wie ein gut gepflegtes Git-Repository für Gedanken.</p>
<h2 id="kopf-frei-git-commit--m-thought-process-checkpointed">Kopf frei: <code>git commit -m "Thought process checkpointed"</code></h2>
<p>Unser Gehirn ist großartig, aber wenn es um aktive Kontexte geht, ist es kein Mehrkern-Wunder mit unendlich RAM. Jeden Gedanken, jede offene Aufgabe, jede vage Idee im Kopf zu behalten, frisst mentale Kapazität. Kennst du das Gefühl, wenn dich um 3 Uhr nachts ein Detail aus Projekt X wach hält, obwohl du eigentlich an Projekt Y arbeiten sollst?</p>
<p>Aufschreiben ist wie ein <code>git commit</code> für deine Gedanken. Sobald du eine Idee, einen Plan oder ein Problem so weit aufgeschrieben hast, dass du weißt, du kannst den Faden später wieder aufnehmen, kann dein Gehirn loslassen. Es vertraut darauf, dass die Information sicher ist. Das schafft Platz! Das ist, als würdest du 50 Browser-Tabs schließen, weil du weißt, dass die Links in deinen Lesezeichen gespeichert sind. Aufgeschriebene Gedanken machen den Kopf frei und dich bereit für die nächste Herausforderung – oder zumindest für einen erholsameren Schlaf.</p>
<img src="/assets/images/busy-minded-human.jpg">
<h2 id="vom-gedankenchaos-zur-konzept-leinwand">Vom Gedankenchaos zur Konzept-Leinwand</h2>
<p>Eine unausgesprochene, nicht aufgeschriebene Idee ist wie ein Geist. Sie schwebt herum, vage, undefiniert. Du drehst dich gedanklich im Kreis und kommst nicht wirklich voran. Diese Idee zu formulieren und aufzuschreiben ist wie der erste Pinselstrich auf einer leeren Leinwand. Es ist der <code>mkdir my-new-project &#x26;&#x26; cd my-new-project</code>-Moment für deine Kreativität.</p>
<p>Du legst die grundlegenden Strukturen fest. Du gibst der Idee eine Form. Und plötzlich siehst du nicht nur, was da ist, sondern auch, was fehlt. Du schaffst einen Rahmen, in dem sich neue, detailliertere Gedanken entfalten können. Ohne diesen ersten Schritt bleibt die Leinwand leer, und die Idee bleibt nur ein flüchtiger Einfall.</p>
<hr>
<img src="/assets/images/keys-not-just-for-coding.jpg">
<p>Für mich hat sich das Schreiben von einer lästigen Pflicht in eine unverzichtbare Superkraft verwandelt. Es schafft <strong>Klarheit</strong>, spart <strong>Zeit</strong>, verbessert die <strong>Zusammenarbeit</strong>, verschafft <strong>mentalen Freiraum</strong> und wirkt als Katalysator für <strong>Ideen</strong>.</p>
<p>Meine Herausforderung an dich lautet also: Wenn du das nächste Mal vor einem kniffligen Problem stehst, widersteh dem Drang, sofort auf die Tastatur einzuhämmern. Nimm dir ein paar Minuten. Öffne einen einfachen Texteditor (oder schnapp dir Stift und Papier, du Rebell!). Versuch, die Lösung oder das Konzept in klare Worte zu fassen. Du wirst überrascht sein, wie viele Bugs du findest, bevor sie überhaupt zu Code werden.</p>
<p>Dein zukünftiges, weniger gestresstes Ich wird es dir danken (oder dich zumindest etwas seltener verfluchen). Happy Writing – und <em>dann</em> Happy Coding!</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="software-development" /><category term="writing" /><category term="productivity" /><category term="career" /><category term="llm" /><category term="communication" /><category term="workflow" /><summary type="html"><![CDATA[Warum Schreiben die am meisten unterschätzte Fähigkeit von Entwicklern ist – klare Texte schärfen das Denken, verbreiten Wissen und werden im Zeitalter der LLMs zur Superkraft.]]></summary></entry><entry><title type="html">Die Rolle des Iterierens</title><link href="https://www.manuel-holzrichter.de/de/2024/01/17/die-rolle-des-iterierens/" rel="alternate" type="text/html" title="Die Rolle des Iterierens" /><published>2024-01-17T22:07:49.000Z</published><updated>2024-01-17T22:08:32.000Z</updated><id>https://www.manuel-holzrichter.de/de/2024/01/17/die-rolle-des-iterierens</id><content type="html" xml:base="https://www.manuel-holzrichter.de/de/2024/01/17/die-rolle-des-iterierens/"><![CDATA[<p>Erlebe Softwareentwicklung durch meine Augen. Von den ersten zögerlichen Schritten bis zu den Erfolgen von heute ist meine Geschichte geprägt von Erkenntnissen, Rückschlägen und entscheidenden Wendepunkten. Erfahre, wie ich mich vom unerfahrenen Entwickler zum agilen Denker entwickelt habe und dabei verstanden habe, wie wichtig kleine, iterative Schritte sind.</p>
<p><img src="/assets/images/busy-developer.jpg" alt="Beschäftigter Entwickler, der an mehreren Aufgaben arbeitet"></p>
<p>Mein erstes großes Projekt begann zwei Jahre nach meinem Einstieg in die Softwareentwicklung. Gemeinsam mit meinem Projektleiter besprach ich die anstehenden Aufgaben, und nach und nach setzte ich die Features um. Damals arbeiteten wir ohne <a href="https://www.manuel-holzrichter.de/de/2024/01/11/die-rolle-von-tests/">automatisierte Tests</a> und ihre Vorteile. Deshalb zeigten sich die Probleme dieses Vorgehens erst relativ spät.</p>
<p>Schwierigkeiten gab es immer dann, wenn die entwickelten Features zum ersten Mal auf die späteren Nutzer trafen. Schnell zeigte sich, dass Annahmen vom Projektbeginn entweder missverstanden oder nicht vollständig berücksichtigt worden waren. Die fehlenden Informationen führten zu umfangreicher Nacharbeit, die im ursprünglichen Projektplan nicht vorgesehen war. Was bedeutet ungeplante Arbeit? Genau, eine stressige Zeit mit vielen Überstunden.</p>
<p>Die Überstunden waren Motivation genug, diese Probleme in meinen künftigen Projekten zu vermeiden. Die Idee war trügerisch einfach, im Rückblick aber falsch: Features wurden auf Basis der ursprünglichen Problemstellung und der Anforderungen umgesetzt. Im Einsatz bei den Endnutzern zeigte sich, dass bestimmte Aspekte in der Planungsphase nicht bedacht worden waren. Um das zu vermeiden, wurde die Planungsphase intensiver und detaillierter, damit nichts übersehen wurde. Diese Bemühungen beseitigten in den folgenden Projekten zwar die gröbsten Planungsfehler, am Ergebnis änderte sich aber im Kern nichts: Sobald die Nutzer mit den Features arbeiteten, tauchten weitere Aspekte auf, die umfangreiche Anpassungen nach sich zogen.</p>
<p>Mit der Zeit setzte sich die Erkenntnis fest, dass man nicht alles im Voraus wissen kann.
Softwareentwicklung ist wie ein Spaziergang im Nebel. Wir sehen nicht weit und schon gar nicht das Ziel. Wir müssen kleine Schritte machen und jedes Mal neu bewerten, worauf wir gestoßen sind und welchen Weg wir einschlagen wollen. Aber wie lässt sich das auf Softwareentwicklung übertragen?</p>
<p>Ein neuer Ansatz entstand: Statt die gesamte benötigte Funktionalität auf einmal umzusetzen, konzentrierten wir uns darauf, nur das absolut Notwendige zu implementieren, um den Kernnutzen zu liefern. Diese Teillösung präsentierten wir den Endnutzern, und die Erkenntnisse aus ihrem Feedback flossen direkt in die nächste Entwicklungsphase ein.</p>
<p>Bis dahin hatten wir nach einem Modell gearbeitet, bei dem zu Projektbeginn einmalig eine zentrale Entwicklungsinstanz aufgesetzt wurde. Diese lief so lange, wie der Kunde die entstandenen Anwendungen bei sich vor Ort betrieb. Wir bekamen jedoch ein Problem, als die wachsende Zahl an Präsentationen für Endnutzer dazu führte, dass die Entwicklung vorübergehend stillstehen musste. Das konnten wir uns auf Dauer nicht leisten. Also beschlossen wir, dass jeder Entwickler seine Funktionalität lokal umsetzt und sie dann schrittweise in die zentrale Entwicklungsinstanz integriert. Allerdings war die Zahl der Installationen inzwischen so stark gewachsen, dass das als zu aufwendig galt. Es war nicht praktikabel, dass ein Entwickler 4 Stunden damit verbringt, eine lokale Entwicklungsumgebung aufzusetzen, nur um ein kleines Feature zu entwickeln.</p>
<p>Das neue Ziel war klar: Eine lokale Entwicklungsumgebung sollte sich mit nur einem Befehl erstellen lassen. Zu meiner Überraschung gelang das relativ schnell. Mit der Zeit zeigten sich die positiven Effekte dieses Zustands. Jetzt war es mühelos möglich, kurzfristig eine Demo-Instanz aufzusetzen oder mit dem Kunden Tests durchzuführen. Viele Aufgaben, die vorher umständlich wirkten, gingen nun leicht von der Hand, denn eine Anwendung auf dem neuesten Entwicklungsstand war nur einen einfachen Befehl entfernt.</p>
<p><img src="/assets/images/celebrating-2.jpg" alt="Entwickler feiert den verbesserten Arbeitsablauf"></p>
<p>Diese Erfahrung hat mir gezeigt, dass es sich lohnt, alles zu optimieren, was eine Entwicklungsiteration in die Länge zieht. Je schneller ich eine Iteration abschließen kann, desto effizienter bin ich und desto mehr schaffe ich in meiner Arbeitszeit. Als mir das klar wurde, konzentrierte ich mich darauf, die zeitaufwendigsten und ressourcenintensivsten Teile einer Entwicklungsiteration zu optimieren.</p>
<p>Ich kann mit Freude berichten, dass Überstunden nicht mehr zu meinem Arbeitsalltag gehören. Weil wir unsere Entwicklungsiterationen effizienter gemacht haben, können wir Erkenntnisse von Kunden nahtlos in die nächste Iteration einfließen lassen, ohne dafür viel Zeit aufwenden zu müssen. So bewegen wir uns in kleinen Schritten durch den Nebel und können mühelos die Richtung ändern, wenn es nötig ist.</p>
<h2 id="fazit">Fazit</h2>
<p>Mein Vorgehen bei Softwareprojekten hat sich im Laufe der Zeit stark verändert. Anfangs lag der Fokus auf intensiver Planung. Die Erkenntnis, dass man unmöglich alles im Voraus wissen kann, führte dann zu einer agileren Arbeitsweise.</p>
<p>Mit einem iterativen Vorgehen, bei dem nur das Nötige umgesetzt wird, lässt sich früh auf das Feedback der Endnutzer reagieren und die Entwicklung entsprechend anpassen. Der Wechsel zu lokalen Entwicklungsumgebungen, die sich mit einem einzigen Befehl erstellen lassen, hat nicht nur den Entwicklern die Arbeit erleichtert, sondern auch flexiblere Präsentationen und Tests mit Kunden ermöglicht.</p>
<p>Die wichtigste Erkenntnis: Wer Prozesse kontinuierlich verbessert und die zeitaufwendigen Teile von Entwicklungsiterationen optimiert, arbeitet effizienter. Das hat nicht nur die Überstunden überflüssig gemacht, sondern auch die Fähigkeit verbessert, sich an Kundenanforderungen anzupassen. Das Bild vom Spaziergang durch den Nebel zeigt: Du kannst nicht alles im Voraus sehen, aber du kannst in kleinen Schritten vorankommen und flexibel die Richtung ändern.</p>]]></content><author><name>Manuel Holzrichter</name></author><category term="software-development" /><category term="agile" /><category term="iteration" /><category term="devops" /><category term="productivity" /><summary type="html"><![CDATA[Warum kleine, iterative Schritte besser sind als die große Auslieferung am Ende: was ich auf dem Weg zum agilen Entwickler gelernt habe, eine kurze Feedback-Schleife nach der anderen.]]></summary></entry></feed>