Ein Softwaremodul wird zukunftsfähig: Refactoring von gewachsenem C++ zu modernen Value Semantics

1.9.2026
Zwei Bugs. Zwei ganze Tage verloren. Die Lösung war nicht der nächste Patch, sondern ein gezielt modernisiertes Kernmodul. Eine Fallstudie.

von Tim Varelmann

Diese Fallstudie wird mit Genehmigung von Lucky Data geteilt.

Ergebnisse auf einen Blick

  • Eine ganze Klasse wiederkehrender Bugs ist jetzt architektonisch unmöglich. Weniger böse Überraschungen bei der Entwicklung, planbare Software-Releases.
  • Weniger und einfacherer Code bedeutet ab hier günstigere und schnellere Entwicklung.
    • Fast die Hälfte des fehleranfälligsten Codes ist weg, bei gleichzeitig mehr Funktionalität. Zusätzlich deutliche Vereinfachungen im nachgelagerten Code.
    • Eine ganze Gruppe von Hilfsklassen ist obsolet geworden und wurde komplett entfernt. Laufender Wartungsaufwand, der schlicht nicht mehr anfällt.
  • Streaming vom Backend hat die Bulk-Loads abgelöst, die Nutzer beim Start ausgebremst haben. Der erste Bildschirm zeigt echte Daten fast sofort.

Über den Kunden

Lucky Data ist ein deutsches IT-Unternehmen, das IT-Dienstleistungen anbietet. Darüber hinaus entwickelt Lucky Data Logistik-Dispositionssoftware, die modernisiert, wie Disponenten die Logistik in der Bauindustrie und in Binnenhäfen planen. Die Software ist im Einsatz, wird laufend weiterentwickelt und in regelmäßigen Releases ausgeliefert. Der Anspruch dabei: schneller auf Kundenanforderungen reagieren, ohne dass Releases unberechenbar werden. Lucky Data investiert bewusst in technische Qualität und langfristige Produktstabilität. Bluebird Optimization unterstützt diese Entwicklung.

Diese Fallstudie beschreibt die Modernisierung eines gewachsenen Kernmoduls: von stark pointer-basiertem C++ hin zu modernen Value Semantics. Der Umbau schließt eine ganze Kategorie von Bugs aus, halbiert den Umfang des fehleranfälligsten Codes nahezu und hat eine schnellere, reaktivere Nutzererfahrung möglich gemacht. Für ein Unternehmen, auf dessen Software täglich wichtige operative Entscheidungen beruhen, verkürzt diese Kombination (weniger Fehler, leichter änderbar, schneller für den Nutzer) den Weg von neuen Kundenanforderungen zu ausgelieferten Features.

--

Über Monate hinweg lief die Entwicklung rund. Neue Features gingen in regelmäßigen Releases raus. Nichts Dramatisches, nichts brannte an. Ruhiges Fahrwasser, welches erlaubt, zu planen, was als Nächstes kommt, statt sich den Kopf zu zerbrechen über das, was gerade kaputt ist.

Anfang März 2026 änderte sich das. Zweimal innerhalb von acht Tagen. Und beide Male ging es im Kern um dieselbe Frage: Wem gehören diese Daten eigentlich?

Der erste Bug

Er tauchte in Code auf, der sich noch in Entwicklung befand: ein hartnäckiger Bug mit reproduzierbarem Symptom und ohne offensichtliche Ursache. Ich habe einen ganzen Tag damit verbracht, ihn zu suchen, und hatte am Abend nichts als einen Patch: ein Stück Code, das das Symptom eliminierte, ohne es zu erklären. Mit einem Release am Horizont und anderen noch offenen Aufgaben entschied ich, die Ursachensuche aufzuschieben.

Ein ganzer Tag war weg, ohne etwas Vorzeigbares außer einem Fix, den ich nicht vollständig rechtfertigen konnte. Entsprechend unruhig war ich am Abend. Wenn man nicht erklären kann, warum ein Bug auftrat, kann man auch nicht wirklich sicher sein, dass er weg ist.

Der zweite Bug

Eine Woche später, der nächste Bug. Andere Oberfläche, gleiches Gefühl: schwer einzugrenzen, Symptome, die nicht sauber zu dem Code passten, der sie eigentlich hätte produzieren sollen. Wieder fast ein ganzer Tag, wieder ein Patch, wieder Symptome entfernt ohne sie zu verstehen. Der Release stand unmittelbar bevor. Der Patch ging rein, und der Release ging raus.

Nach dem Release habe ich beide Bugs nebeneinandergelegt. Ihre Ähnlichkeit war kein Zufall: andere Auslöser, aber dieselbe Handschrift. Es ging um eines: geteilter Zugriff auf Daten, ohne dass klar ist, wem sie gehören.

