Architektura systemu bez chaosu: decyzje, zespół i dowód, że zmiana ma sens

Praktyczny warsztat o architekturze: kiedy jest potrzebna, kto powinien ją prowadzić, jak podejmować decyzje, jak dokumentować granice i jak przejąć cudzy system bez zgadywania. Z mapą myśli, kodem, pytaniami i mini-grą decyzyjną.

Poniżej znajduje się pełna treść artykułu. Quizy i symulacje wymagają włączenia JavaScript.

Warsztat architektoniczny bez teatru

Dobra architektura zaczyna się wtedy, gdy przestajemy zgadywać.

Nie od diagramu. Nie od nazwy frameworka. Nie od hasła "mikroserwisy". Najpierw trzeba odpowiedzieć na proste, trudne pytanie: co w tym systemie będzie zmieniało się często, co musi zostać stabilne, a czego nie wolno zepsuć przy drugim wdrożeniu?

Architektura jest dobra wtedy, gdy firma może zmieniać produkt bez paniki. Gdy zespół wie, gdzie leży odpowiedzialność. Gdy płatność, formularz, panel klienta, e-mail i raport nie są sklejone w jedną kulę, której nikt nie chce dotknąć przed weekendem.

Co architektura robi

Ustala granice, zapisuje decyzje, zmniejsza koszt zmian i mówi zespołowi, co ma się stać, gdy coś pójdzie źle.

Czego architektura nie robi

Nie zastępuje myślenia, testów, odpowiedzialności ani rozmowy z biznesem. Sama dokumentacja nie utrzyma produkcji.

Kiedy zaczyna być pilna

Gdy jedna poprawka wymaga dotknięcia wielu modułów, a nikt nie umie przewidzieć skutków ubocznych.

Definicja, która działa w praktyce

Architektura to decyzje, które trudno później odkręcić.

Framework można zmienić. Kolor przycisku można zmienić. Nazwę endpointu też da się poprawić. Dużo trudniej zmienić model danych, granice odpowiedzialności, sposób integracji z płatnościami, politykę uprawnień, pipeline wdrożeniowy albo zasady obserwowalności.

Jeżeli decyzja ma wysoki koszt cofnięcia, powinna być decyzją architektoniczną. Jeżeli nie ma wysokiego kosztu cofnięcia, nie róbmy z niej wielkiej narady. To prosta zasada, ale ratuje projekty przed dwoma skrajnościami: chaosem bez zasad i biurokracją bez wartości.

Mapa myśli Dobra decyzja architektoniczna
ProblemCo naprawdę boli firmę?
DaneCo jest źródłem prawdy?
GranicaGdzie kończy się odpowiedzialność modułu?
AwariaCo robimy, gdy provider milczy?
DowódJak sprawdzimy, że działa?
CofnięcieJak wrócimy do stabilnego stanu?

Co jest potrzebne

Pięć rzeczy, bez których architektura zostaje tylko rysunkiem.

cel biznesowy granice procesu właściciel decyzji dowód działania zapis powodu wyboru

Najczęściej brakuje nie technologii, tylko odpowiedzialności. Zespół wie, że "trzeba zrobić integrację", ale nikt nie zapisuje, kto odpowiada za retry, co się dzieje przy duplikacie, gdzie trafia błąd, kto odbiera wynik i kiedy uznajemy temat za zamknięty.

Dlatego architektura powinna być krótka, czytelna i związana z prawdziwym przepływem. Jeśli klient płaci za audyt, system ma wiedzieć, co oznacza płatność, kiedy uruchomić pomiar, jak zapisać raport, jak wysłać informację i co pokazać, gdy któryś etap nie wyszedł.

Kiedy wystarczy lekki porządek, a kiedy potrzebna jest formalna architektura?

Sytuacja Wystarczy Nie wolno pominąć
Strona marketingowa Standard komponentów, SEO, formularze, analityka. Backup treści, bezpieczeństwo formularzy, monitoring błędów.
Panel klienta Jasne role, sesje, logi, audyt zmian. Uprawnienia, retencja danych, reset hasła, testy E2E.
Checkout i płatności Osobny przepływ zamówienia i odbioru. Idempotencja webhooka, podpisy, refund, status płatności.
Integracje ERP/API Kontrakty, kolejki, retry, mapa danych. Obsługa duplikatów, DLQ, zgodność danych, wersjonowanie.
System regulowany ADR, security review, kontrola dostępu, ślad audytowy. RODO, powierzenie danych, retencja, procedury incydentu.

Ludzie i odpowiedzialność

