Software Engineering

Legacy-Software modernisieren, ohne den Betrieb anzuhalten

Warum der Big-Bang-Rewrite scheitert und wie Altsysteme im laufenden Betrieb umgebaut werden: Sicherheitsnetz, Nähte, Strangler Fig, Datenbankmigration.

OSSDL Lab Veröffentlicht 13 Min. Lesezeit

Ein Altsystem lässt sich im laufenden Betrieb umbauen, wenn drei Schritte in dieser Reihenfolge erfolgen: erst ein Sicherheitsnetz aus reproduzierbarem Build, Versionskontrolle, automatisiertem Deployment und Charakterisierungstests; dann eine Fassade vor dem System, hinter der Verkehr umgeleitet werden kann; danach der schrittweise Ersatz einzelner Funktionsbereiche, jeder mit eigener Auslieferung und eigenem Rückweg. Jede dieser Etappen ist einzeln nützlich, auch wenn das Projekt danach für ein Jahr liegen bleibt. Genau das ist der entscheidende Unterschied zum Neubau.

Der Big-Bang-Rewrite – neues System auf grünem Feld, Umschaltung an einem Wochenende – scheitert selten an der Programmierarbeit. Er scheitert daran, dass das Altsystem während der gesamten Bauzeit weiterläuft, gepflegt werden muss und sich fachlich verändert. Zwei Codebasen konkurrieren um die gleichen Entwickler, jede Änderung im Bestand muss im Neubau nachgezogen werden, und die Spezifikation für den Neubau existiert nur an einer Stelle vollständig: im alten Code, einschließlich der Sonderfälle, die niemand mehr erklären kann. Dazu kommt die Ökonomie: Ein Neubau liefert erst am Ende Nutzen und erst am Ende belastbare Erkenntnisse darüber, ob die Annahmen stimmten. Wird das Budget vorher gekürzt, ist der Ertrag null – während ein inkrementeller Umbau nach jeder Etappe einen Zustand hinterlässt, der besser ist als der vorige.

Dieser Artikel beschreibt das Vorgehen konkret: was „Legacy“ technisch bedeutet, wie eine Bestandsaufnahme aussieht, die auf Messdaten statt auf Dokumentation beruht, wie das Sicherheitsnetz gebaut wird, wo ein System aufgeschnitten werden kann, wie das Strangler-Fig-Muster über einen Reverse Proxy praktisch umgesetzt wird, warum die Datenbank der schwierigste Teil ist – und unter welchen Bedingungen ein Rewrite trotz allem die richtige Entscheidung ist.

Was „Legacy“ tatsächlich bedeutet – und was nicht

Alter ist kein Kriterium. Ein COBOL-Batch, der seit den Neunzigern Zinsen berechnet, jede Nacht durchläuft, in einem Repository liegt, aus einer Pipeline gebaut wird und eine Testsuite besitzt, ist kein Problemfall, sondern ein stabiler Vermögenswert. Eine drei Jahre alte Node-Anwendung, deren Build nur auf dem Laptop eines ausgeschiedenen Mitarbeiters funktionierte, ist einer.

Legacy-System

Ein Softwaresystem, dessen Änderung mehr kostet, als der fachliche Nutzen der Änderung rechtfertigt, weil mindestens eine Voraussetzung für sicheres Ändern fehlt: automatisierte Tests als Verhaltensnachweis, ein reproduzierbarer Build, verfügbares Wissen über Fachlogik und Betrieb, oder ein automatisiertes Deployment mit definiertem Rückweg.

