Feature-Toggles & Trunk-Based Development Release-Architektur

Feature-Toggles & Trunk-Based Development: Architektur für sichere Releases

Carola Schulte
Carola Schulte 1. März 2026 18 min Lesezeit

Das neue Checkout-Feature ist fertig. Vier Wochen Arbeit, eigener Branch, alle Tests grün. Der Merge dauert zwei Tage, weil sich in der Zwischenzeit 47 Dateien auf main geändert haben. Am Ende funktioniert der Merge, aber drei andere Features sind kaputt. Das Deployment wird verschoben. Kommt Ihnen das bekannt vor?

Kurz gesagt: Feature-Toggles entkoppeln Deployment von Release. Code kann jederzeit deployt werden, auch wenn das Feature noch nicht fertig ist – weil es per Toggle deaktiviert bleibt. In Kombination mit Trunk-Based Development entfallen langlebige Feature-Branches und die Merge-Hölle gleich mit.

Das Problem mit Feature-Branches

Feature-Branches fühlen sich sicher an: Jede Entwicklerin arbeitet isoliert, niemand stört niemanden. Aber diese Isolation ist trügerisch. Je länger ein Branch lebt, desto größer wird der Abstand zum Hauptzweig – und desto schmerzhafter wird der Merge.

Branch-Lebensdauer Typisches Merge-Erlebnis
1–2 Tage Meist problemlos, wenige Konflikte
1–2 Wochen Merge-Konflikte wahrscheinlich, Nacharbeit nötig
Mehr als 2 Wochen Merge wird zum eigenen Projekt. Subtile Bugs entstehen durch sich überlagernde Änderungen

Das eigentliche Problem ist nicht der Merge selbst, sondern der verzögerte Integrationstest. Zwei Entwickler ändern dieselbe Klasse in verschiedene Richtungen. Beide Tests sind grün – auf ihrem jeweiligen Branch. Erst beim Merge zeigt sich, dass die Änderungen nicht zusammenpassen. Und dann ist die Ursache schwer zu finden, weil Wochen an Änderungen auf einmal zusammenfließen.

Trunk-Based Development: Kurze Wege statt langer Branches

Trunk-Based Development (TBD) ist kein neues Konzept – Google, Meta und andere große Engineering-Organisationen arbeiten seit Jahren so. Die Idee: Ziel ist häufige Integration in kleinen Schritten – idealerweise täglich, bei kleinen Änderungen auch mehrfach am Tag.

In vielen Teams bedeutet TBD nicht „nie Branches“, sondern extrem kurzlebige Review-Branches statt wochenlanger Feature-Branches. Pull Requests bleiben, aber der Branch lebt Stunden statt Wochen.

Die Grundregeln

  • Kurzlebige Branches: Wenn überhaupt, dann maximal 1–2 Tage. Für Code-Reviews per Pull Request reicht das
  • Kleine Commits: Lieber fünf kleine, in sich abgeschlossene Commits als ein großer. Jeder Commit lässt den Build grün
  • Unfertige Features verbergen: Code für ein halbfertiges Feature wird deployt, ist aber per Toggle deaktiviert. Niemand merkt es
  • CI ist Pflicht: Jeder Commit durchläuft die gesamte Test-Suite. Rote Builds werden sofort gefixt, nicht „später“
Voraussetzung: Trunk-Based Development funktioniert nur mit einer zuverlässigen CI-Pipeline und einem Team, das rote Builds ernst nimmt. Ohne automatisierte Tests ist TBD nicht sinnvoll – dann merkt niemand, wenn ein Commit etwas kaputt macht.

Workflow in der Praxis

# Morgens: aktuellen Stand holen
git pull --rebase origin main

# Feature-Arbeit in kleinen Schritten
git add src/Checkout/ShippingCalculator.php
git commit -m "Add shipping cost calculation for EU countries"

# Vor dem Push: nochmal rebasen + Tests
git pull --rebase origin main
./vendor/bin/phpunit
git push origin main

# Oder: kurzer PR-Branch für Review
git checkout -b add-eu-shipping
git push -u origin add-eu-shipping
# PR erstellen, Review, Merge, Branch löschen

Entscheidend: Der Branch lebt Stunden, nicht Wochen. Der Merge ist trivial, weil der Abstand zum Trunk minimal ist.

