Was ich beim Aufbau von Subscription Manager lernen wollte

Warum ich eine kleine .NET-Anwendung gebaut habe — einfach genug, um sie fertigzustellen, aber real genug, um daraus zu lernen.

Ich arbeite seit vielen Jahren mit .NET, aber im Berufsalltag entsteht Software meistens in einem bereits vorhandenen Umfeld.

Die Architektur ist schon da. Die Infrastruktur ebenfalls. Viele Entscheidungen wurden bereits getroffen, oft lange bevor man zu einem Projekt dazukommt.

Ich wollte ein kleines Projekt haben, in dem ich solche Entscheidungen selbst treffen und anschließend sehen konnte, was passiert, wenn all diese Teile zusammenarbeiten müssen.

So entstand Subscription Manager.

Klein genug, um es fertigzustellen

Subscription Manager begann als recht einfache Anwendung, mit der sich wiederkehrende Abonnements verwalten und ihre Kosten verfolgen lassen.

Die erste Version brauchte keine komplizierte Domäne. Sie brauchte Abonnements, eine API, Persistenz, eine Webanwendung und einige Tests.

Subscription Manager
│
├── Abonnements
├── API
├── Blazor
├── Persistenz
└── Tests

Der begrenzte Umfang war Absicht.

Ich wollte nicht jedes Problem nachbilden, das bei einem kommerziellen SaaS-Produkt auftreten könnte. Das Projekt sollte klein genug bleiben, damit ich es verstehen und fertigstellen konnte, aber gleichzeitig genug Raum für realistischere Probleme bieten.

Abonnements eigneten sich dafür gut.

Der grundlegende Anwendungsfall war einfach, führte aber ganz natürlich zu Benutzern, Konten, Währungen, externen Diensten und schließlich auch zu Zahlungen.

Als die Anwendung realer wurde

Die Authentifizierung war eines der ersten Themen, durch die sich der Charakter der Anwendung veränderte.

Login und Registrierung hinzuzufügen war an sich nicht besonders interessant. Nützlicher waren die Probleme, die rundherum entstanden.

Sitzungen konnten ablaufen. E-Mail-Adressen mussten bestätigt werden. Konten hatten einen eigenen Lebenszyklus. Beim Löschen eines Kontos musste ich außerdem entscheiden, was mit den Daten des Benutzers passieren sollte.

Durch solche Details wurde die Anwendung als Lernprojekt interessanter, weil die einzelnen Funktionen nicht mehr voneinander isoliert waren.

Eine Änderung an einer Stelle konnte sich auf einen anderen Teil der Anwendung auswirken.

Genau das wollte ich erleben.

Externe Systeme veränderten das Projekt

Mit der Zeit kamen mehrere externe Dienste hinzu.

Auf den ersten Blick wirkten sie ähnlich, weil es sich jeweils um APIs handelte. Tatsächlich brachten sie aber sehr unterschiedliche Probleme mit sich.

Integration Was dadurch hinzukam
NBP Externe Referenzdaten und Währungsumrechnung
OpenAI Nichtdeterministische Ergebnisse
Stripe Externer Zustand, Zahlungen und Webhooks

Die Integration mit NBP war vergleichsweise vorhersehbar. Die Anwendung rief Wechselkurse ab und nutzte sie, um die Kosten von Abonnements in verschiedenen Währungen zu vergleichen.

Bei OpenAI war das anders.

Ich ergänzte eine Sparplan-Funktion, die anhand der vorhandenen Abonnements mögliche Wege vorschlug, wiederkehrende Kosten zu reduzieren.

Vom Modell eine brauchbare Antwort zu bekommen, war der einfache Teil. Mehr Arbeit machte es, die Funktion so in die Anwendung einzubauen, dass sie sich wie ein normaler Teil davon verhielt.

Dafür brauchte es Konfiguration, Fehlerbehandlung, Nutzungslimits und Tests für die Teile des Verhaltens, die die Anwendung kontrollieren konnte.