Eine unbequeme Chance

Der Code war rund um Pointer designt, mit geteiltem Zugriff auf Daten, der sich über große Teile des Codes zog. Symptome, die weit entfernt von ihrer Ursache auftauchen, kommen fast immer von solchen Pointern.

Die Schlussfolgerung war unbequem, aber sie war vor allem eine Chance: Diese Struktur passte nicht mehr zu dem, was die Software heute tut und was in den nächsten Releases von ihr verlangt wird. Solange sie unverändert blieb, würde sie weiter Bugs ermöglichen, die unter anderen Namen wiederkommen. Änderte man sie, fiel diese ganze Kategorie weg.

Genau so habe ich es Lucky Data vorgelegt. Nicht als Notfall, sondern als Wahl zwischen zwei Wegen: weiter patchen und diese Bug-Kategorie behalten, oder das Kernmodul einmal gezielt modernisieren und sie loswerden.

Kurzer Exkurs: Stack, Heap und warum Pointer so schwer nachzuvollziehen sind

Um zu erklären, was sich geändert hat, ein kurzer Abstecher dazu, wie Programme Daten speichern:
Computerprogramme halten Daten im Speicher, und der Speicher hat zwei Hauptbereiche: den Stack und den Heap.
Daten auf dem Stack gehören einem bestimmten Stück Code - der Funktion, die sie erzeugt hat. Nur diese Funktion kann sie lesen oder verändern.
Daten auf dem Heap leben für sich. Jedes Stück Code mit einem Pointer (im Wesentlichen eine Adresse, die dem Programm sagt, wo die Daten liegen) kann sie lesen oder verändern. Dasselbe Stück Heap-Daten kann viele Pointer haben, die von vielen Stellen im Programm aus darauf zeigen.
Was daran ist nun problematisch? Auf den ersten Blick klingt das doch in Ordnung: Wenn zwei Teile eines Programms dasselbe Datum verändern, wird jeder seinen guten Grund dafür haben. Und individuell betrachtet stimmt das meistens auch. Das Problem ist nicht die einzelne Änderung. Das Problem ist, dass Entwickler nicht mehr lokal denken können.
Ein Beispiel: Man stelle sich Code vor, der entscheidet, ob LKW 42 um 15 Uhr verfügbar ist. Er liest die Daten: "LKW 42, frei", und beginnt mit dem Zusammenstellen eines Dispositionsauftrags. Zwischen dem Moment des Lesens und dem Moment der echten Disposition markiert ein anderer Teil des Programms, ebenfalls aus einem völlig validen Grund, LKW 42 als in Wartung. Der dispositionierende Code hat keine Möglichkeit, davon zu erfahren. Der LKW wird trotzdem zugewiesen.
Für sich betrachtet sind beide Codeteile korrekt. Der Bug lebt im Raum zwischen ihnen. Und Debugging heißt, jeden Teil des Programms zu verfolgen, der möglicherweise einen Pointer auf LKW 42 hält. In einer ausgewachsenen Codebasis können das Dutzende oder Hunderte von Stellen sein.
Das heißt nicht, dass Pointer schlecht sind. Jeder, der schon einmal eine Browser-Extension, ein Microsoft-Office-Add-in oder ein Spielemod installiert hat, hat von ihnen profitiert: Die ganze Kategorie "ein laufendes Programm um etwas erweitern, das es ursprünglich nicht kannte" beruht auf pointer-artigen Mechanismen. Pointer sind an der richtigen Stelle die richtige Wahl.
Für den zentralen Datenspeicher, um den es hier geht, gab es aber keinen solchen Grund. Die Komplexität der Pointer war reiner Kostenfaktor, ohne Nutzen.

Im gewachsenen Kernmodul fielen drei Dinge auf

Geteilter Datenzugriff, für den es keinen heutigen Bedarf gibt

Die Codebasis griff zu Pointern als Standardwerkzeug, auch in diesem Teil des Codes. Für das, was der zentrale Datenspeicher tatsächlich leistet, braucht es diese Freiheit nicht. Übrig bleibt genau der oben beschriebene Kostenfaktor: geteilter Datenzugriff ohne klaren Besitzer, Lebenszyklen, die Stück für Stück rekonstruiert werden mussten, und der Raum zwischen je zwei Zugriffen als potenzieller Bug.

Mutexes als Narbengewebe

Mutexes sind Koordinationsmechanismen für parallel laufenden Code: Sie verhindern, dass zwei Ausführungs-Threads sich gegenseitig in die Daten pfuschen. Aber zurzeit läuft dieser Teil der Software auf einem einzigen Thread. Jeder Mutex darin war irgendwann in der Vergangenheit als Patch zu einem Bug hinzugefügt worden, der wie eine Race Condition aussah. Sie waren Überbleibsel alter Feuerwehreinsätze und bremsten den Code aus.