Code-Reviews in TBD

TBD heißt nicht „kein Review“. Die Frage ist nur, wie. Drei gängige Modelle:

  • Kurzlebige PR-Branches: Branch erstellen, PR öffnen, Review am selben Tag, Merge. Das ist der häufigste Ansatz in kleineren bis mittleren Teams
  • Pair Programming: Zwei Entwickler arbeiten gemeinsam. Der Code ist beim Commit bereits reviewt – kein separater Review-Schritt nötig
  • Post-Commit-Review: Bei sehr hoher Commit-Frequenz (Google-Stil): Commit geht auf main, Review folgt asynchron. Erfordert starke Test-Abdeckung und Vertrauen im Team

Für die meisten Teams ist der erste Ansatz der pragmatischste: kurze Branches, schnelle Reviews, gleicher Tag.

Feature-Toggles: Die vier Typen

Nicht jeder Toggle ist gleich. Martin Fowler unterscheidet vier Kategorien, die sich in Lebensdauer und Dynamik unterscheiden. Diese Unterscheidung ist wichtig, weil sie bestimmt, wie Sie den Toggle technisch umsetzen und wann Sie ihn wieder entfernen.

Toggle-Typ Lebensdauer Wer steuert Beispiel
Release-Toggle Tage bis Wochen Entwicklung Neues Checkout hinter Toggle verstecken, bis es fertig ist
Experiment-Toggle Wochen bis Monate Produkt/Business A/B-Test: Welcher Button-Text konvertiert besser?
Ops-Toggle Dauerhaft Betrieb Bei Last-Spitzen den Produkt-Empfehlungs-Service deaktivieren
Permission-Toggle Dauerhaft Business/Vertrieb Premium-Feature nur für zahlende Kunden (grobgranular – kein Ersatz für ein echtes Rollen-/Rechtesystem)
Die wichtigste Regel: Release-Toggles sind temporär. Sobald das Feature stabil läuft, muss der Toggle und der gesamte Toggle-Code entfernt werden. Ein Toggle, der seit sechs Monaten auf true steht, ist kein Toggle mehr – er ist toter Code mit Wartungskosten.

Implementierung: Vom einfachen if bis zur Toggle-Architektur

Stufe 1: Config-basiert

Für den Anfang reicht eine einfache Konfigurationsdatei. Kein Framework, keine Datenbank – ein Array und ein if.

// config/features.php
return [
    'new_checkout'        => false,
    'dark_mode'           => true,
    'ai_recommendations'  => false,
];

// FeatureToggle.php
class FeatureToggle
{
    private array $features;

    public function __construct(string $configPath)
    {
        $this->features = require $configPath;
    }

    public function isEnabled(string $feature): bool
    {
        return $this->features[$feature] ?? false;
    }
}

// Verwendung
$features = new FeatureToggle(__DIR__ . '/config/features.php');

if ($features->isEnabled('new_checkout')) {
    $controller = new NewCheckoutController();
} else {
    $controller = new CheckoutController();
}

Vorteil: Null Abhängigkeiten, sofort verständlich. Nachteil: Zum Umschalten braucht man ein Deployment. Für Release-Toggles ist das in der Regel ausreichend.

Stufe 2: Datenbank-basiert

Wenn Toggles ohne Deployment umschaltbar sein sollen – zum Beispiel für Ops-Toggles oder Experimente – gehören sie in die Datenbank.