Kto powinien wprowadzać architekturę i ile osób powinno brać udział?

Najgorszy model to jedna osoba, która "wymyśla architekturę" i wrzuca ją zespołowi jako prawdę objawioną. Drugi zły model to zebranie dwunastu osób, gdzie każda ma opinię, a nikt nie bierze odpowiedzialności.

Dla większości projektów wystarczy mały skład: osoba od celu biznesowego, techniczny właściciel backendu i danych, ktoś od frontendu albo doświadczenia użytkownika, osoba od operacji i bezpieczeństwa oraz, jeśli trzeba, ktoś znający domenę: sprzedaż, księgowość, logistykę albo obsługę klienta.

3-4 osobymała aplikacja, jedna domena, szybkie decyzje
5-6 osóbpanel klienta, płatności, integracje, kilka ról
6-8 osóbdane wrażliwe, wiele systemów, audyt i formalny odbiór

Więcej osób nie oznacza lepszej architektury. Często oznacza wolniejsze decyzje i odpowiedzialność rozmytą między wszystkich. Lepiej mieć mały zespół, który potrafi podjąć decyzję, zapisać powód i udowodnić wynik.

Zagadka 01

Klient płaci za audyt, ale analiza strony może potrwać kilka minut. Co robisz?

Uruchamiam cały audyt w webhooku Stripe.

Zapisuję płatność i publikuję zdarzenie do outboxa.

Pokazuję komunikat "raport wkrótce" bez kolejki.

Wybierz decyzję i sprawdź konsekwencję.

Przykład z produkcji

Mała funkcja, duży koszt, jeśli granice są złe.

Wyobraźmy sobie prosty przepływ: klient podaje adres strony, płaci za audyt, system uruchamia analizę, zapisuje wynik, wysyła e-mail i pokazuje raport w panelu. Na slajdzie brzmi łatwo. W produkcji od razu pojawiają się pytania: co jeśli Stripe wyśle webhook dwa razy, e-mail nie wyjdzie, audyt potrwa dłużej, klient odświeży stronę po płatności albo raport powstanie, ale zapis do bazy się nie uda?

Architektura zaczyna się dokładnie tutaj. Nie przy wyborze biblioteki. Przy odpowiedzi na to, jak system zachowa się w realnym świecie.

Kod, który ostrzega

Krótki kod też może robić za dużo.

Ten przykład wygląda niewinnie, ale miesza płatność, audyt, raport, e-mail i provider zewnętrzny w jednym miejscu. Po awarii trudno powiedzieć, co już się wydarzyło.

public async Task CompleteOrder(Guid orderId)
{
    var order = await db.Orders.FindAsync(orderId);
    order.Status = "Paid";

    var audit = await crawler.Run(order.TargetUrl);
    order.ReportJson = JsonSerializer.Serialize(audit);

    await email.Send(order.Email, "Raport gotowy", audit.Summary);
    await stripe.MarkProcessed(order.PaymentIntentId);
    await db.SaveChangesAsync();
}

Lepsza granica

Najpierw decyzja, potem skutki uboczne.

Transakcja powinna zapisać fakt i zamiar dalszej pracy. Audyt, e-mail i integracje mogą działać osobno, z retry, monitoringiem i jasnym statusem dla klienta.

public sealed record ConfirmAuditPayment(
    Guid OrderId,
    string PaymentIntentId,
    DateTimeOffset ConfirmedAt);

public sealed class ConfirmAuditPaymentHandler
{
    public async Task Handle(ConfirmAuditPayment command)
    {
        var order = await orders.Get(command.OrderId);

        order.MarkAsPaid(command.PaymentIntentId, command.ConfirmedAt);
        outbox.Add(new AuditRequested(order.Id, order.TargetUrl));
        outbox.Add(new CustomerNotificationRequested(order.Id));

        await unitOfWork.SaveChanges();
    }
}

SOLID bez religii

Zasady mają pomagać, a nie przykrywać brak decyzji.

Single Responsibility mówi: jeśli zmiana w e-mailu psuje płatność, odpowiedzialności są pomieszane. Open/Closed mówi: jeśli każdy nowy provider wymaga przeróbki połowy systemu, brakuje kontraktu. Dependency Inversion mówi: jeśli domena zależy od szczegółu technicznego, to szczegół zaczyna rządzić produktem.

W praktyce wystarczy zacząć od prostych kontraktów, które nie kłamią:

public interface AuditReportStore
{
    Task Save(AuditReport report, CancellationToken cancellationToken);
}

