# DDD i Clean Architecture w Go: pragmatyczne podejście bez nadmiernego inżynieringu
Dług techniczny wynikający z problemów architektonicznych pochłania do 40% budżetu IT firm. W projektach Go często objawia się to nadmierną strukturą: 200 plików z pustymi interfejsami i warstwami, w których logika biznesowa gubi się w gąszczu. Przyczyny to błędne stosowanie DDD i Clean Architecture, zaczerpnięte z Java czy C#. Ale te wzorce, użyte w sposób pragmatyczny, rozwiązują problem nadmiernej złożoności. Pokazujemy, jak uniknąć nadmiernego inżynieringu i stworzyć system łatwy w utrzymaniu.
Dlaczego projekty w Go zamieniają się w labirynt plików?
Programiści przychodzący do Go z innych języków często przenoszą architektoniczne nawyki, które nie pasują do idiomatycznego Go. Zgodnie z Go Developer Survey 2024, głównym problemem jest utrzymanie jednolitych standardów kodu z powodu różnego poziomu doświadczenia i nieidiomatycznych wzorców. Na przykład tworzenie struktury z folderami domain, application, infrastructure na wzór Java, ale bez zrozumienia sedna.
Badanie Carnegie Mellon potwierdza: problemy architektoniczne to główne źródło długu technicznego. W Go wygląda to tak:
- Folder
dtoz duplikującymi się strukturami. mappersmiędzy warstwami, gdzie konwersja jest trywialna.- Interfejsy w
domain, zależne od infrastruktury (np.database/sql). - Use case, który tylko wywołuje repozytorium bez logiki biznesowej.
Rezultat: 200 plików, ale ani jednego z opisem realnych reguł biznesowych. Clean Architecture i DDD stają się nie rozwiązaniem, lecz przyczyną problemu.
DDD: nie o foldery, lecz o język i granice
Domain-Driven Design to nie zestaw folderów, ale podejście do modelowania dziedziny problemowej. Kluczowe koncepcje:
Język wszechobecny (Ubiquitous Language)
Programiści i biznes muszą używać tych samych terminów wszędzie: w kodzie, dokumentacji i rozmowach. Jeśli menedżer mówi „potwierdzić zamówienie”, w kodzie powinna być metoda Confirm(), a nie SetStatus(). To eliminuje nieporozumienia i przyspiesza onboarding.
Ograniczony kontekst (Bounded Context)
Duży system składa się z kilku kontekstów. Na przykład „klient” w dziale sprzedaży (lead z lejkiem sprzedażowym) i w wsparciu (bilety) to różne modele. Bounded Context wyznacza jasne granice, w których model jest spójny. W mikrousługach jeden serwis zazwyczaj odpowiada jednemu kontekstowi.
Encja (Entity) i obiekt-wartość (Value Object)
- Entity ma unikalny identyfikator i zachowuje tożsamość (np.
Order). Zawiera reguły biznesowe. - Value Object jest niezmienny i definiowany przez atrybuty (np.
Money). Dwa obiekty z identycznymi polami są równoważne.
Przykład Value Object w Go:
type Money struct {
Amount int64
Currency string
}
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, errors.New("currency mismatch")
}
return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}, nil
}
Agregat (Aggregate)
Grupa obiektów traktowana jako całość. Ma korzeń (Aggregate Root), przez który odbywa się dostęp. Na przykład Order (korzeń) obejmuje OrderItem. Reguła: zmiany wprowadzane tylko przez korzeń, co gwarantuje spójność.
Clean Architecture: reguła zależności jako podstawa
Esencja Clean Architecture polega na tym, że logika biznesowa nie zależy od szczegółów (bazy danych, frameworki). Jedyna reguła: zależności wskazują do wewnątrz (Dependency Rule).
Jak to działa w Go
- domain: zawiera model biznesowy (Entity, Value Object, interfejsy repozytoriów). Brak zewnętrznych importów poza stdlib.
- application: use cases orkiestrowane domeną. Zależne tylko od domain.
- infrastructure: implementacje adapterów (bazy danych, klienci HTTP). Zależne od domain przez interfejsy.
- delivery: punkty wejścia (HTTP, gRPC). Zależne od application.
Jeśli domain importuje database/sql — architektura jest naruszona. Poprawny przykład: interfejs repozytorium zdefiniowany w domain, a implementacja w infrastructure/postgres.
Zalety połączenia DDD i Clean Architecture
| Aspekt | Efekt | Metryka poprawy |
|----------------|---------------------------------|-------------------|
| Testowalność | Izolacja domeny od infrastruktury | +40% coverage |
| Elastyczność | Wymiana adapterów w godziny | -90% czasu |
| Zrozumiałość | Jasne granice komponentów | -70% onboarding |
Rzeczywista struktura projektu: jak uniknąć 200 plików
Optymalna struktura dla serwisu Go:
internal/
domain/
order.go # agregat Order
order_repo.go # interfejs repozytorium
application/
create_order.go # use case
infrastructure/
postgres/
order_repo.go # implementacja
delivery/
http/
order_handler.go
Kluczowe zasady:
- Interfejsy repozytoriów — w
domain, nie winfrastructure. - Brak folderów
dtoimappersbez potrzeby: używaj tych samych struktur w warstwach, o ile nie narusza to granic. - Logika biznesowa — tylko w
domain. Na przykład metodaCancel()dlaOrdersprawdza status:
func (o *Order) Cancel() error {
if o.status == StatusShipped {
return errors.New("cannot cancel shipped order")
}
o.status = StatusCancelled
return nil
}
Co najważniejsze
- Ubiquitous Language — podstawa DDD. Jeśli terminy w kodzie nie zgadzają się z biznesem, model będzie błędny.
- Dependency Rule — sedno Clean Architecture. Naruszenie prowadzi do spaghetti-kodu.
- Pragmatyzm — nie twórz warstw bez potrzeby. W Go często można obejść się bez
dto, jeśli struktury pasują. - Agregaty — granica transakcji. Zmiany tylko przez korzeń dla spójności.
- Testowanie — domena testowana bez infrastruktury, co przyspiesza rozwój.
— Editorial Team
Brak komentarzy.