Czego chciałem się nauczyć, budując Subscription Manager

Dlaczego zbudowałem małą aplikację .NET — na tyle prostą, żeby ją ukończyć, ale na tyle rzeczywistą, żeby móc się na niej czegoś nauczyć.

Pracuję z .NET od wielu lat, ale większość oprogramowania zawodowo tworzy się w już istniejącym środowisku.

Architektura już jest. Infrastruktura również. Wiele decyzji zostało podjętych wcześniej, często na długo przed dołączeniem do projektu.

Chciałem mieć mały projekt, w którym sam będę mógł podejmować takie decyzje, a potem zobaczyć, co się stanie, kiedy wszystkie te elementy będą musiały ze sobą współpracować.

Tak powstał Subscription Manager.

Coś wystarczająco małego, żeby to ukończyć

Subscription Manager zaczął się jako dość prosta aplikacja do zarządzania cyklicznymi subskrypcjami i śledzenia ich kosztów.

Pierwsza wersja nie potrzebowała skomplikowanej domeny. Potrzebowała subskrypcji, API, persystencji, aplikacji webowej i kilku testów.

Subscription Manager
│
├── subskrypcje
├── API
├── Blazor
├── persystencja
└── testy

Ograniczony zakres był celowy.

Nie próbowałem odtworzyć wszystkich problemów, jakie może mieć komercyjny produkt SaaS. Chciałem zbudować coś na tyle małego, żebym mógł to zrozumieć i ukończyć, a jednocześnie zostawić miejsce na pojawienie się bardziej rzeczywistych problemów.

Subskrypcje dobrze się do tego nadawały.

Podstawowy przypadek użycia był prosty, ale w naturalny sposób prowadził do użytkowników, kont, walut, zewnętrznych usług, a w końcu także płatności.

Kiedy aplikacja stała się bardziej rzeczywista

Uwierzytelnianie było jedną z pierwszych rzeczy, które zmieniły charakter aplikacji.

Samo dodanie logowania i rejestracji nie było szczególnie interesujące. Ciekawsze problemy pojawiły się wokół nich.

Sesje mogły wygasać. Adresy e-mail wymagały potwierdzenia. Konta miały swój cykl życia. Usunięcie konta oznaczało również konieczność zdecydowania, co powinno się stać z danymi użytkownika.

Dzięki takim szczegółom aplikacja stawała się bardziej przydatna jako projekt do nauki, ponieważ poszczególne funkcje przestawały być od siebie odizolowane.

Zmiana w jednym miejscu mogła wpływać na inną część aplikacji.

Właśnie tego chciałem doświadczyć.

Zewnętrzne systemy zmieniły projekt

Z czasem aplikacja została zintegrowana z kilkoma zewnętrznymi usługami.

Na początku wyglądały podobnie, bo wszystkie były API, ale każda z nich wprowadzała zupełnie inny rodzaj problemu.

Integracja Co wprowadziła
NBP Zewnętrzne dane referencyjne i przeliczanie walut
OpenAI Niedeterministyczne wyniki
Stripe Zewnętrzny stan, płatności i webhooki

Integracja z NBP była stosunkowo przewidywalna. Aplikacja pobierała kursy walut i używała ich do porównywania kosztów subskrypcji w różnych walutach.

OpenAI działało inaczej.

Dodałem funkcję planu oszczędnościowego, która na podstawie danych o subskrypcjach sugerowała możliwe sposoby ograniczenia cyklicznych wydatków.

Uzyskanie użytecznej odpowiedzi od modelu było łatwą częścią. Więcej pracy wymagało sprawienie, żeby ta funkcja zachowywała się jak normalna część aplikacji.

Potrzebowała konfiguracji, obsługi błędów, limitów użycia oraz testów tych elementów zachowania, które aplikacja mogła kontrolować.

Stripe wprowadził jeszcze inny rodzaj granicy. Dostawca płatności mógł teraz zmienić stan subskrypcji poza aplikacją, co oznaczało konieczność obsługi checkoutu, zmian subskrypcji i webhooków.

Użycie trzech różnych usług w jednym małym projekcie dało mi więcej niż zbudowanie trzech osobnych przykładów integracji z API.

Kiedy aplikacja przestała działać tylko na moim komputerze

Przez pewien czas większość aplikacji wygodnie działała na moim komputerze.

Przeniesienie jej w stronę środowiska produkcyjnego odsłoniło kolejną grupę problemów.

Konfiguracja musiała być poprawna dla konkretnego środowiska. Baza danych musiała istnieć i mieć wykonane migracje. Sekrety trzeba było gdzieś przechowywać. GitHub Actions musiał uwierzytelniać się w Azure. A sama aplikacja potrzebowała sposobu na informowanie, czy działa prawidłowo.

kod
 ↓
CI
 ↓
infrastruktura
 ↓
konfiguracja
 ↓
wdrożenie
 ↓
działająca aplikacja

Po drodze kilka rzeczy wymagało poprawienia.

OIDC subject nie pasował. Sprawdzenie bazy danych podczas wdrożenia wymagało korekty. Konfiguracja mogła korzystać z wartości zapasowych w sposób, którego nie chciałem. Ustawienia produkcyjne zachowywały się inaczej niż lokalne.

Żaden z tych problemów nie był szczególnie dramatyczny.

Razem były jednak dobrym przypomnieniem, że uruchamianie aplikacji lokalnie ukrywa całą kategorię pracy.

Nie musiałem budować wszystkiego

Celem nigdy nie było zrobienie z Subscription Managera przykładu każdej technologii, którą znałem.

Nie potrzebował każdego wzorca, każdego elementu infrastruktury ani każdej funkcji, której większy produkt mógłby kiedyś potrzebować.

Dodawanie kolejnych rzeczy powiększyłoby repozytorium, ale niekoniecznie uczyniłoby je bardziej użytecznym.

Chciałem mieć wystarczająco dużo współpracujących elementów, żeby ich wzajemne zależności były rzeczywiste, a jednocześnie zachować projekt na tyle mały, żebym nadal mógł rozumieć i zmieniać go w całości.

To ograniczenie stało się częścią eksperymentu.

Kiedy nowy element miał konkretne zastosowanie, mogłem go dodać i zobaczyć, co się zmieni. Kiedy takiego zastosowania nie było, nie widziałem powodu, żeby dodawać go tylko po to, by architektura wyglądała na bardziej kompletną.

Co z tego wyniosłem

Kiedy Subscription Manager działał już na produkcji, był czymś znacznie większym niż aplikacja CRUD, od której zacząłem.

Miał użytkowników, persystencję, zewnętrzne integracje, funkcję wykorzystującą AI, płatności, testy, infrastrukturę i proces wdrożenia.

Co ważniejsze, widziałem, jak te elementy rozwijały się razem, zamiast patrzeć wyłącznie na gotową strukturę.

Kiedy aplikacja już działała, pojawiło się kolejne pytanie.

Gdybym zaczynał drugi produkt, które części tego projektu rzeczywiście chciałbym wykorzystać ponownie?

Skopiowanie całego repozytorium byłoby łatwe. Ciekawsze było zdecydowanie, co naprawdę warto wykorzystać ponownie.

To pytanie ostatecznie doprowadziło mnie do Launchpada.