# Micro-Frontends: Teamschnitt im Browser

URL: https://www.mikebild.dev/de/blog/micro-frontends-teamschnitt-browser/

Vier Teams, ein `index.html`, ein gemeinsames Deployment pro Woche. Die Rechnung dahinter sieht harmlos aus und ist es nicht: Jedes Release verlangt zwei Tage Code-Freeze, eine Regressionsrunde über die komplette Oberfläche und vier Freigaben. Ist ein Team nicht fertig, warten drei andere. Bei fünfzig Arbeitswochen im Jahr sind das im besten Fall fünfzig Gelegenheiten, etwas auszuliefern – und in der Praxis deutlich weniger, weil ein einziger fehlgeschlagener Smoke-Test das gesamte Release zurückhält. Dabei sind die Backends dieser Teams längst getrennte Services mit eigenen Pipelines. Im Browser aber laufen alle vier in einem einzigen JavaScript-Bundle wieder zusammen, und mit dem Bundle kehrt genau der Engpass zurück, den die Aufteilung in Services eigentlich beseitigen sollte.

## Ein Organisationsproblem mit technischem Symptom

Micro-Frontends werden gern als Frontend-Thema verhandelt: welches Framework, welche Integrationstechnik, wie klein ein Fragment sein darf. Mir ist in den letzten Monaten aufgefallen, dass diese Diskussion regelmäßig am Kern vorbeiläuft. Die eigentliche Frage lautet: Bildet die Struktur im Browser die Struktur der Organisation ab – oder arbeitet sie dagegen? Conways Beobachtung von 1968 gilt auch hier: Systeme spiegeln die Kommunikationswege der Organisation, die sie baut. Wenn vier Teams fachlich getrennt arbeiten, aber eine gemeinsame Single-Page-Anwendung pflegen, entsteht die Reibung genau an dieser Naht – als Merge-Konflikte in der Router-Konfiguration, als Abstimmungsrunden über CSS-Klassen, als Release-Kalender, der zum Dauerthema wird.

Neu ist das Muster nicht. Der ThoughtWorks Technology Radar führt „Micro Frontends" seit November 2016, seit Mai dieses Jahres im Ring „Trial". Zalando betreibt mit dem Mosaic-Projekt und dem Layout-Service Tailor seit gut zwei Jahren eine solche Architektur in Produktion. Neu ist höchstens die Einsicht, die sich beim Nachbauen einstellt: Der technische Teil ist der einfache.

## Eine lauffähige Beispiel-Landschaft

Um das nicht nur zu behaupten, habe ich im Juli das Repository `introduction-micro-frontends` zusammengestellt – eine lauffähige Beispiel-Landschaft mit einem Gateway auf Port 7000, dahinter vier eigenständige Anwendungen: Home (7010), Docs (7020), Products (7030) und Contact (7040), dazu eine Catalog-API, eine GraphQL-API auf Port 9000 und eine gemeinsame Komponenten-Bibliothek auf Port 5000. Jede Anwendung startet für sich, hat ihr eigenes Build und könnte von einem eigenen Team betrieben werden. Das Gateway kennt die Anwendungen über eine schlichte Service-Discovery – wahlweise aus Dateien, aus Docker/Kubernetes oder aus der Google Cloud – und routet nach Pfad.

```mermaid
flowchart LR
  B[Browser] --> G[Gateway<br/>:7000]
  G -->|/| H[home-app<br/>:7010<br/>Team A]
  G -->|/docs| D[docs-app<br/>:7020<br/>Team B]
  G -->|/products| P[products-app<br/>:7030<br/>Team C]
  G -->|/contact| C[contact-app<br/>:7040<br/>Team D]
  P --> Q[graphql-api<br/>:9000]
  H -. shared UI .-> L[components-library<br/>:5000]
  P -. shared UI .-> L
```

Das Diagramm zeigt bereits die zentrale Entwurfsentscheidung: Die Schnittlinien verlaufen vertikal. Jedes Team besitzt einen fachlichen Ausschnitt von der Route bis zur API – nicht eine horizontale Schicht wie „alle Buttons" oder „das gesamte CSS". Genau dieser Schnitt macht unabhängige Deployments erst möglich; alles andere ist Handwerk.

