post

Trunk-Based Development statt Branch-Friedhof

Kurze Integrationszyklen mit Feature Flags schlagen langlebige Branches: tägliche Integration ohne Merge-Hölle und ein Release, das zur Entscheidung wird statt zum Deploy-Ereignis.

≈ 8 Min. Lesezeit

In fast jedem Team, das ich in den letzten Jahren begleitet habe, gab es diesen einen Branch. Er hieß feature/redesign oder refactor/payment und lebte seit sechs Wochen. Niemand traute sich mehr, ihn zurückzumergen, weil master sich in der Zwischenzeit weitergedreht hatte und ein Merge einem archäologischen Ausgrabungsprojekt gleichkam. Dieser Branch ist kein Einzelfall, er ist ein Symptom. Und das Symptom hat einen Namen, den ich in Reviews gern benutze: der Branch-Friedhof.

Ich möchte in diesem Artikel eine These vertreten, die zunächst kontraintuitiv klingt, sich in der Praxis aber immer wieder bestätigt: Kurze Integrationszyklen mit Feature Flags schlagen langlebige Branches. Nicht ein bisschen, sondern deutlich. Es geht dabei nicht um ein hübscheres Git-Diagramm, sondern um ein Geschäftsergebnis: verkürzte Zyklen, weniger Reibung im Team, höhere Qualität im Auslieferungsprozess. Und es geht um eine Entkopplung, die vieles einfacher macht, als es klingt: Deploy und Release sind zwei verschiedene Dinge.

Die eigentliche Idee: Integrationsdistanz klein halten

Fangen wir bei der Kernidee an, nicht beim Werkzeug. Trunk-Based Development – so wie Paul Hammant es auf trunkbaseddevelopment.com beschreibt – bedeutet im Wesentlichen: Alle Entwickler integrieren ihre Arbeit mindestens einmal täglich in einen einzigen gemeinsamen Branch. Nennen wir ihn main oder trunk. Dieser Branch bleibt jederzeit releasebar. Branches, falls es sie überhaupt gibt, sind kurzlebig – deutlich unter einem Tag.

Der entscheidende Begriff dahinter ist Integrationsdistanz. Jeder Tag, den ein Branch abseits des Trunks lebt, vergrößert die Distanz zwischen dem, woran ich arbeite, und dem, was der Rest des Teams sieht. Diese Distanz ist keine lineare Größe. Sie wächst überproportional, weil sich Änderungen auf beiden Seiten anhäufen und miteinander in Konflikt geraten können. Ein Merge nach sechs Wochen ist nicht sechsmal so teuer wie einer nach einer Woche, sondern ein Vielfaches davon – und vor allem ist er unvorhersehbar.

Martin Fowler bringt das in seinem Begriff der Continuous Integration auf den Punkt, und zwar wörtlicher, als viele es lesen. „Continuous Integration“ heißt tatsächlich fortlaufend integrieren. Ein langlebiger Feature-Branch ist per Definition das Gegenteil: aufgeschobene Integration, deferred integration. Man kann nicht gleichzeitig Continuous Integration betreiben und tagelang auf eigenen Branches sitzen. Das eine schließt das andere aus. In seinen Patterns for Managing Source Code Branches liefert Fowler die passende Taxonomie: Integrationsfrequenz, der Begriff des „healthy branch“, die Mainline. Wer diese Sprache einmal verinnerlicht hat, sieht den Branch-Friedhof sofort.

Was die Daten sagen – vorsichtig formuliert

Ich bin skeptisch gegenüber Methodik-Versprechen, die sich nur auf Bauchgefühl stützen. Deshalb schätze ich die Arbeit von Nicole Forsgren, Jez Humble und Gene Kim in Accelerate und den State-of-DevOps-Reports. Dort wird Trunk-Based Development nicht als Meinung behandelt, sondern als statistisch identifizierte Capability.

Ich formuliere das bewusst sauber, weil die Versuchung groß ist, hier zu übertreiben. Die Daten zeigen: Höhere Software-Delivery-Performance korreliert mit wenigen aktiven Branches im Repository – Größenordnung drei oder weniger – und mit Merges in den Trunk mindestens einmal pro Tag. Trunk-Based Development ist eine der Praktiken, die Continuous Delivery statistisch prädiziert. Nicht „verursacht garantiert“, sondern prädiziert und korreliert. Das ist ein wichtiger Unterschied, den ich in Beratungsgesprächen nie verwische. Aber die Richtung ist eindeutig, und sie deckt sich mit dem, was ich in Teams beobachte: Wer die Integrationsdistanz klein hält, liefert schneller, seltener kaputt und mit weniger Drama.

Der Denkfehler: Deploy ist nicht Release

Jetzt kommt der Teil, an dem viele Diskussionen kippen. „Wenn alle ständig auf den Trunk mergen – wie soll dann jemals etwas Halbfertiges nicht in Produktion landen?“ Diese Frage ist berechtigt, und ihre Antwort ist der eigentliche Hebel dieses ganzen Ansatzes.