CREATE TABLE feature_toggles (
    name VARCHAR(100) PRIMARY KEY,
    enabled BOOLEAN NOT NULL DEFAULT FALSE,
    description TEXT,
    toggle_type VARCHAR(20) NOT NULL
        CHECK (toggle_type IN ('release', 'experiment', 'ops', 'permission')),
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO feature_toggles (name, enabled, toggle_type, description) VALUES
    ('new_checkout', false, 'release', 'Neuer Checkout-Prozess mit Adressvalidierung'),
    ('kill_switch_recommendations', true, 'ops', 'Empfehlungs-Engine bei Last abschalten'),
    ('premium_export', true, 'permission', 'CSV-Export nur für Premium-Kunden');
class DbFeatureToggle
{
    private array $cache = [];

    public function __construct(private PDO $db) {}

    public function isEnabled(string $feature): bool
    {
        if (!isset($this->cache[$feature])) {
            $stmt = $this->db->prepare(
                "SELECT enabled FROM feature_toggles WHERE name = ?"
            );
            $stmt->execute([$feature]);
            $row = $stmt->fetch();
            $this->cache[$feature] = $row ? (bool)$row['enabled'] : false;
        }

        return $this->cache[$feature];
    }

    public function allEnabled(): array
    {
        $stmt = $this->db->query(
            "SELECT name FROM feature_toggles WHERE enabled = TRUE"
        );
        return $stmt->fetchAll(PDO::FETCH_COLUMN);
    }
}
Performance: Laden Sie alle Toggles einmal pro Request und cachen Sie das Ergebnis im Objekt (wie oben). Bei vielen Toggles und hohem Traffic kann zusätzlich ein Redis-Cache sinnvoll sein – mit kurzer TTL (z.B. 30 Sekunden), damit Änderungen schnell greifen.

Stufe 3: Kontext-abhängige Toggles

Für Experimente und graduelle Rollouts brauchen Sie Toggles, die nicht global an/aus sind, sondern vom Kontext abhängen: Welcher Nutzer? Welches Land? Welcher Prozentsatz?

class ContextualToggle
{
    public function __construct(private PDO $db) {}

    public function isEnabled(string $feature, array $context = []): bool
    {
        $toggle = $this->getToggle($feature);
        if (!$toggle) return false;

        // Global deaktiviert?
        if (!$toggle['enabled']) return false;

        // Kein Kontext nötig? Dann global aktiv
        if (empty($toggle['rules'])) return true;

        $rules = json_decode($toggle['rules'], true);

        // Prozent-Rollout: z.B. 10% der Nutzer
        if (isset($rules['percentage'])) {
            // Eingeloggt: User-ID (stabil). Nicht eingeloggt: Session-ID (instabil bei Login).
            $identifier = $context['user_id'] ?? $context['session_id'] ?? null;
            if ($identifier === null) return false;
            $bucket = crc32($feature . ':' . $identifier) % 100;
            return $bucket < $rules['percentage'];
        }

        // Nutzer-Whitelist: bestimmte User-IDs
        if (isset($rules['user_ids']) && isset($context['user_id'])) {
            return in_array($context['user_id'], $rules['user_ids'], true);
        }

        // Land-basiert
        if (isset($rules['countries']) && isset($context['country'])) {
            return in_array($context['country'], $rules['countries'], true);
        }

        return false;
    }

    private function getToggle(string $feature): ?array
    {
        $stmt = $this->db->prepare(
            "SELECT * FROM feature_toggles WHERE name = ?"
        );
        $stmt->execute([$feature]);
        return $stmt->fetch() ?: null;
    }
}

// Verwendung: Gradueller Rollout
$toggle = new ContextualToggle($db);

if ($toggle->isEnabled('new_checkout', [
    'user_id' => $currentUser->id,
    'country' => $currentUser->country,
])) {
    // Neuer Checkout
}

Warum crc32 statt random_int? Weil ein Nutzer immer dasselbe Ergebnis bekommen soll. Mit einer Hash-Funktion über User-ID + Feature-Name landet derselbe Nutzer immer im selben Bucket – egal wie oft er die Seite aufruft. Für anonyme Nutzer ist eine Session-ID nur ein Näherungswert – bei Login, Gerätewechsel oder Cookie-Reset kann sich der Bucket ändern.

Architektur-Patterns für Toggle-Code

Pattern 1: Branching by Abstraction

Statt if/else durch den gesamten Code zu streuen, nutzen Sie ein Interface und zwei Implementierungen. Der Toggle entscheidet, welche Implementierung injiziert wird.

interface CheckoutProcessor
{
    public function process(Cart $cart): Order;
}

class LegacyCheckoutProcessor implements CheckoutProcessor
{
    public function process(Cart $cart): Order
    {
        // Bisherige Logik
    }
}

class NewCheckoutProcessor implements CheckoutProcessor
{
    public function process(Cart $cart): Order
    {
        // Neue Logik mit Adressvalidierung
    }
}

// In der Service-Konfiguration (z.B. DI-Container)
$container->set(CheckoutProcessor::class, function () use ($features, $db) {
    if ($features->isEnabled('new_checkout')) {
        return new NewCheckoutProcessor($db);
    }
    return new LegacyCheckoutProcessor($db);
});

Vorteil: Der Toggle ist an genau einer Stelle. Der restliche Code arbeitet nur gegen das Interface und weiß nicht, welche Implementierung dahinter steckt. Beim Entfernen des Toggles löschen Sie die alte Klasse und die Factory-Logik – fertig.

Pattern 2: Dark Launching

Die neue Implementierung läuft im Hintergrund mit, aber ihr Ergebnis wird verworfen. So testen Sie unter realer Last, ohne dass Nutzer betroffen sind.

class DarkLaunchCheckout implements CheckoutProcessor
{
    public function __construct(
        private CheckoutProcessor $current,
        private CheckoutProcessor $candidate,
        private LoggerInterface $logger
    ) {}

    public function process(Cart $cart): Order
    {
        // Aktuelles Ergebnis – das zählt
        $result = $this->current->process($cart);

        // Kandidat im Hintergrund – Ergebnis wird nur geloggt
        try {
            $candidateResult = $this->candidate->process(clone $cart);
            if ($result->total !== $candidateResult->total) {
                $this->logger->warning('dark_launch.mismatch', [
                    'feature'   => 'new_checkout',
                    'cart_id'   => $cart->id,
                    'current'   => $result->total,
                    'candidate' => $candidateResult->total,
                ]);
            }
        } catch (\Throwable $e) {
            $this->logger->error('dark_launch.error', [
                'feature' => 'new_checkout',
                'error'   => $e->getMessage(),
            ]);
        }

        return $result;
    }
}
Nur bei lesenden/idempotenten Operationen: Dark Launching funktioniert nur, wenn der Kandidat keine Seiteneffekte hat. Payment-Gateways, E-Mail-Versand, Lagerbestandsänderungen – all das wird zweimal ausgeführt, wenn beide Implementierungen laufen:
// GEFAHR: Dark Launch bei schreibenden Operationen
$this->paymentGateway->charge($cart);  // Wird zweimal ausgeführt!
$this->mailer->sendConfirmation($order); // Kunde bekommt zwei E-Mails!

// NUR bei lesendem/idempotenten Code:
$this->pricingCalculator->calculate($cart); // Safe

Dark Launching erhöht außerdem Last und Komplexität, weil zwei Implementierungen gleichzeitig laufen. Bei CPU-intensiven Operationen kann das spürbar sein – im Zweifel nur für einen Prozentsatz der Requests aktivieren.

Pattern 3: Gradueller Rollout

Statt sofort für alle Nutzer einzuschalten, rollen Sie schrittweise aus: erst intern, dann 5%, dann 20%, dann 100%.

// Rollout-Plan als Konfiguration
$rolloutPlan = [
    'new_checkout' => [
        'phase_1' => ['type' => 'user_ids', 'value' => [1, 2, 3]],           // Internes Team
        'phase_2' => ['type' => 'percentage', 'value' => 5],                   // 5% der Nutzer
        'phase_3' => ['type' => 'percentage', 'value' => 20],                  // 20%
        'phase_4' => ['type' => 'percentage', 'value' => 100],                 // Alle
    ]
];

// Zwischen den Phasen: Metriken vergleichen (Toggle ON vs. OFF)
// - Error Rate: Anstieg um mehr als wenige Prozentpunkte? Rollback.
// - p95 Latency: Deutlich erhöht? Rollback.
// - Conversion: Bei Experiment-Toggles erst ab ausreichend Samples
//   (mehrere Hundert pro Variante) bewerten.
// Wenn außerhalb Toleranz: Rollback auf vorherige Phase.

Dieser Ansatz reduziert das Risiko erheblich. Wenn bei 5% ein Problem auftritt, sind nur 5% betroffen – und Sie können sofort auf 0% zurückschalten, ohne ein Deployment.

Konkrete Schwellwerte hängen von Ihrer Baseline ab. Erst zwei Wochen Normalbetrieb messen, dann Toleranzen definieren. „Error Rate darf nicht um mehr als X Prozentpunkte steigen“ ist aussagekräftiger als ein absoluter Wert.

Toggle-Hygiene: Aufräumen als Pflicht

Feature-Toggles sind technische Schulden ab dem Moment, in dem sie ihren Zweck erfüllt haben. Ein System mit 200 aktiven Toggles, von denen 150 seit Monaten auf true stehen, ist schwerer zu verstehen als nötig.

Der Toggle-Lifecycle

// 1. Toggle erstellen (mit Ablaufdatum!)
INSERT INTO feature_toggles (name, enabled, toggle_type, expires_at)
VALUES ('new_checkout', false, 'release', '2026-04-15');

// 2. Entwicklung hinter Toggle
// 3. Gradueller Rollout
// 4. Feature stabil → Toggle auf true für alle
// 5. Toggle und alten Code entfernen (!)
// 6. Zeile aus feature_toggles löschen

Toggle-Inventar pflegen

class ToggleReport
{
    public function __construct(private PDO $db) {}

    public function getStaleToggles(int $daysThreshold = 30): array
    {
        $cutoff = (new \DateTimeImmutable("-{$daysThreshold} days"))->format('Y-m-d H:i:s');
        $stmt = $this->db->prepare("
            SELECT name, toggle_type, enabled, updated_at, expires_at
            FROM feature_toggles
            WHERE toggle_type = 'release'
              AND enabled = TRUE
              AND updated_at < ?
            ORDER BY updated_at ASC
        ");
        $stmt->execute([$cutoff]);
        return $stmt->fetchAll();
    }

    public function getExpiredToggles(): array
    {
        return $this->db->query("
            SELECT name, toggle_type, expires_at
            FROM feature_toggles
            WHERE expires_at IS NOT NULL
              AND expires_at < NOW()
        ")->fetchAll();
    }
}
Praxistipp: Bauen Sie den getExpiredToggles()-Check in Ihre CI-Pipeline ein. Wenn ein Release-Toggle sein Ablaufdatum überschritten hat, schlägt der Build fehl – das erzwingt das Aufräumen. Harte Maßnahme, aber wirksam.

Testen mit Feature-Toggles

Feature-Toggles erhöhen die Zahl möglicher Code-Pfade. Drei Toggles ergeben theoretisch acht Kombinationen. Genau deshalb müssen Release-Toggles kurzlebig bleiben – je weniger gleichzeitig aktiv, desto überschaubarer die Testmatrix. Pragmatischer Ansatz:

  • Jeden Toggle einzeln testen: Feature an vs. Feature aus. Das sind 2×n Tests, nicht 2n
  • Standard-Pfad testen: Alle Toggles in der Default-Konfiguration (wie in Production)
  • Neuer Pfad testen: Der Toggle, der gerade geändert wird, in beiden Zuständen
  • Kombinationen nur bei bekannter Interaktion: Wenn zwei Toggles dasselbe Feature betreffen, testen Sie die Kombination. Allerdings: Zwei Toggles für dasselbe Feature (new_checkout und checkout_address_validation) sind meist ein Design-Problem – besser einen Toggle mit klarem Scope
class FeatureToggleTest extends TestCase
{
    public function testNewCheckoutEnabled(): void
    {
        $features = new FeatureToggle(['new_checkout' => true]);
        $processor = $this->createCheckoutProcessor($features);

        $order = $processor->process($this->createTestCart());

        $this->assertTrue($order->hasAddressValidation());
    }

    public function testNewCheckoutDisabled(): void
    {
        $features = new FeatureToggle(['new_checkout' => false]);
        $processor = $this->createCheckoutProcessor($features);

        $order = $processor->process($this->createTestCart());

        $this->assertFalse($order->hasAddressValidation());
    }

    private function createCheckoutProcessor(FeatureToggle $features): CheckoutProcessor
    {
        if ($features->isEnabled('new_checkout')) {
            return new NewCheckoutProcessor($this->db);
        }
        return new LegacyCheckoutProcessor($this->db);
    }
}

Ops-Toggles: Kill-Switches für den Ernstfall

Ops-Toggles sind die Feature-Toggles des Betriebs. Sie erlauben, teure oder instabile Komponenten abzuschalten, ohne den gesamten Service herunterzufahren.

class CircuitBreaker
{
    public function __construct(
        private FeatureToggle $features,
        private LoggerInterface $logger
    ) {}

    public function getProductRecommendations(int $productId): array
    {
        // Ops-Toggle: Empfehlungen bei Last abschalten
        if (!$this->features->isEnabled('recommendations_engine')) {
            $this->logger->info('recommendations.disabled_by_toggle');
            return []; // Graceful Degradation: leere Empfehlungen
        }

        try {
            return $this->recommendationService->getFor($productId);
        } catch (\Throwable $e) {
            $this->logger->error('recommendations.failed', [
                'product_id' => $productId,
                'error'      => $e->getMessage(),
            ]);
            return []; // Fallback: keine Empfehlungen
        }
    }
}

Der Unterschied zu Release-Toggles: Ops-Toggles bleiben dauerhaft im Code. Sie sind bewusst eingebaute Sicherheitsventile. Ein Admin-Panel zum Umschalten (oder ein Endpunkt, der per curl erreichbar ist) erlaubt dem Ops-Team, in Sekundenschnelle zu reagieren – ohne auf ein Deployment zu warten.

Häufige Fehler

1. Toggles nie aufräumen

Das häufigste Problem. Release-Toggles werden angelegt, das Feature geht live – und der Toggle bleibt einfach stehen. Nach einem Jahr hat das Team 80 Toggles, von denen niemand weiß, welche noch relevant sind.

Gegenmaßnahme: Jeder Release-Toggle bekommt ein expires_at-Datum. CI schlägt Alarm, wenn es überschritten ist. Wer den Toggle anlegt, ist für das Aufräumen verantwortlich.

2. Toggle-Logik im gesamten Code verstreut

// Schlecht: Toggle an 17 Stellen im Code
if ($features->isEnabled('new_checkout')) { /* ... */ }
// ... 200 Zeilen weiter ...
if ($features->isEnabled('new_checkout')) { /* ... */ }
// ... in einem Template ...
if ($features->isEnabled('new_checkout')) { /* ... */ }

// Besser: Toggle an einer Stelle, Abstraktion dahinter
// (siehe Branching by Abstraction oben)

Je mehr Stellen einen Toggle abfragen, desto schwerer ist er zu entfernen. Ideal: ein Toggle, eine Stelle. In der Praxis sind zwei bis drei Stellen akzeptabel – mehr ist ein Code-Smell.

3. Toggles als Berechtigungssystem missbrauchen

Feature-Toggles können „Premium-Feature nur für zahlende Kunden“ abbilden. Aber wenn Sie feingranulare Rechte brauchen (Rollen, Permissions, Mandantentrennung), gehört das in ein echtes Berechtigungssystem – nicht in Toggle-Konfigurationen.

4. Keine Metriken pro Toggle

Wenn Sie nicht messen, wie sich ein Toggle auf Fehlerrate, Latenz und Conversion auswirkt, fliegen Sie blind. Jeder Experiment- und Release-Toggle sollte mit Metriken verknüpft sein, die Sie zwischen den Rollout-Phasen prüfen.

Build vs. Buy: Brauche ich ein Toggle-Framework?

Situation Empfehlung
Wenige Release-Toggles (<10), kleines Team Config-Datei oder DB-Tabelle reicht. Kein Framework nötig
Experiment-Toggles, A/B-Tests, graduelle Rollouts Eigene kontextabhängige Toggle-Klasse (wie Stufe 3 oben) oder ein leichtgewichtiges SDK
Viele Teams, viele Toggles, Audit-Anforderungen Managed Service (LaunchDarkly, Unleash, Flagsmith). Audit-Trail, Segmentierung und SDKs sind inkludiert
Pragmatischer Einstieg: Starten Sie mit der einfachsten Lösung, die Ihren aktuellen Bedarf deckt. Eine Config-Datei mit fünf Toggles ist besser als ein eingerichtetes LaunchDarkly, das niemand nutzt. Upgraden Sie, wenn der Bedarf real wird – nicht vorher.

Toggle-Governance: Wer darf was?

In kleinen Teams ist die Frage trivial: Jeder darf alles. In größeren Organisationen brauchen Toggles klare Ownership-Regeln, denn einen Toggle auf Production umzuschalten ist ein Release – auch wenn kein Deployment stattfindet.

  • Wer darf Toggles anlegen? Entwickler (Release-Toggles), Product Owner (Experiment-Toggles), Ops (Ops-Toggles). Klare Zuordnung nach Toggle-Typ
  • Wer darf umschalten? Release-Toggles: Entwicklungsteam. Ops-Toggles: On-Call/DevOps. Experiment-Toggles: Product Owner nach Absprache
  • Wer räumt auf? Wer den Toggle anlegt, ist für das Entfernen verantwortlich. Kein „das macht irgendwer“

Toggle-Audit-Trail

Jede Änderung an einem Production-Toggle sollte protokolliert werden. Wenn nach einem Toggle-Flip Fehler auftreten, müssen Sie nachvollziehen können, wer wann was umgeschaltet hat.

CREATE TABLE toggle_audit (
    id SERIAL PRIMARY KEY,
    toggle_name VARCHAR(100) NOT NULL,
    changed_by INT NOT NULL,
    old_value BOOLEAN NOT NULL,
    new_value BOOLEAN NOT NULL,
    reason TEXT,
    changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
class ToggleService
{
    public function __construct(private PDO $db) {}

    public function setEnabled(string $name, bool $enabled, int $userId, string $reason = ''): void
    {
        $stmt = $this->db->prepare("SELECT enabled FROM feature_toggles WHERE name = ?");
        $stmt->execute([$name]);
        $current = $stmt->fetchColumn();

        // Toggle aktualisieren
        $stmt = $this->db->prepare(
            "UPDATE feature_toggles SET enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?"
        );
        $stmt->execute([$enabled, $name]);

        // Audit-Eintrag
        $stmt = $this->db->prepare(
            "INSERT INTO toggle_audit (toggle_name, changed_by, old_value, new_value, reason)
             VALUES (?, ?, ?, ?, ?)"
        );
        $stmt->execute([$name, $userId, (bool)$current, $enabled, $reason]);
    }
}

Das Zusammenspiel: TBD + Toggles + CI/CD

Feature-Toggles und Trunk-Based Development ergänzen sich zu einem Release-Workflow, der häufige Deployments ohne Risiko ermöglicht:

  1. Entwickler committet auf main – neuer Code hinter Release-Toggle
  2. CI baut und testet – beide Toggle-Zustände werden geprüft
  3. Deployment auf Production – Feature ist deployt, aber deaktiviert
  4. Gradueller Rollout – erst Team, dann 5%, dann alle
  5. Feature stabil – Toggle und alter Code werden entfernt
  6. Nächster Commit – Aufräum-Commit geht direkt auf main

Das Ergebnis: Deployment ist langweilig. Kein Feature-Freeze, keine Release-Nächte, kein „wir deployen nur donnerstags“. Code fließt kontinuierlich, und jedes Deployment ist klein genug, um problemlos rückgängig gemacht zu werden – per Toggle statt per Rollback.

Checkliste: Feature-Toggles & Trunk-Based Development

  • Branches kurzlebig? Maximal 1–2 Tage, dann Merge oder direkt auf main
  • CI-Pipeline zuverlässig? Jeder Commit wird automatisch getestet. Rote Builds werden sofort gefixt
  • Toggle-Typen unterschieden? Release (temporär), Experiment (mittelfristig), Ops (dauerhaft), Permission (dauerhaft)
  • Release-Toggles mit Ablaufdatum? expires_at gesetzt, CI warnt bei Überschreitung
  • Toggle-Logik zentralisiert? Maximal 2–3 Stellen pro Toggle. Bei mehr: Branching by Abstraction
  • Metriken pro Toggle? Fehlerrate, Latenz, Conversion – zwischen Rollout-Phasen prüfen
  • Aufräum-Prozess definiert? Wer den Toggle anlegt, ist für das Entfernen verantwortlich
  • Toggle-Audit vorhanden? Wer hat wann welchen Toggle umgeschaltet? Production-Änderungen protokolliert
  • Ownership geklärt? Für jeden Toggle-Typ: Wer darf anlegen, umschalten, entfernen
  • Schema-Kompatibilität bedacht? Feature-Toggles schützen Code, aber nicht automatisch inkompatible Datenbankänderungen. Migrationen vorwärts- und rückwärtsverträglich planen

Carola Schulte

Carola Schulte

Software-Architektin mit Fokus auf Release-Architektur, Legacy-Modernisierung und System-Design.

Beratung anfragen

Release-Strategie modernisieren?

Ich analysiere Ihren aktuellen Release-Prozess und entwickle eine Strategie für sichere, häufige Deployments – ob Feature-Toggles, Trunk-Based Development oder beides.

Kostenlose Erstanalyse anfragen