Stell dir vor, du übernimmst eine Symfony-3.4-Applikation. 40.000 Zeilen PHP. Kein Test. Controller, die 800 Zeilen lang sind und gleichzeitig die Datenbank befragen, HTML zusammenbauen und E-Mails verschicken. Das System läuft — aber niemand im Team traut sich noch, daran zu rühren.
Ein Rewrite? Ausgeschlossen. Sechs Monate Stillstand, und am Ende hast du dieselben Bugs, nur in neuem Code. Was funktioniert: das Strangler-Fig-Pattern. Ich nutze es seit Jahren, um genau solche Systeme zu übernehmen — ohne Production anzuhalten.
Der Baum, der seinen Wirt ersetzt
Der Name kommt aus der Botanik. Die Würgefeige (Ficus aurea) wächst um einen bestehenden Baum herum, Ast für Ast, bis sie ihn vollständig ersetzt — der alte Baum stirbt ab, der neue steht fest. Martin Fowler hat das Bild 2004 auf Software übertragen: du baust das neue System nicht neben dem alten, sondern um es herum.
In der Praxis heißt das: kein Big-Bang-Release. Kein „wir frieren Features ein, während wir migrieren“. Stattdessen läuft das alte System weiter, und du migrierst Stück für Stück — Route für Route, Modul für Modul.
Wie das in PHP konkret aussieht
Bei der Symfony-3.4-Übernahme haben wir einen Proxy-Layer vorgeschaltet. Jede Request kommt rein, und wir entscheiden: geht das an den alten Monolithen, oder an das neue Symfony-6-Modul?
// config/routes.yaml — neue Route übernimmt, alte bleibt als Fallback
_legacy:
resource: \'../src/Legacy/routes.php\'
prefix: /
invoices_new:
path: /rechnungen/{id}
controller: AppControllerInvoiceController::show
# überschreibt die Legacy-Route für diesen Pfad
Die Legacy-Routes bleiben im System. Aber sobald ein neues Modul eine Route übernimmt, gewinnt die neue Route. Kein Deployment-Risiko — wenn etwas schiefläuft, kommentiere ich die neue Route aus, und der Monolith übernimmt wieder.
Das Facade-Muster als Übergangsschicht
Das Routing allein reicht nicht. Der alte Code hat Zustand, der über Sessions, globale Variablen und manchmal sogar $_GLOBALS fließt. Um das neue Modul von diesem Chaos zu isolieren, baue ich eine dünne Facade:
// src/Legacy/Bridge/InvoiceRepository.php
final class InvoiceRepository
{
public function __construct(
private readonly Connection $db,
) {}
public function find(int $id): Invoice
{
// Ruft die gleiche DB-Tabelle ab wie der alte Code —
// aber über Doctrine DBAL, typsicher, testbar.
$row = $this->db->fetchAssociative(
\'SELECT * FROM invoices WHERE id = ?\',
[$id]
);
if ($row === false) {
throw new InvoiceNotFoundException($id);
}
return Invoice::fromRow($row);
}
}
Der alte Code schreibt weiterhin in die gleiche Tabelle. Das neue Modul liest aus derselben Tabelle — aber über eine saubere Schnittstelle. Keine Datenmigration, kein doppelter Schreibpfad, kein Sync-Problem.
Wann das Pattern funktioniert — und wann nicht
Das Strangler-Fig-Pattern funktioniert gut, wenn:
- das System HTTP-basiert ist (Routes lassen sich einzeln übernehmen)
- die Datenbankstruktur stabil bleibt (kein Schema-Rewrite parallel)
- du Feature Flags oder Route-Overrides nutzen kannst
- das Team parallel liefern darf — neue Features kommen ins neue Modul, Bugfixes noch in den Monolithen
Es funktioniert schlecht, wenn der Monolith ein verteiltes Transaktionsproblem hat — also wenn eine einzelne User-Aktion in 5 verschiedene Tabellen schreibt, die alle konsistent sein müssen. Dort musst du erst die Datenarchitektur klären, bevor du migrierst.
Was danach bleibt
Nach 14 Monaten hatten wir 80 % der Symfony-3.4-Applikation migriert. Die restlichen 20 % — ein altes Reporting-Modul, das niemand mehr nutzte — haben wir abgeschaltet. Der „neue“ Code war zu dem Zeitpunkt schon 14 Monate in Production. Keine große Migration, kein Freeze, kein Risiko-Release.
Das ist der Kern des Patterns: du migrierst im laufenden Betrieb, und das System ist zu jedem Zeitpunkt deploybar. Nicht am Ende — die ganze Zeit.