Deploy und Release sind zwei verschiedene Ereignisse. Deploy heißt: Der Code liegt auf der Produktionsumgebung. Release heißt: Das Verhalten ist für Nutzer aktiv. Feature Toggles – von Fowler in der Bliki beschrieben und von Pete Hodgson in Feature Toggles (aka Feature Flags) ausführlich systematisiert – entkoppeln genau diese beiden Dinge. Der Code wird ausgerollt, aber das Feature ist ausgeschaltet. Das Aktivieren wird zu einer Konfigurations- und Business-Entscheidung, nicht zu einem technischen Deploy-Ereignis mit Nachtschicht und Rollback-Plan.

flowchart LR
    subgraph friedhof["Branch-Friedhof"]
        direction TB
        M0["main"] --> M1["main"]
        M0 --> B1["feature branch<br/>Tag 1"]
        B1 --> B2["... Tag 20 ..."]
        B2 --> B3["divergiert weit"]
        M1 --> MG(("dicker<br/>Merge-Konflikt"))
        B3 --> MG
    end
    subgraph tbd["Trunk-Based"]
        direction TB
        T0["trunk"] --> T1["+ commit"]
        T1 --> T2["+ commit"]
        T2 --> T3["+ commit"]
        T3 --> T4["trunk bleibt grün"]
        T4 -. "toggle on<br/>= Release" .-> R(("Release<br/>als Schalter"))
    end

Das Diagramm zeigt den Kern: Links divergiert ein Branch über zwanzig Tage und mündet in einen schmerzhaften Merge. Rechts fließt ein durchgehender Trunk mit vielen kleinen Commits, und die Aktivierung passiert auf einer eigenen Achse – zeitlich entkoppelt vom Merge und vom Deploy. Release wird zum Schalter.

Hodgson unterscheidet vier Kategorien von Toggles, und diese Unterscheidung ist praktisch wertvoll, weil jede einen anderen Lebenszyklus hat. Release Toggles verstecken unfertige Arbeit und sollen kurzlebig sein. Ops Toggles steuern das Betriebsverhalten, etwa um eine teure Funktion unter Last abzuschalten. Experiment Toggles treiben A/B-Tests und passen gut zum Build-Measure-Learn-Gedanken aus Eric Ries’ Lean Startup. Permission Toggles schalten Verhalten für bestimmte Nutzergruppen frei. Wer diese vier nicht auseinanderhält, behandelt am Ende jeden Flag gleich – und das rächt sich.

Wie unfertige Arbeit sicher auf dem Trunk lebt

Ein Release Toggle im einfachsten Fall sieht so aus. Wichtig ist, dass der Flag nicht als loses if im Fachcode verstreut liegt, sondern hinter einer sauberen Abstraktion:

// The domain code does not know about flag plumbing.
// It asks a small, well-named abstraction.
if (features.isEnabled("new-checkout-flow")) {
    return newCheckoutService.process(cart);
}
return legacyCheckoutService.process(cart);

Auf der Konfigurationsseite entscheidet sich, ob ein Deploy zugleich ein Release ist. Ein deaktivierter Flag bedeutet: Der Code ist da, aber niemand sieht ihn. Das ist Deploy ohne Release:

features:
  new-checkout-flow:
    enabled: false        # deploy != release
    strategy: canary      # gradual rollout instead of big-bang
    percentage: 5         # start with 5% of traffic

Für große Umbauten reicht ein einzelner if allerdings nicht. Wenn ich ein zentrales Modul ersetze, hilft mir das Muster Branch by Abstraction, das Fowler beschreibt und Paul Hammant zugeschrieben wird. Die Idee: Ich ziehe ein gemeinsames Interface ein, unter dem alte und neue Implementierung parallel im Trunk leben. Die Umschaltung erfolgt über eine Factory oder einen Toggle. Statt eines wochenlangen Branches integriere ich ein großes Refactoring in kleinen, täglich integrierten Schritten:

public interface PaymentGateway {
    PaymentResult charge(Order order);
}

// Both live in the trunk at the same time.
final class LegacyPaymentGateway implements PaymentGateway { /* ... */ }
final class NewPaymentGateway    implements PaymentGateway { /* ... */ }

// A factory selects the implementation behind a toggle.
PaymentGateway gateway = features.isEnabled("new-payment-gateway")
        ? new NewPaymentGateway()
        : new LegacyPaymentGateway();

Ergänzend gibt es Dark Launching und das Keystone Interface: Man baut ein großes Feature Stück für Stück, aber verdrahtet das sichtbare Element – den Schlussstein – erst ganz zuletzt. Bis dahin läuft der neue Code vielleicht schon mit, sammelt Daten oder wird gegen Produktionslast getestet, ohne dass ein Nutzer etwas bemerkt. Auch Canary Releases und Blue-Green Deployments spielen hier hinein: Ausrollen und Aktivieren werden zu graduellen, kontrollierbaren Vorgängen statt zum großen Knall.

Der Kontrast: GitFlow und der Reflex zum langen Branch

