Forschung & Entwicklung (R&D)

Dein erster R&D-Prototyp braucht keinen Rewrite

Ein Forschungsprototyp funktioniert auf deinem Laptop, aber die Anwendung läuft seit Jahren mit echten Daten und festen Erwartungen. Ich würde die neue Idee deshalb nicht als neuen Kern der Anwendung einbauen, sondern als austauschbare Entscheidung an genau einer bestehenden Stelle. So kann das Team ihren Nutzen prüfen, ohne für ein Experiment die Architektur neu zu schreiben.

Ein guter Einstiegspunkt ist kleiner als der Prototyp

Angenommen, deine Anwendung sortiert eingehende Aufgaben bislang nach Alter. Das R&D-Team hat einen Algorithmus entwickelt, der zusätzlich die Zahl früherer Fehlversuche berücksichtigt. Der naheliegende Impuls ist, die gesamte Verarbeitungspipeline zu übernehmen, weil der Prototyp dort bereits funktioniert. Genau das würde ich nicht tun: Ein übernommener Prototyp verändert leicht auch Validierung, Datenzugriff und Fehlerbehandlung, obwohl eigentlich nur die Reihenfolge getestet werden soll.

Suche stattdessen die Funktion, die heute aus einer Liste gültiger Aufgaben eine Reihenfolge macht. Diese Funktion ist dein Einstiegspunkt. Schreibe vor der Änderung auf, was sie bekommt und zurückgibt: etwa Aufgaben mit stabiler ID, Alter und Fehlerzahl als Eingabe sowie dieselben IDs in sortierter Reihenfolge als Ausgabe. Bleibt dieser Vertrag gleich, können Aufrufer unverändert bleiben und du kannst beide Varianten mit denselben Eingaben vergleichen.

Ein solcher Einstiegspunkt muss nicht perfekt abstrahiert sein. Liegt die Sortierung derzeit mitten in einer längeren Methode, reicht zunächst eine kleine Extraktion, sofern die vorhandenen Tests danach unverändert bestehen. Ein großes Interface mit mehreren Implementierungen wäre hier voreilig, weil du noch nicht weißt, ob der neue Algorithmus bleibt. Die Diskussion um R&D in der Softwareentwicklung: Trends und Best Practices kannst du ergänzend lesen; für diesen Eingriff ist die konkrete Ein- und Ausgabe der Funktion aussagekräftiger als eine allgemeine Best Practice.

Halte Datenbankzugriffe außerhalb beider Sortierfunktionen. Das ist keine Stilfrage: Wenn die experimentelle Variante zusätzliche Datensätze aus PostgreSQL 16 lädt, vergleichst du nicht mehr nur zwei Sortierregeln, sondern auch zwei Abfragewege. Fehlen ihr benötigte Felder, ergänze sie zunächst am bestehenden Ladepfad und prüfe dessen Kosten separat. Ein Redis-7-Cache wäre kein Ersatz für diese Prüfung, weil ein Cache-Treffer die Mehrkosten nur verdecken kann.

Bevor du Code änderst, sichere zwei oder drei reale, anonymisierte Eingaben als Testfälle. Notiere auch die bislang erwartete Ausgabe und Sonderfälle wie gleiche Werte oder leere Listen. Diese Beispiele beweisen nicht, dass der neue Ansatz besser ist; sie verhindern aber, dass schon die Vorbereitung unbemerkt das bisherige Verhalten verändert.

Der erste Schalter soll Verhalten isolieren, nicht Infrastruktur ersetzen

Für den ersten Schritt genügt ein standardmäßig ausgeschalteter Schalter direkt am Einstiegspunkt. Das folgende Beispiel läuft mit Python 3.11 ohne zusätzliche Pakete. Setzt du beim Start RND_RANK_V1=1, wird die alternative Reihenfolge ausgegeben; ohne die Variable bleibt das alte Verhalten aktiv.

import os

def rank(items, use_experiment=False):
    baseline = sorted(items, key=lambda x: x["age"], reverse=True)
    if not use_experiment:
        return baseline
    return sorted(items, key=lambda x: x["age"] - 2 * x["errors"], reverse=True)

items = [{"id": "a", "age": 4, "errors": 2},
         {"id": "b", "age": 3, "errors": 0}]
enabled = os.getenv("RND_RANK_V1", "0") == "1"
print([item["id"] for item in rank(items, enabled)])