## Der einfache Teil: Integration

Die Integrationstechniken sind schnell aufgezählt, und keine davon ist spektakulär.

Die älteste ist der Hyperlink. Wenn `/products` und `/contact` getrennte Anwendungen sind, reicht ein `<a href="/contact">` – der Browser lädt die nächste Seite, das nächste Team übernimmt. Kein geteilter Code, keine geteilte Laufzeitumgebung, dafür ein sichtbarer Seitenwechsel. Für viele Übergänge ist das völlig ausreichend, und es ist die einzige Integration, die garantiert nie bricht.

Sollen Fragmente verschiedener Teams auf einer Seite erscheinen, komponiert man serverseitig. Server-Side-Includes beherrscht jeder nginx von Haus aus:

```nginx
# gateway: compose one page from team fragments
location / {
  ssi on;
  proxy_pass http://home-app:7010;
}

location /fragment/header {
  proxy_pass http://components-library:5000/header;
}
```

```html
<!-- home-app: index.html -->
<!--#include virtual="/fragment/header" -->
<main>
  <!--#include virtual="/fragment/product-teaser" -->
</main>
```

Zalandos Tailor treibt dasselbe Prinzip weiter und streamt Fragmente parallel, mit Timeout und Fallback pro Fragment. Edge-Side-Includes verlagern die Komposition in den Varnish- oder CDN-Layer. Allen Varianten gemeinsam: Die Seite entsteht aus unabhängig deploybaren Teilen, und ein langsames Fragment blockiert nicht das ganze Release, sondern höchstens seinen eigenen Platzhalter.

Clientseitig sind Custom Elements die sauberste Klammer – seit v1 in Chrome und Safari nativ, in Firefox und Edge im Moment noch per Polyfill:

```js
// products-app: expose a fragment as a custom element
class ProductTeaser extends HTMLElement {
  connectedCallback() {
    const sku = this.getAttribute('sku');
    fetch(`/products/api/teaser/${sku}`)
      .then((res) => res.text())
      .then((html) => { this.innerHTML = html; });
  }

  disconnectedCallback() {
    // team C cleans up its own listeners here
  }
}

customElements.define('product-teaser', ProductTeaser);
```

Das aufnehmende Team schreibt `<product-teaser sku="42"></product-teaser>` und muss nicht wissen, mit welchem Framework das Fragment intern gebaut ist. Die DOM-Schnittstelle – Attribute rein, Events raus – ist der Vertrag zwischen den Teams. Daneben gibt es iframes: verrufen, aber die einzige Technik mit vollständiger Isolation von CSS, JavaScript und globalem Zustand. Für das Zahlungsformular eines Drittanbieters oder ein Altmodul, das niemand mehr anfassen will, nach wie vor eine legitime Wahl. Und schließlich orchestriert single-spa mehrere Framework-Anwendungen innerhalb einer Single-Page-Anwendung – das löst das Routing-Problem elegant, holt aber alle Teams zurück in eine gemeinsame Laufzeitumgebung mit gemeinsamem Fehlerraum.

Welche Technik trägt, entscheidet sich an wenigen Fragen:

- Wie viel Seitenwechsel verträgt der Nutzerfluss – reicht ein Link?
- Muss die Seite ohne JavaScript ein brauchbares Erstbild liefern (SSI, Tailor), oder ist die Zielgruppe ohnehin angemeldet (Client-Komposition)?
- Wie hart müssen Fragmente isoliert sein – genügt Namensdisziplin, oder braucht es die iframe-Grenze?
- Wer betreibt den Kompositionspunkt: ein Plattform-Team das Gateway, oder jedes Team seinen eigenen Einstieg?

## Sorgfaltspflichten: Routing, Abhängigkeiten, CSS

Drei Dinge muss man trotzdem sauber regeln, sonst holt einen der Monolith durch die Hintertür wieder ein.