Vererbung als einziger Zugang zu den Daten

Der Code nutzte eine abstrakte Basisklasse als einzigen Zugang zu den unterschiedlichen Datenobjekten, die in der Software verarbeitet werden. Vererbung ist ein Mechanismus, der unterschiedlichen Dingen eine übergeordnete Kategorie gibt. Sie ist nützlich, wenn man diese einheitliche Behandlung wirklich braucht, aber in C++ zieht sie Pointer praktisch zwangsläufig nach sich: Verschiedene Typen über eine gemeinsame Kategorie zu behandeln erfordert Pointer. Dazu kommen Laufzeitkosten durch die dynamische Bindung, und der Code wird schwerer nachvollziehbar.

Das Refactoring

Mit steigenden Anforderungen an Release-Geschwindigkeit, Wartbarkeit, neue Kundenwünsche und die geplante Streaming-Fähigkeit sprach viel dafür, jetzt umzubauen statt weiter zu patchen. Lucky Data hat sich dafür entschieden.

Abgesichert haben wir den Umbau über Zeitpunkt und Zuschnitt. Er startete direkt nach einem Release, also mit dem größtmöglichen Abstand zum nächsten Termin. Ich habe zwei Wochen Vollzeit veranschlagt und in diesen zwei Wochen nichts anderes geplant. Die übrigen Entwickler haben parallel weiter an Features gearbeitet, ein Entwicklungsstopp war nicht nötig.

Zwei Entscheidungen haben den größten Teil der Arbeit gemacht.

Der zentrale Datenspeicher hält seine Daten jetzt per value

Wenn ein Teil der Anwendung ein Datenobjekt braucht, fragt er über eine eindeutige ID an und bekommt eine Kopie. Die Kopie gehört dem Aufrufer (und liegt auf dessen Stack). Niemand sonst kann hineingreifen und etwas ändern. Wenn der Aufrufer fertig ist, verschwindet die Kopie einfach: keine Buchführung, keine Lecks, keine Überraschungen.

Komposition statt Vererbung

Wir haben uns für den Grundsatz prefer composition over inheritance entschieden: Statt dass die verschiedenen Datenobjekte einen gemeinsamen Vorfahren teilen, enthalten sie jetzt jeweils ein kleines gemeinsames Stück (die Eigenschaften, die sie tatsächlich teilen) und sind ansonsten unabhängige Typen.

Die übergeordnete Kategorie, die früher der einzige Zugang zu diesen Daten war, steht weiterhin zur Verfügung für Code, der die verschiedenen Datenobjekte wirklich einheitlich behandeln muss: sie ist jetzt mit einem modernen C++17-Mechanismus namens Visitor-Pattern umgesetzt, von dem Entwickler nicht einmal wissen müssen, um ihn zu nutzen. Entscheidend aber: sie ist nicht mehr die einzige Tür. Teile der Anwendung, die wissen, mit welcher Kategorie von Datenobjekten sie zu tun haben, können jetzt gezielt danach fragen. Weniger "über alle Kategorien iterieren und anschließend filtern". Das macht die Software geradlinig und lesbarer.

Was sich in der Praxis geändert hat

  • Eine ganze Klasse von Bugs ist weg. Nicht seltener, unmöglich. Lifetime-Probleme, stille Datenkorruption durch geteilten Zustand, Symptome, die wie Parallelitäts-Bugs in nicht-parallelem Code riechen: Sie alle setzen geteilten Zugriff voraus. Den gibt es nun nicht mehr, also gibt es auch die Bugs nicht mehr.
  • Fast die Hälfte der Codezeilen in der Implementierung des zentralen Datenspeichers ist verschwunden, bei gleichzeitig gewachsenem Funktionsumfang. Was bleibt, ist Code, den neue Entwickler lesen und verstehen können, ohne sich zuerst eine mentale Karte davon aufzubauen, wer sonst noch woran rührt.
  • Nachgelagerter Code wurde einfacher. Der Data Provider, der eine zentrale Kalenderansicht speist, hat rund ein Viertel seines Codes verloren. Andere UI-Modelle folgten dem gleichen Muster in kleinerem Umfang.
  • UI-Updates wurden zielgerichteter. Benachrichtigungen über Änderungen sind jetzt nach Datenobjekt-Kategorien getrennt. Komponenten, die nur eine Kategorie interessiert, müssen die Updates der anderen nicht mehr abonnieren und anschließend rausfiltern.
  • Eine ganze Hilfsklassen-Hierarchie wurde gelöscht. Das frühere Design brauchte spezielle Klassen nur dafür, inkrementelle Daten-Updates durch die Oberfläche zu transportieren. Die sind weg: die am einfachsten zu wartenden Klassen sind die, die es nicht gibt :)
  • Ein geplantes Feature kam deutlich früher als erwartet. Streaming vom Backend stand ohnehin auf der Roadmap, um die großen initialen Ladevorgänge abzulösen. Mit dem Wegfall der Pointer-Koordination war die Anwendung ohne weitere Vorarbeit bereit, Datenströme zu konsumieren. Ein erheblicher Teil der Streaming-Entwicklung war damit als Nebeneffekt des Refactorings schon erledigt. Inzwischen ist auch die Backend-Seite ausgeliefert, und Nutzer sehen ihren ersten Bildschirm mit echten Daten fast sofort.

