Skalowalność DDD w ASP.NET: problemy po 30 agregatach
Przekroczenie 30 agregatów w projekcie ASP.NET z Entity Framework Core prowadzi do krytycznych problemów z zasadą "jeden zapytanie – jeden agregat". Każdy agregat wymaga pełnej ładowania z obiektami zagnieżdżonymi, co generuje zapytania SQL z wieloma JOIN. To szczególnie widoczne w systemach ERP czy marketplacach, gdzie głębokość zagnieżdżenia osiąga 5–6 poziomów.
Przykład typowego zapytania dla agregatu Order:
var orders = await context.Orders
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.Include(o => o.Customer)
.ThenInclude(c => c.Contacts)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.ThenInclude(p => p.Category)
.Include(o => o.Items)
.ThenInclude(i => i.Discount)
.ToListAsync();
Odpowiednie zapytanie SQL zawiera dziesiątki LEFT JOIN, co znacznie spowalnia działanie. Aktualizacja jednego pola w nagłówku zamówienia wymaga załadowania wszystkich pozycji OrderItems jako części agregatu, co dodatkowo obciąża system.
Eksplozja specyfikacji i ich ograniczenia
Wzorzec Specification inkapsuluje logikę filtrowania, ale przy 30+ agregatach kolekcja klas Specification rośnie do setek plików. Kompozycja przez And/Or prowadzi do nieoptymalnego kodu SQL ze względu na zależność od IQueryable EF Core.
Główne problemy:
- Zależność od konkretnego ORM: specyfikacje nie działają z Dapperem ani gRPC bez ponownego pisania.
- Złożoność kompozycji w rzeczywistych scenariuszach z rolami, statusami i strefami czasowymi.
- Czas poświęcony na abstrakcje przekracza czas potrzebny na implementację logiki biznesowej.
Transakcje między agregatami i nadmiarowa infrastruktura
Scoped DbContext w ASP.NET Core pasuje do wzorca Unit of Work w małych projektach. W dużych systemach przypadki biznesowe wymagają zmian w 5+ agregatach jednocześnie: tworzenie zamówienia, płatności, rezerwacji magazynowej i zadania w call centerze.
Rozwiązania są sprzeczne z DDD:
- Jedna transakcja bazy danych – powoduje powstanie "God-serwisów" i narusza granice agregatów.
- Rozproszone Saga lub Outbox – zwiększają złożoność nawet 10-krotnie.
W rezultacie 70% kodu to infrastrukturalny "klej" do synchronizacji, a nie logika biznesowa.
Navigacja w kodzie: tysiące plików i spowolnienie iteracji
Struktura DDD (API/Application/Domain/Infrastructure + Features) przy 30+ agregatach generuje ponad 1000 plików. Zmiana jednego pola w encji dotyka:
- Encji domenowej.
- Konfiguracji Fluent API.
- Migracji.
- Polecenia (Command).
- Walidatora (FluentValidation).
- Obsługiwacza (Handler).
- DTO.
- Mapera (AutoMapper).
- Specyfikacji.
- Testów.
To zmniejsza szybkość rozwoju, utrudnia onboardowanie i utrzymanie kodu.
Co jest ważne
- Wydajność: ciężkie Include/ThenInclude generują nieefektywne zapytania SQL, czas odpowiedzi API rośnie wykładniczo.
- Specyfikacje: nie skalują się, są związane z EF Core, kompozycja prowadzi do błędów.
- Transakcje: operacje między agregatami wymagają kompromisów między DDD a wymaganiami biznesowymi.
- Baza kodu: ponad 1000 plików spowalnia iteracje o 50–70%.
- Hybrydowy podejście: DDD tylko w jądrze, CQRS/Vertical Slices dla obrzeży.
Rekomendacje migracji
Dla projektów z 30+ agregatami przejdź na hybrydowe architektury:
- CQRS: rozdziel komendy i zapytania, używaj modeli odczytu bez Include.
- Vertical Slices: grupuj kod według funkcjonalności, minimalizując warstwy.
- Feature-Sliced Design: modułowość bez sztywnej architektury "lukowej".
To zachowuje zasady DDD w skomplikowanych domenach, upraszczając resztę.
— Editorial Team
Brak komentarzy.