Erstens die Routing-Hoheit. Die URL ist die öffentliche Schnittstelle der gesamten Anwendung, also braucht sie einen Eigentümer. Im Beispiel-Repository besitzt das Gateway die oberste Ebene – `/products` gehört Team C, und zwar vollständig, inklusive aller Unterpfade. Sobald zwei Teams über einen Pfad verhandeln müssen, ist der Schnitt falsch gezogen. Deep-Links müssen dabei funktionieren wie auf jeder anständigen Website: Ein Reload auf `/products/42` landet beim richtigen Team, ohne dass ein zentraler Client-Router davon wissen muss. Das Gateway selbst bleibt bewusst dumm – es kennt Pfade und Ziele, aber keine Fachlichkeit. Jede Regel mehr an dieser Stelle macht es zum nächsten gemeinsamen Deployment, über das wieder alle abstimmen müssen.

Zweitens die geteilten Abhängigkeiten. Vier Anwendungen, die je ihr eigenes React bündeln, schicken das Framework viermal über die Leitung – bei einer Bibliothek von gut 100 KB ist das messbar, aber selten der Untergang. Die Alternative, eine zentral vorgegebene Framework-Version für alle, klingt sparsamer und kostet mehr: Jedes Framework-Update wird wieder ein Vier-Team-Projekt, und der gemeinsame Release-Takt ist zurück. Im Repository bündelt deshalb jede Anwendung selbst. Diesen Preis zahle ich bewusst, weil er in Kilobyte anfällt statt in Meetings.

Drittens die CSS-Isolation. CSS hat einen globalen Namensraum, und der ist in einer komponierten Seite das Einfallstor für Fernwirkung: Team B ändert eine Klasse `.card`, und bei Team C verrutscht das Layout. Präfix-Konventionen pro Team helfen sofort und kosten nichts. Shadow DOM kapselt Styles vollständig, hängt aber am selben Browser-Support wie Custom Elements. Der zuverlässigste Schutz bleibt Disziplin: keine globalen Selektoren, keine Element-Selektoren, kein `!important` über Team-Grenzen hinweg.

## Der schwere Teil: Wem gehört der Header?

Bis hierher war alles Technik, und Technik lässt sich ausprobieren. Die Fragen, an denen solche Vorhaben tatsächlich hängen bleiben, stehen in keiner nginx-Konfiguration.

Der Header ist das beste Beispiel. Er erscheint auf jeder Seite, gehört fachlich niemandem und technisch allen. In einem Projekt haben wir drei Antworten durchgespielt. Ein eigenes „Header-Team" schafft eine neue horizontale Schicht und damit einen neuen Engpass – jede fachliche Änderung wird zum Ticket bei einem anderen Team. Die zweite Antwort: Ein fachliches Team besitzt den Header, alle anderen binden ihn als Fragment ein. Das funktioniert, solange dieses Team die Wünsche der anderen ernst nimmt – der Header wird zum internen Produkt mit internen Kunden. Die dritte Antwort steht im Beispiel-Repository: Der Header liegt in der Komponenten-Bibliothek, versioniert, und jedes Team aktualisiert in seinem eigenen Takt. Der Preis ist Ehrlichkeit gegenüber der Realität – für ein paar Tage sind zwei Header-Versionen gleichzeitig live. Wer das nicht aushält, hat sich gegen unabhängige Deployments entschieden und sollte es dann auch so nennen.

Dieselbe Struktur hat die Design-Frage. Ein gemeinsames Erscheinungsbild entsteht nicht durch ein zentrales CSS, sondern durch einen Styleguide, der wie ein Produkt gepflegt wird: mit Versionen, Changelog und Ansprechpartnern. Atomic Design liefert dafür ein brauchbares Vokabular – kleine, dumme Bausteine werden geteilt, zusammengesetzte Ansichten gehören den Teams. Je höher eine Komponente in dieser Hierarchie steht, desto teurer ist ihr Teilen.

## Was geteilter Zustand kostet