Stripe brachte noch eine andere Art von Grenze mit sich. Ein Zahlungsanbieter konnte nun den Zustand eines Abonnements außerhalb der Anwendung verändern. Dadurch mussten Checkout, Änderungen an Abonnements und Webhooks berücksichtigt werden.

Drei unterschiedliche Dienste in einem kleinen Projekt zu verwenden, war für mich hilfreicher, als drei getrennte API-Beispiele zu bauen.

Als die Anwendung nicht mehr nur auf meinem Rechner lief

Eine Zeit lang lief der größte Teil der Anwendung problemlos auf meinem Entwicklungsrechner.

Der Schritt in Richtung Produktion brachte eine weitere Gruppe von Problemen zum Vorschein.

Die Konfiguration musste für die jeweilige Umgebung stimmen. Die Datenbank musste vorhanden und migriert sein. Secrets brauchten einen sicheren Platz. GitHub Actions musste sich bei Azure authentifizieren können. Und die Anwendung selbst brauchte eine Möglichkeit zu melden, ob sie ordnungsgemäß lief.

Code
 ↓
CI
 ↓
Infrastruktur
 ↓
Konfiguration
 ↓
Deployment
 ↓
laufende Anwendung

Unterwegs mussten einige Dinge korrigiert werden.

Ein OIDC-Subject passte nicht. Eine Datenbankprüfung beim Deployment musste korrigiert werden. Die Konfiguration konnte auf Werte zurückfallen, obwohl ich das nicht wollte. Produktionseinstellungen verhielten sich anders als die lokale Umgebung.

Keines dieser Probleme war besonders dramatisch.

Zusammen waren sie aber eine gute Erinnerung daran, dass eine lokal laufende Anwendung eine ganze Kategorie von Arbeit unsichtbar machen kann.

Ich musste nicht alles bauen

Das Ziel war nie, aus Subscription Manager ein Beispiel für jede Technologie zu machen, die ich kannte.

Die Anwendung brauchte nicht jedes Pattern, jede Infrastrukturkomponente oder jede Funktion, die ein größeres Produkt irgendwann benötigen könnte.

Mehr Dinge hinzuzufügen hätte das Repository größer gemacht, aber nicht unbedingt nützlicher.

Ich wollte genug zusammenwirkende Teile haben, damit ihre Abhängigkeiten real wurden, und das Projekt gleichzeitig klein genug halten, um es weiterhin als Ganzes verstehen und verändern zu können.

Diese Begrenzung wurde selbst zu einem Teil des Experiments.

Wenn ein neuer Baustein einen konkreten Zweck hatte, konnte ich ihn hinzufügen und beobachten, was sich dadurch änderte. Wenn es keinen solchen Zweck gab, musste ich ihn nicht nur deshalb einbauen, damit die Architektur vollständiger aussah.

Was ich daraus mitgenommen habe

Als Subscription Manager schließlich in Produktion lief, war daraus deutlich mehr geworden als die CRUD-Anwendung, mit der ich angefangen hatte.

Es gab Benutzer, Persistenz, externe Integrationen, eine Funktion mit AI-Unterstützung, Zahlungen, Tests, Infrastruktur und einen Deployment-Prozess.

Wichtiger war für mich, dass ich gesehen hatte, wie diese Teile gemeinsam gewachsen waren, anstatt nur die fertige Struktur vor mir zu haben.

Als die Anwendung lief, entstand daraus eine neue Frage.

Wenn ich ein zweites Produkt beginnen würde, welche Teile dieses Projekts würde ich tatsächlich noch einmal verwenden wollen?

Das gesamte Repository zu kopieren wäre einfach gewesen. Interessanter war die Entscheidung, was es wirklich wert war, wiederverwendet zu werden.

Diese Frage führte mich schließlich zu Launchpad.