Latem 2025 napisałem na tym blogu, że świadomie odkładam własną bibliotekę DDD na półkę. Nie była skończona, ale była wystarczająco dobra, a ja miałem produkt do budowania. Ostatni commit padł we wrześniu. Potem cztery miesiące ciszy.
W styczniu otworzyłem repo biblioteki pierwszy raz od jesieni. Nie po to, żeby do niej wrócić z planem. Po to, żeby naprawić jedno słowo.
Pole, które przez całą jesień kłuło mnie w oczy
Każde zdarzenie domenoweZdarzenie domenowe (domain event)Fakt, który zaszedł w domenie (np. „użytkownik zarejestrowany”). Inne części systemu mogą na niego zareagować.
czytaj więcej →
— fakt, że coś się stało, np. „użytkownik się
zarejestrował”, „ogłoszenie zostało dodane” — ma w bibliotece pole, które
opisuje, czym jest to zdarzenie. Latem nazwałem je pospiesznie: eventType.
Brzmiało rozsądnie. Typ zdarzenia. Tyle.
Po miesiącach kodu opartego o tę bibliotekę zacząłem zauważać, że ta nazwa
mnie myli. Słowo „type” sugeruje klasę, kategorię, wariant z enuma —
coś, co grupuje. A pole nie trzymało żadnej grupy. Trzymało nazwę
konkretnego zdarzenia — UserRegistered, OfferPublished. To była identyfikacja
instancji typu zdarzenia, nie typ.
Drobiazg. Kosmetyka. Dokładnie ten rodzaj zmiany, który łatwo odłożyć na „kiedyś”, bo przecież działa. Działało przez cztery miesiące i jeszcze przez kolejne cztery by działało. Po prostu za każdym razem, gdy pisałem nowego subscribera albo nową asercję w testach, na mikrosekundę musiałem sobie przypomnieć, że tutaj „type” znaczy co innego niż w reszcie świata TypeScripta. Każda taka mikrosekunda jest tania osobno. Razem przez cztery miesiące to była już rachunek za drobne irytacje.
Cudzy kontrakt to cudzy problem
Gdyby ta biblioteka była zewnętrzna — @nestjs/cqrs, czyjś pakiet z npm —
miałbym wybór. Przypiąć starą wersję, poczekać na fork, otworzyć issue
i wzruszyć ramionami. Cudza nazwa, cudza decyzja, cudze problemy.
Ale ten kontrakt jest mój. Sam go napisałem, sam go konsumuję, sam zdecydowałem, że stara nazwa mi przeszkadza. Nie ma do kogo napisać zgłoszenia. Jest tylko biblioteka leżąca na półce — i produkt, który stoi na każdej jej linijce.
To było dla mnie nowe. Cztery miesiące wcześniej, gdy pisałem post o świadomej pauzie, myślałem o bibliotece jak o narzędziu, które się odkłada — jak młotek, po który się sięga, kiedy trzeba przybić gwóźdź. Po pół roku codziennego użycia zacząłem ją widzieć inaczej: to nie jest młotek. To jest język, w którym mówi mój produkt. Drobna pomyłka w tym języku zaczyna z czasem brzmieć fałszywie w każdym zdaniu, które się w nim napisze.
Wieczór, w którym to po prostu zrobiłem
Sama zmiana w bibliotece zajęła kilkanaście minut. Refaktor pola, zmiana wersji na nową dużą — taką, która jawnie krzyczy „uwaga, stary kod się nie skompiluje”. Zbudowanie biblioteki, opublikowanie wewnętrznie.
Potem przeszedłem do produktu, przepiąłem wersję biblioteki na nową — i kompilator zaczął wymiotować błędami. Pierwsza kontrola: jak wiele miejsc to dotknie. Odpowiedź: ponad sto sześćdziesiąt plików. Każdy emitter zdarzeń, każdy subscriber, każdy mapper, każdy test — wszystko, co kiedykolwiek czytało to jedno pole pod starą nazwą.
Usiadłem do tego z prostym planem: nie myśl, nie improwizuj, idź za błędami kompilatora. Otwierasz plik, zamieniasz, zapisujesz, lecisz dalej. Trochę jak sprzątanie po remoncie — nie projektujesz pokoju od nowa, po prostu zbierasz gruz, jeden kawałek po drugim.
Po niecałych dwóch godzinach kompilator umilkł. Wtedy zaczęły się testy — i to one są właściwą historią tego wieczoru. Każdy z ponad szesnastu tysięcy testów chwilę po starcie biegnie obok jakiegoś zdarzenia. Jeśli o czymś zapomniałem, jeśli gdzieś przeoczyłem stare nazewnictwo, jeśli mapper serializuje pole inaczej w produkcji niż w teście — testy mówią o tym natychmiast, czerwoną listą po prawej stronie ekranu.
Pierwsze przejście: kilkaset czerwonych. Druga runda, po naprawie kilku wzorców — kilkadziesiąt. Trzecia — kilkanaście, wszystkie w jednym module, który serializował zdarzenia do bazy starym mapperem. Po jego poprawie ekran wreszcie poszedł zielony.
Jeden wieczór. Ale ten „jeden wieczór” to nie była moja zasługa, tylko zasługa testów, które kiedyś musiałem napisać. Gdyby ich nie było, taka zmiana w stu sześćdziesięciu plikach byłaby grą w ruletkę: skompilowałoby się, wyglądało na działające, a w produkcji wybuchłoby tydzień później na zdarzeniu, którego nie sprawdziłem. Cały komfort tej nocy kupiłem wcześniej — pisząc testy, których wtedy nie chciało mi się pisać.
Luksus zera użytkowników
Łatwo opowiedzieć ten wieczór jak heroizm. Solo, w nocy, sto sześćdziesiąt plików, zielone testy nad ranem. Tylko że to nie był heroizm — to był luksus.
Nie mam jeszcze prawdziwych użytkowników. Gdyby ten system był na produkcji z żywymi danymi i ludźmi, którzy w środku środkowej nocy są zalogowani i coś robią, zmiana kontraktu w setkach miejsc byłaby operacją na otwartym sercu: wersjonowanie kontraktu, okres przejściowy, kompatybilność wsteczna, migracja emitowanych zdarzeń w kolejkach. Tygodnie roboty, nie jeden wieczór.
Dziś to po prostu wieczór i git. Mogę zmienić zdanie i poprawić nazwę,
której sam się dorobiłem, bo nikt po drugiej stronie tego nie odczuje. To
jest jeden z bardzo nielicznych przywilejów etapu, na którym jestem — i staram
się go nie marnować na zwlekanie z rzeczami, które za pół roku byłyby
nieodwracalne.
Co naprawdę znaczyła pauza
Najciekawsze nie jest to, że biblioteka wróciła. Ciekawsze jest to, jak wróciła: nie z planu, nie z roadmapy, nie z notesu pomysłów. Wróciła, bo produkt jej potrzebował konkretnie tego konkretnego dnia. Cztery miesiące półki to nie był okres, w którym o niej zapomniałem — to był okres, w którym żyłem z jej kontraktem na co dzień i powoli rozumiałem, czego brakuje.
Bo to jest hydraulika. Rury pod podłogą, których nikt nigdy nie zobaczy. Dopiero gdy mieszkasz w domu kilka miesięcy, wiesz, gdzie są źle położone i co naprawdę warto przerobić. Plan zrobiony na początku, zanim w domu zamieszkałeś, byłby grą w zgadywanie.
Kilka uwag praktycznych, dla zainteresowanych warsztatem
- Czyste cięcie zamiast warstwy zgodności. Mogłem zostawić starą nazwę jako alias i utrzymywać dwie nazwy obok siebie przez jakiś czas. Świadomie tego nie zrobiłem — przy jednym konsumencie (moim własnym API) warstwa zgodności to dług, nie ułatwienie. Duża wersja biblioteki jawnie sygnalizuje: stary kod się nie skompiluje, i dobrze, niech się nie kompiluje.
- Migracja sterowana kompilatorem i testami. Zamiana nazwy → kompilator pluje błędami → idziesz za błędami, jeden po drugim → testy mówią, kiedy skończyłeś. Bez tych dwóch warstw byłaby to praca po omacku.
- Symetria po obu stronach tego samego dnia. Najpierw zmiana w bibliotece i nowa wersja, potem konsument tego samego wieczoru. Kontrakt i jego konsument muszą iść w parze — inaczej zostaje rozjazd, który ugryzie tygodnie później, gdy nikt już nie pamięta, czemu pole nazywa się tak a nie inaczej.
- Nazwa to API. Sześć liter, w stu sześćdziesięciu miejscach. Każdy, kto
kiedykolwiek będzie czytał ten kod — łącznie ze mną za pół roku — szybciej
zrozumie
eventNameniżeventType. Refaktor nazwy to nie kosmetyka, to zmiana w interfejsie publicznym biblioteki.