Diese vier Lücken erklären das gesamte beobachtete Verhalten solcher Systeme. Ohne Tests ist jede Änderung eine Wette, weil niemand weiß, was sie sonst noch bewirkt – also wird nichts geändert, und die technische Schuld wächst. Ohne reproduzierbaren Build ist unklar, ob das Artefakt im Produktivbetrieb überhaupt aus dem vorliegenden Quellcode entstanden ist; Sicherheitsupdates werden damit unkalkulierbar. Ohne Kenntnisträger fehlt die Unterscheidung zwischen Fachregel und Zufall: Ob eine Rundung auf zwei Stellen eine gesetzliche Anforderung oder ein Bug von 2011 ist, entscheidet über die Zulässigkeit jeder Umsetzung. Und ohne automatisiertes Deployment ist die Auslieferungsfrequenz so niedrig, dass Änderungen in großen, riskanten Paketen gebündelt werden – was die Fehlerquote pro Auslieferung weiter erhöht.

Die praktische Konsequenz: Modernisierung beginnt nicht mit neuer Technologie, sondern mit dem Schließen dieser vier Lücken. Wer eine Anwendung ohne Tests und ohne reproduzierbaren Build in Container packt, hat sie nicht modernisiert, sondern nur verschoben.

Bestandsaufnahme: messen, was läuft, nicht lesen, was dokumentiert ist

Die vorhandene Dokumentation ist in Altsystemen systematisch veraltet, weil sie nicht Teil des Auslieferungsprozesses war. Belastbar sind nur Laufzeitdaten. Drei Quellen liefern innerhalb weniger Tage ein realistisches Bild.

Erstens der eingehende Verkehr: Welche Endpunkte werden tatsächlich aufgerufen, von wem, wie oft? Ein Zugriffslog über vier Wochen zeigt, welche Funktionen in diesem Zeitraum nicht aufgerufen wurden – Kandidaten für toten Code, sofern keine monats-, quartals- oder jahresgebundenen Abläufe darunter liegen.

# Häufigkeitsverteilung der aufgerufenen Pfade aus dem nginx-Zugriffslog
awk '{print $7}' /var/log/nginx/access.log \
  | sed 's/?.*//' \
  | sort | uniq -c | sort -rn | head -40

# Änderungshäufigkeit pro Datei über zwei Jahre: Kandidaten für Umbau
git log --since='24 months ago' --name-only --pretty=format: \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -30

Zweitens die Datenbank. pg_stat_statements zeigt die tatsächliche Lastverteilung und damit die fachlichen Kernabläufe; ungenutzte Tabellen und Indizes markieren Bereiche, die gefahrlos gekapselt werden können.

-- Teuerste Abfragen nach Gesamtzeit, PostgreSQL 17
SELECT calls,
       round(total_exec_time::numeric, 1) AS total_ms,
       round(mean_exec_time::numeric, 2)  AS mean_ms,
       left(query, 120) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 25;

-- Tabellen ohne Lesezugriffe seit dem letzten Statistik-Reset
SELECT relname, seq_scan, idx_scan, n_live_tup
FROM pg_stat_user_tables
WHERE seq_scan + coalesce(idx_scan, 0) = 0
ORDER BY n_live_tup DESC;

Drittens die Peripherie: Cronjobs, Batchläufe, Dateiaustausch über SFTP, Nachtdrucke, Excel-Exporte, die in Fachabteilungen weiterverarbeitet werden. Diese Integrationen sind selten dokumentiert und im Umbau der häufigste Grund für Ausfälle, weil sie erst auffallen, wenn sie fehlen.

Risikokartierung: Änderungshäufigkeit gegen Schadenshöhe

Aus der Bestandsaufnahme entsteht eine Karte mit zwei Achsen: Wie oft muss ein Bereich geändert werden, und wie hoch ist der Schaden, wenn er falsch arbeitet? Häufig geändert und hoher Schaden – dort beginnt der Umbau, denn dort zahlt sich das Sicherheitsnetz sofort aus. Selten geändert und niedriger Schaden bleibt unverändert, unabhängig davon, wie unschön der Code ist. Selten geändert, aber hoher Schaden – etwa die Abrechnung – wird gekapselt und eingefroren, nicht umgebaut. Diese Zuordnung ist die eigentliche Architekturentscheidung; alles Weitere ist Ausführung.