Die härteste Währung ist geteilter Zustand. Das Standardbeispiel: Team C legt auf der Produktseite einen Artikel in den Warenkorb, und der Mini-Warenkorb im Header – Besitz eines anderen Teams – soll die neue Anzahl anzeigen. Der naheliegende Reflex ist ein globaler Store, den alle Fragmente lesen und schreiben. Damit handelt man sich exakt die Kopplung wieder ein, die man gerade losgeworden ist: ein gemeinsames Zustandsschema, dessen Änderung alle Teams gleichzeitig betrifft, plus die Frage, wessen Version des Stores geladen wird.

Die günstigere Antwort: Fragmente teilen keinen Speicher, sie teilen Ereignisse. Das DOM bringt den Mechanismus mit, ganz ohne zusätzliche Bibliothek:

```js
// products-app: announce the change, share no memory
window.dispatchEvent(new CustomEvent('cart:item-added', {
  detail: { sku: '42', quantity: 1 }
}));

// header fragment: react and re-fetch its own truth
window.addEventListener('cart:item-added', () => {
  fetch('/cart/api/summary')
    .then((res) => res.json())
    .then((summary) => updateBadge(summary.count));
});
```

Der Header hält seinen eigenen Zustand und holt sich die Wahrheit aus der Warenkorb-API; das Ereignis ist nur der Anstoß. Der Vertrag zwischen den Teams schrumpft auf einen Ereignisnamen und ein kleines `detail`-Objekt – beides lässt sich dokumentieren und abwärtskompatibel erweitern.

Ähnlich liegt der Fall beim Anmeldestatus, dem zweiten Dauerkandidaten für geteilten Zustand. Kein Fragment braucht das Session-Objekt eines anderen Teams: Ein Cookie auf der gemeinsamen Domain, vom Gateway geprüft, genügt als geteilte Tatsache – jedes Fragment entscheidet selbst, was es einem abgemeldeten Besucher zeigt. Wo das nicht reicht, etwa für den Anzeigenamen im Header, liefert wieder eine kleine API die Antwort, statt dass ein globales Objekt durch alle Fragmente gereicht wird. Trotzdem gilt: Jedes solche Ereignis ist eine Schnittstelle, und Schnittstellen brauchen Pflege. Wenn die Liste der teamübergreifenden Ereignisse zweistellig wird, ist das kein Tooling-Problem, sondern ein Hinweis, dass der fachliche Schnitt nicht stimmt – die Teams arbeiten dann längst am selben Gegenstand, nur eben gegeneinander.

## Unterm Strich

Der Merksatz, den ich aus dem Bau der Beispiel-Landschaft mitnehme: Micro-Frontends kauft man nicht als Technik, man kauft sie als Organisationsform – die Technik ist in zwei Wochen gelernt, der Teamschnitt nicht. Wer nur ein Frontend-Team hat, braucht das alles nicht; ein gut strukturierter Monolith mit sauberen Modulgrenzen ist dann die ehrlichere Architektur. Interessant wird der Umbau, wenn Freigaberunden, Code-Freeze und Release-Warteschlangen messbar Zeit kosten und mehrere Teams tatsächlich unabhängig liefern könnten, wenn man sie ließe. Dann lohnt es sich, zuerst die unbequemen Fragen zu klären – wem gehört der Header, wer pflegt den Styleguide, welche Ereignisse bilden den Vertrag – und erst danach zwischen SSI, Custom Elements und iframe zu wählen. Die Integrationstechnik ist am Ende austauschbar. Der Teamschnitt, den sie in den Browser verlängert, ist es nicht.

## Weiterführende Quellen

- Repository mit den lauffähigen Beispielen: https://github.com/MikeBild/introduction-micro-frontends
- ThoughtWorks Technology Radar zu Micro Frontends: https://www.thoughtworks.com/radar/techniques/micro-frontends
- Zalando Tailor, Layout-Service aus Project Mosaic: https://github.com/zalando/tailor
- single-spa, Router für mehrere Framework-Anwendungen: https://github.com/single-spa/single-spa
- MDN zu Custom Elements: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements
