Gdy aplikacja zaczyna obsługiwać rabaty, limity, statusy zamówień albo uprawnienia, szybko okazuje się, że samo zapisanie danych nie wystarcza. To reguły decydują, co użytkownik może zrobić, kiedy operacja jest poprawna i jakie skutki powinna wywołać. W tym artykule pokazuję, czym jest logika biznesowa aplikacji, gdzie ją umieszczać w projekcie .NET, jakie wzorce pomagają ją uporządkować oraz jak unikać kodu, którego później nikt nie chce dotykać.
Dobra architektura chroni reguły biznesowe przed chaosem
- Logika biznesowa opisuje zasady, według których aplikacja podejmuje decyzje.
- Kontroler powinien przyjąć żądanie, ale nie powinien znać wszystkich reguł domeny.
- Model domenowy sprawdza się przy złożonych zasadach, a prosty serwis aplikacyjny wystarczy przy nieskomplikowanym CRUD-zie.
- Clean Architecture i architektura heksagonalna izolują reguły od bazy danych, frameworka i zewnętrznych API.
- Testy jednostkowe powinny sprawdzać najważniejsze decyzje biznesowe bez uruchamiania całej aplikacji.
Co naprawdę należy do logiki biznesowej
Logika biznesowa to część kodu, która odpowiada na pytanie, co w danej sytuacji wolno zrobić. Składają się na nią reguły, ograniczenia, obliczenia i decyzje wynikające z działalności firmy, a nie z technologii użytej do napisania systemu.
W sklepie internetowym będzie to na przykład sprawdzenie, czy produkt jest dostępny, czy klient może użyć kodu rabatowego i czy zamówienie można przełączyć ze stanu „nowe” na „opłacone”. W systemie kadrowym podobne reguły określą, ile dni urlopu pozostało pracownikowi i czy przełożony może zaakceptować dany wniosek.
Najprostszy test jest praktyczny. Jeżeli regułę można opisać językiem domeny, prawdopodobnie należy do modelu biznesowego. Jeżeli opis dotyczy protokołu HTTP, tabeli SQL albo formatu JSON, mówimy raczej o infrastrukturze lub warstwie technicznej.
| Przykład | Typ odpowiedzialności | Gdzie zwykle powinien trafić |
|---|---|---|
| Klient nie może wykorzystać wygasłego kuponu | Reguła biznesowa | Model domenowy lub serwis domenowy |
| Żądanie ma nagłówek Authorization | Obsługa techniczna | Middleware lub warstwa API |
| Zamówienie przechodzi do statusu „wysłane” | Reguła procesu | Encja, agregat lub serwis aplikacyjny |
| Obiekt jest zapisywany w SQL Server | Dostęp do danych | Repozytorium lub adapter infrastruktury |
W praktyce najwięcej problemów powstaje wtedy, gdy te kategorie mieszają się w jednym miejscu. Kontroler zaczyna liczyć rabat, odpytuje bazę, wywołuje operatora płatności i ustawia status encji. Taki kod może działać, ale każda zmiana wymaga później ostrożnego przekopywania całego przepływu.
Gdzie umieścić reguły w aplikacji .NET
Nie ma jednej architektury dobrej dla każdego projektu. Inaczej zaprojektuję mały panel administracyjny z kilkoma formularzami, a inaczej system zamówień, w którym reguły zmieniają się co tydzień i mają wpływ na pieniądze. Rozmiar kodu nie jest jedynym kryterium. Ważniejsza jest złożoność decyzji, które aplikacja musi podejmować.
W prostym projekcie wystarczy często klasyczny podział na warstwę API, serwisy aplikacyjne, dostęp do danych i modele. Serwis aplikacyjny koordynuje wtedy przypadek użycia, na przykład „utwórz produkt”, a repozytorium ukrywa szczegóły bazy danych.
Przy większej liczbie reguł lepiej oddzielić domenę od technologii. W Clean Architecture zależności kierują się do środka, czyli w stronę modelu domenowego. Architektura heksagonalna stosuje podobną ideę, używając portów i adapterów. Port to kontrakt, a adapter to konkretna implementacja, na przykład klient REST albo repozytorium Entity Framework Core.
| Podejście | Gdzie trafiają reguły | Kiedy ma sens | Ryzyko |
|---|---|---|---|
| Prosta architektura warstwowa | Serwisy i modele domenowe | Małe i średnie aplikacje CRUD | Serwisy mogą stać się klasami „od wszystkiego” |
| Clean Architecture | Warstwa domeny i przypadki użycia | Systemy rozwijane przez dłuższy czas | Więcej projektów, interfejsów i kodu pomocniczego |
| Architektura heksagonalna | Rdzeń domeny niezależny od adapterów | Wiele integracji i potrzeba łatwego testowania | Abstrakcje mogą być przesadne przy prostych funkcjach |
| DDD | Encje, obiekty wartości, agregaty i serwisy domenowe | Złożone domeny z istotnymi regułami | Niepotrzebna formalizacja prostych ekranów |
Sam nie zaczynam projektu od tworzenia pięciu warstw tylko dlatego, że wygląda to profesjonalnie. Najpierw sprawdzam, gdzie znajduje się ryzyko. Jeśli aplikacja ma głównie odczyty i proste formularze, rozbudowane DDD może spowolnić pracę. Gdy jednak reguły są krytyczne, izolacja domeny szybko zwraca koszt początkowej organizacji.