Das Sicherheitsnetz kommt zuerst

Charakterisierungstests statt Spezifikation

Charakterisierungstests beschreiben nicht, was das System tun soll, sondern was es tut – einschließlich der Fehler. Genau das ist ihr Zweck: Sie sichern das aktuelle Verhalten als Bezugspunkt ab, damit ein Umbau messbar verhaltensneutral bleibt. Am schnellsten entstehen sie auf der äußersten Schnittstelle: Anfragen aus dem Produktivverkehr aufzeichnen, gegen eine Referenzinstanz abspielen, Antworten als Golden Master ablegen und bei jeder Änderung vergleichen. Nichtdeterministische Anteile – Zeitstempel, IDs, Zufallswerte – werden vor dem Vergleich normalisiert.

# Golden Master aufzeichnen: Antworten der Altinstanz normalisiert ablegen
while read -r path; do
  curl -s "http://legacy.internal${path}" \
    | jq -S 'del(.timestamp, .requestId)' \
    > "golden/$(echo "$path" | tr '/?&=' '____').json"
done < paths.txt

# Nach dem Umbau: Abweichungen sichtbar machen
diff -ru golden/ candidate/ | head -50

Build reproduzierbar machen

Ein reproduzierbarer Build erzeugt aus demselben Quellstand dasselbe Artefakt – unabhängig von Zeitpunkt und Maschine. Der übliche Bruch liegt in unfixierten Abhängigkeiten: latest-Tags, freie Versionsbereiche, apt-get install ohne Versionsangabe. Fixierung über Digests statt Tags beseitigt die größte Fehlerquelle, weil ein Tag umgehängt werden kann, ein Digest nicht.

# Basisimage per Digest fixieren, nicht per Tag
FROM debian:13-slim@sha256:0000000000000000000000000000000000000000000000000000000000000000