Ich will fair bleiben. GitFlow, das Vincent Driessen 2010 in „A successful Git branching model“ vorgestellt hat, war eine durchdachte Antwort auf ein reales Problem – für eine bestimmte Art von Software. Driessen selbst hat 2020 den Hinweis ergänzt, dass sein Modell nicht für Continuous-Delivery-Web-Apps gedacht war. Genau da liegt der Punkt. Wer ein Produkt mit versionierten Releases und langen Support-Zyklen baut, kann mit einem Branching-Modell wie GitFlow gut leben. Wer eine kontinuierlich ausgelieferte Anwendung betreibt, importiert sich damit den Branch-Friedhof frei Haus.

Als leichteres Gegenmodell existiert seit 2011 GitHub Flow: ein kurzlebiger Branch, ein Pull Request, schneller Merge zurück in die Mainline. Das ist schon deutlich näher an Trunk-Based Development. Der eigentliche Unterschied ist am Ende nicht das Tooling, sondern die Frage, wie lange man bereit ist, Integration aufzuschieben. Meine Erfahrung: Je länger die Antwort, desto teurer die Rechnung.

Ehrlich über die Fallstricke

Ich wäre kein guter Berater, wenn ich diesen Ansatz als reine Erlösung verkaufen würde. Er hat drei Kanten, an denen sich Teams zuverlässig schneiden.

Erstens: Toggle-Schulden, der Toggle-Friedhof. Der Witz an der Sache ist bitter. Man führt Feature Flags ein, um den Branch-Friedhof zu vermeiden, und produziert dabei einen neuen Friedhof aus nie entfernten Flags. Jeder Release Toggle, der nach dem Release im Code bleibt, ist technische Schuld. Er verdoppelt Pfade, verwirrt Leser und verrottet still. Hodgson ist hier deutlich: Release Toggles brauchen ein Ablaufdatum und müssen aktiv zurückgebaut werden. Ich empfehle Teams, das Entfernen eines Flags als festen Teil der Feature-Definition zu behandeln – ein Feature ist erst fertig, wenn sein Release Toggle wieder verschwunden ist.

flowchart LR
    A["create toggle"] --> B["deploy dark"]
    B --> C["gradual release"]
    C --> D["fully on"]
    D --> E["remove toggle"]
    E --> F["trunk stays clean"]

Zweitens: Trunk-Based Development ohne belastbare Testautomatisierung und ein CI-Gate ist gefährlich. Der ganze Ansatz steht und fällt damit, dass der Trunk grün und releasebar bleibt. Wenn Builds und Tests langsam oder unzuverlässig sind, importiert man Instabilität direkt in die Mainline, statt sie in Branches zu isolieren. Der lange Branch ist in schlecht getesteten Codebasen manchmal ein rationaler Selbstschutz. Die richtige Reihenfolge lautet deshalb: erst schnelle, verlässliche Tests und ein Gate, dann tägliche Integration. Nicht umgekehrt. Wer bei häufigen Merges zusätzlich Integrationsrisiken zwischen Services fürchtet, findet mit Consumer-Driven Contract Testing – etwa über Pact – ein wirksames Sicherheitsnetz.

Drittens: die Testmatrix-Explosion. Jeder Flag verdoppelt kombinatorisch die möglichen Pfade durch den Code. Zwei Flags ergeben vier Zustände, drei ergeben acht. Ungetestete Flag-Kombinationen sind ein realer Risikoraum, kein theoretischer. Die Disziplin dagegen ist unspektakulär, aber wirksam: wenige gleichzeitig aktive Toggles, klare Ownership für jeden einzelnen, und ein bewusstes Auge auf die Kombinationen, die tatsächlich zusammen auftreten können.

Warum sich der Aufwand rechnet

Wenn ich all das zusammenziehe, lande ich nicht bei einer Tool-Empfehlung, sondern bei einer Haltung zur Auslieferung. Kleine Integrationsdistanz, ein jederzeit releasebarer Trunk, und die saubere Trennung von Deploy und Release ergeben zusammen etwas, das sich in Geschäftszahlen niederschlägt.

Die Zyklen werden kürzer, weil niemand mehr Tage in Merge-Konflikten verliert. Die Reibung im Team sinkt, weil alle gegen denselben aktuellen Stand arbeiten und Konflikte klein und häufig statt groß und selten sind. Die Qualität steigt, weil ein Feature graduell und rückholbar ausgerollt wird, statt in einem einzigen riskanten Sprung. Und das Release verliert seinen Schrecken: Es ist kein nächtliches Deploy-Ereignis mehr, sondern eine Entscheidung, die jemand mit einem Schalter trifft – zurücknehmbar, messbar, unaufgeregt.

Das ist am Ende der Reiz des Ansatzes. Er ersetzt Prozess-Overhead durch etwas Einfaches und Elegantes. Statt eines aufwendigen Branching-Modells, das den Merge-Schmerz verwaltet, vermeidet man den Schmerz, indem man ihn gar nicht erst entstehen lässt. Der Branch-Friedhof ist kein Naturgesetz. Er ist eine Folge aufgeschobener Integration – und die kann man abschaffen.

Weiterführende Quellen

Kommentare