public interface AuditNotifier
{
    Task NotifyReportReady(Guid orderId, CancellationToken cancellationToken);
}

public sealed class CompleteAuditHandler
{
    public async Task Handle(AuditCompleted completed, CancellationToken ct)
    {
        await reports.Save(completed.Report, ct);
        await notifier.NotifyReportReady(completed.OrderId, ct);
    }
}

Mini-gra

Ułóż przepływ decyzji architektonicznej.

0/6

Plan awarii i retry

Cel biznesowy

Test, monitoring i rollback

Model danych i źródło prawdy

Granice procesu

Kontrakt integracji

Ułóż kroki w kolejności. Zacznij od celu, nie od technologii.

Wyczyść odpowiedzi

Przejęcie cudzego systemu

Nie zaczynaj od refaktoryzacji. Najpierw zrozum, co wolno zepsuć.

Przejęcie istniejącego kodu to nie jest szukanie winnych ani polowanie na brzydkie klasy. To praca detektywistyczna. Trzeba znaleźć przepływy krytyczne, dane źródłowe, integracje, właścicieli decyzji i miejsca, gdzie brak testu może kosztować klienta pieniądze.

Uruchom aplikację lokalnie Znajdź logowanie, płatności, formularze i integracje Narysuj mapę danych Dopisz test charakteryzujący Ustal kryteria odbioru przed zmianą

Test charakteryzujący nie mówi, że stary kod jest piękny. On mówi: "tak system działa dzisiaj i tego nie wolno przypadkiem zmienić".

[Fact]
public async Task Paid_order_requests_audit_only_once_for_duplicate_webhook()
{
    var paymentIntentId = "pi_123";

    await app.HandleStripeWebhook(paymentIntentId);
    await app.HandleStripeWebhook(paymentIntentId);

    var order = await app.FindOrderByPayment(paymentIntentId);

    Assert.Equal(OrderStatus.Paid, order.Status);
    Assert.Single(await app.Outbox.EventsOfType<AuditRequested>(order.Id));
}
Zagadka 02

Masz przejąć moduł bez testów. Co robisz pierwszego dnia?

Refaktoryzuję od razu najbrzydszą klasę.

Piszę test charakteryzujący krytyczny przepływ.

Czytam README i zakładam, że opis jest aktualny.

Wybierz odpowiedź i zobacz, co to oznacza w utrzymaniu systemu.

ADR w praktyce

Decyzja musi mieć powód, konsekwencje i dowód.

Nie zapisujemy decyzji po to, żeby tworzyć dokumentację dla dokumentacji. Zapisujemy ją po to, żeby za trzy miesiące nie zgadywać, dlaczego system działa właśnie tak.

ADR-004: Audyt uruchamiamy asynchronicznie po płatności

Kontekst:
Audyt strony może trwać od kilkunastu sekund do kilku minut.
Checkout musi zakończyć się szybko.

Decyzja:
Po webhooku Stripe zapisujemy zamówienie jako opłacone
i publikujemy zdarzenie AuditRequested do outboxa.

Konsekwencje:
Panel pokazuje status "w kolejce".
Worker może ponowić audyt po błędzie.
Potrzebujemy monitoringu kolejki i martwych zadań.

Dowód:
Test duplikatu webhooka, test restartu workera,
log korelacji orderId.

Uruchom preflight decyzji

Gotowe do sprawdzenia. Kliknij i zobacz, czy decyzja ma pełny ślad.

Pytania kontrolne

Jeśli nie umiesz odpowiedzieć, architektura jeszcze nie jest gotowa.

01Jakie zdarzenie biznesowe rozpoczyna proces?
02Które dane są źródłem prawdy?
03Kto może zaakceptować kosztowną decyzję?
04Co dzieje się przy duplikacie requestu?
05Jak klient zobaczy status pracy?
06Jaki test zamyka odbiór?

Wniosek

Dobra architektura jest najbardziej widoczna wtedy, gdy coś idzie źle.

Jeśli system potrafi jasno pokazać status, powtórzyć bezpiecznie operację, zachować dane, wysłać alert, cofnąć zmianę i dać człowiekowi decyzję w dobrym miejscu, to architektura zaczyna zarabiać na siebie.

Nie dlatego, że jest modna. Dlatego, że zmniejsza koszt chaosu i pozwala zespołowi dowozić zmiany bez strachu.

Wynik decyzji w tym warsztacie 0/2

Zacznij od decyzji. Każda dobra architektura powinna dać się obronić w realnym scenariuszu, a nie tylko na diagramie.

Reset warsztatu