# Exakte Paketversionen, keine Empfehlungen, Zeitstempel neutralisieren
RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      openjdk-21-jre-headless=21.0.8+9-1 \
 && rm -rf /var/lib/apt/lists/*

ENV SOURCE_DATE_EPOCH=1700000000
COPY --link build/app.jar /opt/app/app.jar
ENTRYPOINT ["java", "-jar", "/opt/app/app.jar"]

Der Digest wird beim ersten Pull ermittelt und dann im Repository festgeschrieben. Zur Betriebspraxis von Containern in solchen Umbauten haben wir die Details in Docker im produktiven Einsatz beschrieben; die Pipeline-Seite behandelt CI/CD-Grundlagen.

Deployment automatisieren, bevor Code angefasst wird

Solange eine Auslieferung 40 manuelle Schritte und ein Wartungsfenster braucht, ist jeder Umbauschritt teuer und jeder Rückweg unsicher. Ein automatisiertes Deployment mit definiertem Rollback senkt die Kosten pro Schritt – und damit die sinnvolle Schrittgröße. Das ist der Hebel, der inkrementelle Modernisierung überhaupt möglich macht: Viele kleine Änderungen sind nur dann billiger als eine große, wenn Auslieferung fast nichts kostet.

Nähte finden: wo ein System aufgeschnitten werden kann

Naht (Seam)

Eine Stelle im System, an der Verhalten ersetzt werden kann, ohne den umgebenden Code zu ändern. Typische Nähte sind HTTP-Routen vor der Anwendung, Schnittstellen und Fabrikmethoden im Code, Datenbank-Views zwischen Anwendung und Tabellen sowie Nachrichten-Topics zwischen Komponenten.

Praktisch nutzbar sind vor allem drei Nähte. Die HTTP-Naht liegt vor der Anwendung und erfordert keine Codeänderung – deshalb ist sie der Standardeinstieg. Die Datenbank-Naht entsteht, indem Tabellen umbenannt und durch Views mit dem alten Namen ersetzt werden; die Anwendung merkt nichts, das Schema darunter wird beweglich. Die Nachrichten-Naht ist die tragfähigste, aber teuerste: Sie setzt voraus, dass ein Ablauf ohnehin asynchron werden soll.

Vor jeder Naht steht eine Fassade: eine schmale, dokumentierte Schnittstelle über dem Bestand, die das Innenleben verdeckt. Sie ist der erste Schritt und oft schon der wirtschaftlichste, weil sie neue Entwicklung entkoppelt, ohne den Bestand zu berühren. Der Schnitt entlang fachlicher Grenzen statt technischer Schichten ist dabei die Kernentscheidung – der Gegenstand unserer Arbeit an Softwarearchitektur.

Strangler Fig über einen Reverse Proxy

Das Strangler-Fig-Muster leitet Verkehr routenweise vom Altsystem auf neue Dienste um, bis das Altsystem keinen Verkehr mehr erhält und abgeschaltet werden kann. Der Reverse Proxy ist dabei der Schaltpunkt.

upstream legacy { server 10.0.1.10:8080; }
upstream orders { server 10.0.2.20:3000; }

server {
    listen 443 ssl;
    server_name app.example.org;

    # Bereits ersetzt: Bestellungen
    location /api/orders/ {
        proxy_pass       http://orders;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Alles Übrige unverändert an das Altsystem
    location / {
        proxy_pass       http://legacy;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
  1. Proxy davorschalten, ohne etwas zu ändern. Der Proxy leitet zunächst 100 Prozent an das Altsystem. Diese Etappe ist reine Infrastruktur und muss unauffällig verlaufen, bevor irgendetwas umgeleitet wird.
  2. Einen Bereich auswählen. Klar abgegrenzte Route, überschaubarer Datenzugriff, hohe Änderungsrate, begrenzter Schaden bei Fehlern. Nicht der schwierigste Bereich, aber auch nicht der bedeutungsloseste.
  3. Neu implementieren und spiegeln. Die neue Implementierung parallel mit Produktivanfragen versorgen, ohne ihre Antworten auszuliefern, und die Ergebnisse gegen das Altsystem vergleichen. Abweichungen sind entweder Fehler im Neubau oder unbekannte Fachregeln im Bestand – beides muss geklärt werden.
  4. Anteilig umschalten. Erst intern, dann ein kleiner Verkehrsanteil, dann vollständig. Der Rückweg ist eine Konfigurationsänderung am Proxy, keine Auslieferung.
  5. Den alten Pfad entfernen. Toten Code löschen, sobald er nachweislich keinen Verkehr mehr sieht. Wird dieser Schritt übersprungen, entsteht dauerhafter Parallelbetrieb – der teuerste mögliche Zustand.

Die Datenbank ist der schwierigste Teil

Anwendungscode lässt sich duplizieren und wegwerfen, Daten nicht. Sobald zwei Systeme gleichzeitig dieselben Daten schreiben, entstehen Konsistenzfragen, die auf Anwendungsebene nicht sauber lösbar sind. Drei Verfahren sind praktikabel, mit unterschiedlichen Kosten.

Verfahren Konsistenz Betriebsaufwand Rückweg Geeignet für
Views als Schema-Fassade vollständig, eine Datenbank niedrig einfach, View zurücksetzen Schemaumbau ohne Anwendungsänderung
Change Data Capture eventual, Verzögerung im Sekundenbereich mittel, Connector-Betrieb einfach, Ziel ist nur Leser Leseseite auslagern, Reports, Suche, Migration in Etappen
Dual Writes keine Garantie ohne Transaktionsklammer hoch schwierig, Daten driften nur kurzzeitig und mit Abgleichlauf

Views und Schema-Fassaden

Der günstigste Einstieg bleibt innerhalb einer Datenbank. Tabelle umbenennen, View mit dem alten Namen anlegen, Anwendung unverändert lassen. Danach kann das Schema darunter geändert werden – Spalten aufteilen, Typen korrigieren, Tabellen zerlegen – während die View die alte Sicht stabil hält.

ALTER TABLE kunde RENAME TO kunde_v1;

CREATE VIEW kunde AS
SELECT id, name, strasse, plz, ort
FROM   kunde_v1;

-- Diese einfache View ist in PostgreSQL automatisch schreibbar; INSERT,
-- UPDATE und DELETE der Altanwendung laufen unverändert weiter. Erst wenn
-- das Schema darunter zerlegt wird und die View Joins oder Berechnungen
-- enthält, braucht sie dafür einen INSTEAD OF-Trigger.

Change Data Capture statt Dual Writes

Change Data Capture liest das Transaktionsprotokoll der Datenbank – bei PostgreSQL die logische Replikation, bei MySQL das Binlog – und stellt jede Änderung als Ereignis bereit. Der große Vorteil: Die Altanwendung wird nicht angefasst, und die Reihenfolge der Änderungen entspricht der tatsächlichen Transaktionsreihenfolge. Der neue Dienst baut daraus seinen eigenen Datenbestand auf und ist zunächst reiner Leser, was den Rückweg trivial macht.

Dual Writes – die Anwendung schreibt in beide Systeme – sehen einfacher aus und sind es nicht. Ohne verteilte Transaktion gibt es keinen Zustand, in dem beide Schreibvorgänge garantiert gemeinsam gelingen oder gemeinsam scheitern: Fällt der zweite aus, driften die Bestände; wird er wiederholt, braucht er Idempotenz; bei gleichzeitigen Schreibvorgängen unterscheiden sich die Reihenfolgen zwischen den Systemen. Wer Dual Writes einsetzt, braucht zwingend einen periodischen Abgleichlauf, der Differenzen erkennt und meldet.

Reihenfolge und Strategiewahl

Nicht jeder Bereich braucht dieselbe Behandlung. Die folgende Zuordnung ordnet die gängigen Strategien nach Eingriffstiefe – und der praktische Fehler besteht fast immer darin, eine Zeile weiter unten zu wählen als nötig.

Strategie Was passiert Risiko Wann geeignet
Encapsulate Fassade oder API vor dem unveränderten System niedrig Bestand ist stabil, aber schwer anbindbar; erster Schritt fast immer
Rehost gleicher Code auf andere Infrastruktur verschoben niedrig bis mittel Hardware oder Rechenzentrum am Ende des Lebenszyklus, Zeitdruck
Replatform Laufzeit, Datenbank oder Betriebsmodell gewechselt, Code minimal angepasst mittel Laufzeit ohne Sicherheitsupdates, etwa alte JVM- oder PHP-Stände
Refactor interne Struktur geändert, Verhalten unverändert mittel, mit Tests beherrschbar häufig geänderter Bereich mit hoher Fehlerquote
Rearchitect Zerlegung in Dienste, Schnittstellen neu geschnitten hoch Skalierungs- oder Organisationsgrenze belegbar erreicht
Rebuild Neuimplementierung des gleichen Funktionsumfangs sehr hoch Bereich klein und klar abgegrenzt, Plattform ohne Migrationspfad
Replace Ersatz durch Standardprodukt oder etabliertes Open-Source-System hoch, verlagert auf Prozess und Datenmigration Funktion ist kein Unterscheidungsmerkmal, etwa Ticketing oder Zeiterfassung

Die Reihenfolge über die Bereiche folgt dem Geschäftsrisiko, nicht der technischen Eleganz. Der Bereich mit den meisten Produktionsstörungen und dem höchsten Änderungsdruck kommt zuerst, weil dort jede Verbesserung sofort messbar ist. Ein technisch reizvoller, aber stabiler und selten geänderter Bereich kommt zuletzt – oder nie.

Wissenslücken schließen

Fehlendes Wissen ist kein Nebenproblem, sondern regelmäßig der Engpass. Drei Quellen tragen: der Code selbst als vollständige, aber unkommentierte Spezifikation; Laufzeitdaten, die zeigen, welche Pfade real durchlaufen werden; und die Menschen, die das System benutzen – nicht die, die es gebaut haben.

Wann ein Rewrite die richtige Entscheidung ist

Es gibt Fälle, in denen der inkrementelle Weg teurer ist als der Neubau. Erstens: Die Plattform hat keinen Migrationspfad – eine Anwendung auf einer eingestellten Laufzeitumgebung oder einem proprietären Framework ohne Update lässt sich nicht schrittweise weiterentwickeln, sondern nur einfrieren oder ersetzen. Zweitens: Das System ist klein genug, dass der vollständige Nachbau in wenigen Wochen möglich ist und das Verhalten vollständig über eine Schnittstelle prüfbar bleibt. Drittens: Die fachliche Anforderung hat sich so weit geändert, dass der Bestand nicht mehr das gesuchte Problem löst – dann ist ein Rewrite kein Rewrite, sondern eine Neuentwicklung mit anderem Ziel. Viertens: Es gibt kein Sicherheitsnetz und der Bereich lässt keine Naht zu, etwa bei stark verzahnten Batchprogrammen ohne Schnittstellen.

Auch dann bleibt die Auslieferung inkrementell: Der Neubau geht bereichsweise in Betrieb, hinter derselben Fassade, mit demselben Rückweg. Ein Umschaltwochenende für ein vollständiges System ist unter allen Optionen die mit dem schlechtesten Verhältnis von Risiko zu Erkenntnisgewinn. Wenn Sie diese Abwägung für ein konkretes System durchführen wollen, ist genau das der Ausgangspunkt unserer Arbeit in der Softwareentwicklung und der Linux-Infrastruktur; die Frage der langfristigen Technologiebindung behandeln wir gesondert unter Vendor-Lock-in vermeiden.

Fazit

Modernisierung im laufenden Betrieb ist kein Kompromiss gegenüber dem Neubau, sondern das Verfahren mit der besseren Risikoverteilung: Jede Etappe ist einzeln auslieferbar, einzeln rücknehmbar und einzeln nützlich. Die Arbeit beginnt nicht mit einer Technologieentscheidung, sondern mit vier Grundlagen – Versionskontrolle, reproduzierbarer Build, Charakterisierungstests, automatisiertes Deployment –, weil ohne sie jede Änderung eine Wette bleibt. Der Reverse Proxy macht das Umleiten billig, Views und Change Data Capture machen das Datenschema beweglich, und die Reihenfolge ergibt sich aus Änderungshäufigkeit und Schadenshöhe, nicht aus technischem Reiz. Ein Rewrite bleibt eine legitime Option, aber nur für klar benannte Fälle und auch dann mit inkrementeller Auslieferung. Wer den ersten Schritt sucht: vier Wochen Zugriffslogs auswerten und den Build so weit fixieren, dass er zweimal dasselbe Artefakt erzeugt. Alles Weitere folgt aus dem, was dabei sichtbar wird.

Quellen und weiterführende Dokumentation

  1. Martin Fowler – Strangler Fig Application
  2. PostgreSQL – Logische Replikation
  3. Debezium – Dokumentation zu Change Data Capture
  4. Reproducible Builds – Projektdokumentation
  5. nginx – ngx_http_proxy_module

Themen

  • Legacy
  • Modernisierung
  • Architektur
  • Refactoring
  • Migration

Passende Leistung

Dazu arbeiten wir konkret

Kontakt

Technische Herausforderung besprechen

Beschreiben Sie kurz Ihr System und die offene Frage. Wir antworten mit einer fachlichen Einschätzung – ohne Vertriebsschleife.