Die Gewichtung mit 2 ist hier ein zu justierender Beispielwert, keine belegte optimale Einstellung. Gerade deshalb gehört sie in einen gezielten Test und nicht stillschweigend in die Produktion. In einer echten Anwendung sollten beide Varianten außerdem dieselbe Regel für Gleichstände verwenden, etwa die stabile Aufgaben-ID als zweites Sortierkriterium. Sonst kann ein anderer Gleichstand wie ein Forschungsergebnis aussehen.

Ein Umgebungswert reicht für einen lokalen Versuch oder einen getrennten Testprozess. Für eine schrittweise Freigabe über mehrere Instanzen wäre OpenFeature als einheitliche Schnittstelle für Feature-Flags geeigneter, weil jede Instanz ihre Entscheidung nach derselben Regel treffen muss; dafür kommen allerdings ein Flag-Provider und dessen Betrieb hinzu. Unabhängig vom Werkzeug gehört der Ausgangszustand in die Konfiguration: Bei fehlendem oder ungültigem Wert bleibt die bisherige Sortierung aktiv.

Teste zunächst den Vertrag statt nur das Beispiel. Mit pytest 8 prüfst du, dass keine ID verloren geht, keine hinzukommt und leere Eingaben funktionieren. Hypothesis 6 kann viele Listen mit doppelten Werten erzeugen, weil gerade dort instabile Sortierungen auffallen. Erwarte dabei ausdrücklich nicht, dass beide Funktionen dieselbe Reihenfolge liefern; andernfalls würde der Test das beabsichtigte Experiment verbieten.

Shadow Mode gewinnt vor einem Canary, wenn Folgen schwer rückgängig sind

Für den ersten Einsatz in der laufenden Anwendung bevorzuge ich Shadow Mode: Die bisherige Funktion bestimmt die sichtbare Reihenfolge, während die neue Funktion dieselbe Eingabe nur zur Auswertung erhält. Diese Wahl ist angreifbar, denn Shadow Mode verdoppelt an dieser Stelle Rechenarbeit und sagt noch nichts darüber, wie Menschen auf eine andere Reihenfolge reagieren. Er gewinnt trotzdem, wenn eine falsche Entscheidung Aufgaben tatsächlich umpriorisieren würde, weil er zunächst keine solche Entscheidung auslöst.

Ein Canary ist die andere Option: Ein kleiner Teil der Anfragen erhält die neue Reihenfolge tatsächlich. Er gewinnt, wenn du Auswirkungen auf das spätere Verhalten messen musst, die ein bloßer Vergleich von Listen nicht zeigen kann. Sein Preis ist reales Produktrisiko; außerdem brauchst du eine stabile Zuordnung, damit dieselbe Person nicht bei jedem Aufruf zwischen Varianten wechselt. Als vorsichtigen, zu kalibrierenden Startanteil würde ich 1 % wählen, nicht weil diese Zahl allgemein sicher wäre, sondern weil sie die erste betroffene Gruppe begrenzt.

Im Shadow Mode dürfen Nebenwirkungen nicht zweimal stattfinden. Rufe die neue Sortierfunktion daher mit einer unveränderlichen Kopie der bereits geladenen Daten auf, aber lasse sie weder Aufträge speichern noch Benachrichtigungen versenden. Falls der Prototyp solche Aktionen voraussetzt, ist er noch keine geeignete Shadow-Implementierung: Trenne erst die Berechnung von ihren Folgen. Das ist ein kleinerer Eingriff als eine Neufassung der gesamten Pipeline.

Begrenze auch die Zusatzlast. Angenommen, eure bestehende Messung zeigt über 7 Tage eine p95-Latenz von 80 ms am Einstiegspunkt: Dann wäre ein zunächst festgelegtes Budget von 10 ms zusätzlicher p95-Zeit eine überprüfbare Versuchsschranke, keine Garantie. Miss die Laufzeit beider Varianten getrennt, denn ein schneller Gesamtdurchschnitt kann einzelne besonders große Eingaben verbergen.

Für die Zuordnung von Messwerten über Dienste hinweg kannst du W3C Trace Context und das OpenTelemetry SDK für Python verwenden. Erfasse pro Aufruf die gewählte Variante und eine gemeinsame Trace-ID, aber keine vollständigen Aufgabeninhalte, weil der Vergleich IDs und Zeitwerte benötigt, nicht personenbezogene Nutzdaten. Prometheus und Grafana reichen für Latenz- und Fehlerratenkurven; die veröffentlichte Prometheus-Konfigurationsreferenz nennt 1m als Standardwert für scrape_interval, den du bei kurzen Experimenten bewusst prüfen solltest.

Eine andere Reihenfolge ist noch kein besseres Ergebnis