Zusammengefasst: Eine ganze Fehlerkategorie ist dauerhaft erledigt, das Kernmodul ist leichter, schneller und günstiger wart- und weiterentwickelbar, und ein geplantes Feature ist deutlich früher live gegangen als vorgesehen. Die veranschlagten zwei Wochen fokussierter Arbeit haben wir sogar leicht unterschritten.

Rolf Ruß, Geschäftsführer von Lucky Data, bewertet diese Entwicklung wie folgt:

"Die neue Architektur ist der Hammer, sie macht unseren Entwicklern das Leben leichter. Somit gehen weitere Releases schneller durch."

Von der kurzfristigen Fehlerbehebung zur nachhaltigen Weiterentwicklung

Die eigentliche Arbeit bestand darin, die Architektur als das zu behandeln, was sie ist: die Entscheidung darüber, welche Bugs überhaupt programmierbar sind. Die Aspekte zu identifizieren, die nicht mehr zu den heutigen und zukünftigen Anforderungen passen, und ihnen die Zeit zu geben, die ihr Austausch braucht, war hier der Schlüssel.

Der Alternativpfad ist ja bekannt: Man patcht weiter. Jeder einzelne Patch ist für sich vertretbar, jeder einzelne kostet nur einen Nachmittag. Und irgendwann kostet das Feature, das man eigentlich bauen wollte, ein Quartal statt zwei Wochen.

Lucky Data entwickelt seine Plattform kontinuierlich weiter und beseitigt strukturelle Risiken früh, statt sie mitzuschleppen. Das schafft die Grundlage für zuverlässigere Releases, kürzere Entwicklungszyklen und eine schnellere Umsetzung von Kundenanforderungen. Bluebird Optimization unterstützt diese Weiterentwicklung mit der technischen Expertise für solche Umbauten: von der Diagnose bis zur umgesetzten Architektur.

Heute ist das Entwicklerteam bei Lucky Data wieder in ruhigem Fahrwasser und plant in dieser Ruhe, was als Nächstes kommt.

Kommt dir davon etwas bekannt vor?

Bugs, die unter verschiedenen Gestalten immer wiederkommen. Stellen im Code, vor denen alle ein bisschen Respekt haben. Ein Feature, das nicht am Aufwand hängt, sondern an der Struktur darunter.

Dann lass uns sprechen: Buch dir 30 Minuten in meinem Kalender. Wir legen deine letzten wiederkehrenden Bugs nebeneinander und prüfen, ob sie dieselbe Handschrift haben. Wenn ja, sage ich dir, was es kostet, diese Kategorie loszuwerden.

Und wenn du erstmal mitlesen willst, abonniere Bluebird Briefings, meinen Newsletter zu Optimierungs- und Softwareentwicklungs-Themen wie diesem.

Weitere Beiträge

Erfolgsgeschichte: Optimierung von Lagerbestand unter Unsicherheit

Als enge Deadlines auf komplexe Unsicherheit in einem wegweisenden Lageroptimierungsprojekt trafen, wurde aus der Herausforderung durch Vertrauen und Innovation eine Chance. Das Ergebnis: eine siebenstellige Reduzierung der Lagerkosten bei gleichzeitiger Verbesserung der Materialverfügbarkeit.

8.11.2025
Schnellere Lösungen, süße Belohnungen: Mein Solver-Tuning-Erfolg beim Gurobi Summit

Beim Gurobi Summit in Wien wurde eine Solver-Tuning-Challenge zu einem Wettlauf um Performance – und um eine Sachertorte. Wie gezieltes Parametertuning, mir Kuchen und wertvolle Optimierungserkenntnisse brachte.

5.11.2025

Starten Sie Ihr Projekt noch heute

Moderne Software und mathematische Präzision bringt Sie Ihren Zielen näher.
Jetzt Projektanfrage stellen