Jak poprowadzić przypadek użycia od API do domeny
Dobrym sposobem projektowania jest rozpoczęcie od przypadku użycia, a nie od tabeli w bazie. Załóżmy, że użytkownik chce złożyć zamówienie. Aplikacja powinna sprawdzić pozycje, wyliczyć kwotę, zweryfikować reguły rabatowe, zarezerwować towary i dopiero potem zapisać wynik.
Kontroler może przyjąć dane wejściowe i przekazać je do handlera albo serwisu aplikacyjnego. Nie powinien jednak sam decydować, czy zamówienie może zostać złożone. Ta decyzja należy do obiektu, który zna zasady zamówienia.
public sealed class Order
{
private readonly List _lines = new();
public OrderStatus Status { get; private set; } = OrderStatus.Draft;
public void Submit()
{
if (_lines.Count == 0)
throw new DomainException("Zamówienie nie zawiera produktów.");
if (Status != OrderStatus.Draft)
throw new DomainException("Tego zamówienia nie można już złożyć.");
Status = OrderStatus.Submitted;
}
} To niewielki przykład, ale pokazuje ważną granicę. Metoda Submit chroni niezmienniki, czyli warunki, które zawsze muszą być prawdziwe dla zamówienia. Dzięki temu reguła nie zależy od tego, czy zamówienie zmienia kontroler HTTP, zadanie w tle czy test jednostkowy.
Model domenowy powinien pilnować własnych zasad
Encja nie musi zawierać całej logiki systemu, ale powinna bronić się przed niepoprawnym stanem. Obiekt wartości, taki jak Money, EmailAddress albo DateRange, dobrze nadaje się do zamknięcia małej reguły i walidacji.
Jeśli operacja dotyczy kilku obiektów i nie pasuje naturalnie do jednej encji, można użyć serwisu domenowego. Przykładem będzie wyliczenie ceny zależne od koszyka, klienta i aktualnej kampanii. Serwis domenowy powinien nadal operować na pojęciach biznesowych, a nie na DbContext czy obiektach żądania HTTP.
Serwis aplikacyjny powinien orkiestrwać
Serwis aplikacyjny koordynuje kroki, ale nie powinien przejmować każdej reguły. Pobiera agregat, wywołuje jego metodę, korzysta z portu do rezerwacji magazynowej i zapisuje zmiany. Orkiestracja odpowiada za kolejność działań, natomiast domena odpowiada za ich poprawność.
To rozróżnienie przydaje się szczególnie przy integracjach. Wywołanie operatora płatności może zakończyć się błędem technicznym, ale decyzja, czy zamówienie jest gotowe do opłacenia, nadal powinna wynikać z reguł domenowych.
Wzorce, które porządkują reguły biznesowe
Wzorzec nie jest obowiązkowym elementem każdej klasy. Traktuję go jako nazwany sposób rozwiązania powtarzalnego problemu. Najlepszy wzorzec to ten, który upraszcza zmianę, testowanie albo czytanie kodu.
| Wzorzec | Problem, który rozwiązuje | Przykład zastosowania |
|---|---|---|
| Strategy | Jedna decyzja ma kilka wymiennych wariantów | Różne sposoby naliczania dostawy lub rabatu |
| Specification | Warunki wyboru są złożone lub używane w wielu miejscach | Klient kwalifikujący się do promocji |
| State | Zachowanie zależy od aktualnego statusu | Zamówienie, które można anulować tylko przed wysyłką |
| Domain Service | Reguła dotyczy kilku obiektów | Przeliczenie ceny koszyka według cennika |
| Factory | Utworzenie poprawnego obiektu wymaga kilku decyzji | Budowanie polisy lub zlecenia z odpowiednim typem |
| CQRS | Odczyty i modyfikacje mają różne potrzeby | Rozbudowane raporty obok złożonych operacji zapisu |
Najczęściej zaczynam od zwykłej metody i dopiero po pojawieniu się drugiego wariantu wydzielam strategię. Tworzenie fabryki, strategii i specyfikacji na zapas daje poczucie porządku, ale często kończy się dużą liczbą małych klas, które nie mają jeszcze realnej odpowiedzialności.
Warto też uważać na CQRS. Rozdzielenie ścieżek odczytu i zapisu może poprawić czytelność oraz wydajność, lecz nie rozwiązuje samo z siebie złego modelu domenowego. Przy prostym formularzu edycji produktu dodatkowa infrastruktura będzie zwykle większym kosztem niż korzyścią.
Najczęstsze błędy i sposób ich naprawy
Reguły ukryte w kontrolerach
Kontroler z kilkudziesięcioma warunkami trudno testować i łatwo przypadkowo ominąć z innego miejsca aplikacji. Przeniesienie decyzji do modelu lub serwisu domenowego pozwala użyć jej niezależnie od kanału wejściowego.
Serwis, który robi wszystko
Klasa o nazwie OrderService może z czasem walidować dane, liczyć ceny, wysyłać e-maile, odpalać płatności i zarządzać transakcją. Taki obiekt staje się centrum zależności. Pomaga podział według przypadków użycia i rozdzielenie reguł od integracji.
Model anemiczny
Model anemiczny to zestaw klas z samymi właściwościami, podczas gdy cała wiedza znajduje się w zewnętrznych serwisach. Taki układ nie zawsze jest błędem, szczególnie w prostych systemach, ale przy złożonej domenie prowadzi do rozproszenia reguł i łatwego tworzenia niepoprawnych stanów.
Powielanie tej samej reguły
Jeśli warunek dotyczący dostępności produktu pojawia się w kontrolerze, zadaniu cyklicznym i procesie importu, prędzej czy później wersje zaczną się różnić. Jedno źródło prawdy nie oznacza, że każda funkcja ma korzystać z tej samej klasy, ale że decyzja powinna mieć jasno określone miejsce.
Przeczytaj również: YAGNI w .NET - kiedy upraszcza architekturę, a kiedy szkodzi?
Testowanie wyłącznie przez API
Testy end-to-end są cenne, ale są wolniejsze i bardziej podatne na problemy infrastruktury. Reguły takie jak „nie można anulować wysłanego zamówienia” powinny mieć szybkie testy jednostkowe, a osobne testy integracyjne powinny sprawdzać repozytoria, transakcje i komunikację z usługami zewnętrznymi.
Przy projektowaniu testów zapisuję najpierw scenariusze biznesowe. Dla jednej reguły warto pokryć przypadek poprawny, graniczny i odrzucony. Taki zestaw jest bardziej użyteczny niż mechaniczne dążenie do określonego procentu pokrycia kodu.
Jak dobrać poziom architektury do projektu
Najrozsądniejsza architektura rośnie razem z problemem. Na początku wystarczy czytelny podział odpowiedzialności i kilka prostych zasad. Dopiero gdy pojawiają się kolejne kanały wejściowe, integracje, warianty procesu albo częste zmiany wymagań, warto inwestować w bardziej formalne granice.
- Prosty CRUD może działać z warstwą API, serwisem i repozytorium.
- Proces z wieloma regułami skorzysta z encji, obiektów wartości i serwisów domenowych.
- Wiele integracji uzasadnia porty, adaptery i wyraźne interfejsy.
- Duża domena może wymagać podziału na bounded contexts, czyli odrębne obszary modelu z własnym słownikiem i regułami.
- Mikroserwisy mają sens wtedy, gdy granice biznesowe są już zrozumiałe, a nie jako sposób na uporządkowanie przypadkowo podzielonego monolitu.
Nie rozbijam aplikacji na mikroserwisy tylko po to, by każda część miała własny projekt i bazę. Złożona komunikacja, opóźnienia i problemy z transakcjami są uzasadnione dopiero wtedy, gdy niezależne skalowanie, wdrażanie lub własność zespołów przynoszą konkretną korzyść.
Reguły, które warto ustalić przed pierwszym refaktorem
Przed zmianą architektury spisałbym kilka najważniejszych przypadków użycia i zaznaczył, gdzie dziś podejmowane są decyzje. Następnie sprawdziłbym, które fragmenty zależą od ASP.NET Core, Entity Framework Core, brokera wiadomości lub zewnętrznego API.
Dobrym celem nie jest „mieć Clean Architecture”, tylko móc zmienić regułę rabatową bez edycji kontrolera, migracji bazy i pięciu integracji. Architektura jest udana wtedy, gdy zmiana biznesowa trafia w mały, przewidywalny obszar kodu, a test szybko pokazuje, czy system nadal działa zgodnie z zasadami.
Najpierw zabezpieczyłbym testami reguły, które wpływają na pieniądze, uprawnienia i przejścia statusów. Potem wydzieliłbym zależności techniczne, a dopiero na końcu rozważał cięższe wzorce. Taka kolejność zwykle daje więcej niż efektowna struktura katalogów zbudowana zanim domena zostanie naprawdę poznana.