Im Shadow Mode lässt sich einfach zählen, wie oft neue und alte Ausgabe voneinander abweichen. Diese Quote ist nützlich, weil sie bestätigt, dass das Experiment überhaupt Entscheidungen verändert; sie belegt keinen Nutzen. Wenn sich nur Listen mit identischen Prioritätswerten unterscheiden, prüfe zuerst die Gleichstandsregel, bevor du eine fachliche Verbesserung vermutest.

Definiere vor dem Canary ein beobachtbares Ziel. Für die Aufgabenliste könnte das die Zeit bis zur Bearbeitung dringender Aufgaben sein; als Schutzgröße eignet sich die Zeit bis zur Bearbeitung aller übrigen Aufgaben. Beide Größen brauchst du, weil der neue Algorithmus dringende Aufgaben bevorzugen und dabei andere unbeabsichtigt zurückstellen kann. Halte im Experimentprotokoll fest, welche Ereignisse Beginn und Ende einer Bearbeitung markieren, damit niemand später eine günstigere Definition auswählt.

Miss zusätzlich Fehlerquote und p95-Latenz je Variante. Eine niedrigere Bearbeitungszeit wäre wenig wert, wenn dafür Anfragen scheitern oder die Antwortzeit steigt. Für das erste Review könnt ihr beispielsweise festlegen, dass die Fehlerrate höchstens um 0,2 Prozentpunkte steigen darf; dieser Grenzwert ist eine Teamentscheidung und muss zu eurer Ausgangslage passen. Trenne dabei technische Fehler von fachlich unerwünschten Entscheidungen, weil ein HTTP-Erfolg keine sinnvolle Priorisierung beweist.

Vermeide eine weitere Falle: Werte nur Anfragen aus, bei denen beide Varianten mit denselben verfügbaren Feldern arbeiten konnten. Fehlte bei der Hälfte der Aufgaben die Fehlerzahl, könnte ein scheinbarer Vorteil aus einer verzerrten Teilmenge stammen. Protokolliere deshalb auch, wie viele Eingaben ausgeschlossen wurden und aus welchem Grund. Eine Trendprognose ersetzt diese Prüfung nicht; ergänzend kannst du R&D Trends in der Softwareentwicklung 2026 lesen. Der Titel mag neue Verfahren nahelegen, doch für diese Codebasis zählt zuerst, ob ihre vorhandenen Daten den Vergleich tragen.

Ein Experiment ist erst fertig, wenn auch sein Rückweg feststeht

Schreibe vor der Freigabe auf, wer den Schalter zurücksetzt, welches Signal den Abbruch auslöst und wie lange die Beobachtung dauert. Ohne diese Abmachung kann ein auffälliger Wert zur Diskussion werden, während die betroffene Variante weiterläuft. Der Rückweg sollte ohne Deployment funktionieren, wenn ihr einen zentralen Flag-Provider nutzt; mit einer Umgebungsvariable ist dagegen meist ein Neustart nötig, den ihr einplanen müsst.

Ein Canary braucht außerdem eine feste Zuordnung pro Nutzer oder Aufgabe. Wechselt die Variante bei jedem Request, vermischen sich die Ergebnisse und ein Rollback lässt sich schwer einer betroffenen Gruppe zuordnen. Verwende für diese Zuordnung eine bereits vorhandene stabile ID und dokumentiere die Regel. Eine neue Tabelle allein für die Gruppenzuteilung würde ich vermeiden, solange eine deterministische Zuordnung genügt, weil sie zusätzliche Schreibvorgänge und eine Migration erzeugt.

Nach dem Versuch gibt es mehr als „an“ oder „aus“. Bei einem klaren Nutzen und eingehaltenen Schutzgrößen kannst du den Anteil schrittweise erhöhen. Bei unklaren Daten bleibt die alte Variante aktiv, während du Messlücken schließt. Bei einem negativen Ergebnis entfernst du Algorithmus und Schalter wieder, weil ungenutzte Versuchspfade spätere Änderungen unnötig komplizieren. Halte Entscheidung und Messzeitraum im Repository fest, damit die nächste Person den alten Schalter nicht für ein dauerhaftes Produktmerkmal hält.

Öffne als Erstes die Stelle, an der deine Anwendung heute ihre Entscheidung trifft, und zeichne ihre Ein- und Ausgabe auf. Lege danach einen pytest-Test mit einer realistischen Eingabe und der bisherigen Ausgabe an. Wenn dieser Test ohne neuen Algorithmus grün ist, hast du einen sicheren Ausgangspunkt: Die Forschungsfrage kann beginnen, ohne dass du zugleich einen Rewrite verantworten musst.