DDD-основы и Dependency Injection в Go
Зачем знать: DDD даёт язык и инструменты для моделирования сложного домена, без которых код через год превращается в кашу. DI определяет, как зависимости попадают в код — от ручной сборки в main до runtime-фреймворков (fx). На middle-уровне ждут понимания, КОГДА это применять, а когда — нет.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- В Go идиоматично
- Gotchas
- Best practices
- Вопросы на собесе
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»1.1 Что такое DDD
Заголовок раздела «1.1 Что такое DDD»Domain-Driven Design (Eric Evans, 2003) — подход к проектированию ПО, где домен (бизнес-область) — центр модели. Делится на:
- Strategic DDD: bounded context, context map, ubiquitous language.
- Tactical DDD: entity, value object, aggregate, repository, domain service, domain event.
Middle 1 обычно встречается с tactical DDD; strategic — забота архитекторов.
1.2 Tactical building blocks
Заголовок раздела «1.2 Tactical building blocks»Объект с identity. Две сущности равны, если равны их ID (а не поля).
type User struct { ID uuid.UUID Name string Email string}
u1 := User{ID: id, Name: "Alice"}u2 := User{ID: id, Name: "Bob"}// u1 == u2 если сравнивать по ID — это одна и та же сущностьEntity имеет жизненный цикл (создаётся, меняется, удаляется) и поведение (методы, выражающие бизнес-правила).
Value Object (VO)
Заголовок раздела «Value Object (VO)»Immutable, equality by value, без identity.
type Money struct { Amount int64 Currency string}
// Equality: m1 == m2 если совпадают все поляm1 := Money{Amount: 100, Currency: "USD"}m2 := Money{Amount: 100, Currency: "USD"}// m1 == m2
// Mutability: операции возвращают НОВОЕ значениеfunc (m Money) Add(o Money) Money { return Money{Amount: m.Amount + o.Amount, Currency: m.Currency}}Примеры VO: Money, Address, Email, DateRange, Coordinates.
В Go VO часто реализуется как struct без указателей, чтобы Go копировал по значению.
Aggregate и Aggregate Root
Заголовок раздела «Aggregate и Aggregate Root»Aggregate — кластер entities и VO, образующих границу транзакционной консистентности. Aggregate Root — единственный entity, через который внешний код взаимодействует с aggregate.
Order (Aggregate Root) ├── OrderItem[] (entities внутри aggregate) ├── ShippingAddr (VO) └── Total (VO)Правила:
- Извне доступен только Aggregate Root.
- Изменения внутри aggregate — атомарны (одна транзакция).
- Между aggregates — только ссылки по ID, не по объекту.
- Один Repository — на один Aggregate Root.
// ХОРОШО: меняем item только через rootorder.ChangeItemQuantity(itemID, 3)
// ПЛОХО: достали item напрямуюitem := order.Items[0]item.Quantity = 3 // обход инвариантовRepository
Заголовок раздела «Repository»Абстракция доступа к persistence для Aggregate Root.
type OrderRepository interface { Save(ctx context.Context, o *Order) error GetByID(ctx context.Context, id uuid.UUID) (*Order, error)}Один Repository на одного Aggregate Root. Нет OrderItemRepository — доступ к item только через Order.
Domain Service
Заголовок раздела «Domain Service»Поведение, не принадлежащее ни одной entity. Чисто доменное (без БД, HTTP).
// Pricer — не часть Order, не часть Product. Это domain service.type Pricer struct { discounts DiscountPolicy}
func (p *Pricer) Calculate(order *Order, user *User) Money { base := order.Total if user.IsPremium() { base = p.discounts.Apply(base, "premium") } return base}Domain Event
Заголовок раздела «Domain Event»Факт, произошедший в домене (OrderCreated, PaymentReceived). Используется для интеграции и saga.
type OrderCreated struct { OrderID uuid.UUID UserID uuid.UUID Total Money OccuredAt time.Time}Entity накапливает события, use case публикует:
type Order struct { // ... events []any}
func (o *Order) Cancel() error { if o.Status == StatusShipped { return ErrCannotCancel } o.Status = StatusCanceled o.events = append(o.events, OrderCanceled{OrderID: o.ID, At: time.Now()}) return nil}
func (o *Order) PullEvents() []any { e := o.events o.events = nil return e}Ubiquitous Language
Заголовок раздела «Ubiquitous Language»Единый язык между разработчиками, продактами и бизнесом. Если бизнес говорит «оформление заказа» — в коде не должно быть createPurchase(). Имена в коде = имена в требованиях.
Bounded Context
Заголовок раздела «Bounded Context»Граница, в которой термины однозначны. Один и тот же Customer в Sales и Billing — разные объекты, даже если у них совпадает ID. Каждый bounded context — обычно отдельный модуль или микросервис.
1.3 Когда DDD НЕ нужен
Заголовок раздела «1.3 Когда DDD НЕ нужен»- CRUD-сервис без бизнес-правил (админка, lookup-сервис). Анемичные DTO + handlers — нормально.
- Прототип/MVP. Сначала найдите домен, потом моделируйте.
- Стартап на стадии product-market fit. Слишком рано — модели меняются каждую неделю.
1.4 Что такое DI
Заголовок раздела «1.4 Что такое DI»Dependency Injection — паттерн, где зависимости (объекты, нужные для работы) передаются извне, а не создаются внутри.
// БЕЗ DI: зависимость создаётся внутриtype OrderService struct{}func (s *OrderService) Create() { db := pgxpool.New(...) // hardcoded! db.Exec(...)}
// С DI: зависимость инжектится в конструктореtype OrderService struct { db *pgxpool.Pool}func NewOrderService(db *pgxpool.Pool) *OrderService { return &OrderService{db: db}}В Go DI чаще всего — это просто передача в конструктор. Никакой магии.
2. В Go идиоматично
Заголовок раздела «2. В Go идиоматично»2.1 Manual DI (constructor pattern)
Заголовок раздела «2.1 Manual DI (constructor pattern)»В большинстве проектов достаточно ручной сборки в main.go. Это идиоматично:
func main() { // инфраструктура db := mustOpenDB() cache := redis.NewClient(...) kafka := newKafka()
// адаптеры orderRepo := postgres.NewOrderRepo(db) userRepo := postgres.NewUserRepo(db) publisher := kafkaadapter.New(kafka)
// use cases createOrder := usecase.NewCreateOrderUseCase(orderRepo, userRepo, publisher) payOrder := usecase.NewPayOrderUseCase(orderRepo, paymentClient)
// handlers orderHandler := httpapi.NewOrderHandler(createOrder, payOrder)
// server srv := newServer(orderHandler) srv.Run()}Это и есть DI. В 90% Go-проектов фреймворк не нужен.
2.2 Functional Options
Заголовок раздела «2.2 Functional Options»Когда у конструктора много опциональных параметров — вместо длинного списка аргументов используют functional options:
type Server struct { addr string timeout time.Duration logger *slog.Logger tlsConfig *tls.Config}
type Option func(*Server)
func WithTimeout(t time.Duration) Option { return func(s *Server) { s.timeout = t }}
func WithLogger(l *slog.Logger) Option { return func(s *Server) { s.logger = l }}
func WithTLS(c *tls.Config) Option { return func(s *Server) { s.tlsConfig = c }}
func NewServer(addr string, opts ...Option) *Server { s := &Server{ addr: addr, timeout: 30 * time.Second, // sensible defaults logger: slog.Default(), } for _, opt := range opts { opt(s) } return s}
// Использованиеsrv := NewServer(":8080", WithTimeout(10 * time.Second), WithLogger(myLogger), WithTLS(tlsCfg),)Преимущества:
- API расширяется без breaking changes (добавляешь
WithX, не ломая старый код). - Defaults задаются в одном месте.
- Не приходится передавать nil/zero для неиспользуемых полей.
Идиома пришла из gRPC, net/http, pgx.
2.3 google/wire — compile-time DI
Заголовок раздела «2.3 google/wire — compile-time DI»wire — генератор кода, который собирает граф зависимостей во время компиляции.
Установка: go install github.com/google/wire/cmd/wire@latest.
// wire.go (с build tag, чтобы не компилировался)//go:build wireinject
package main
import ( "github.com/google/wire")
func InitializeApp() (*App, error) { wire.Build( NewPostgresPool, postgres.NewOrderRepo, wire.Bind(new(usecase.OrderRepository), new(*postgres.OrderRepo)), usecase.NewCreateOrderUseCase, httpapi.NewOrderHandler, NewApp, ) return nil, nil // wire генерирует реальное тело}$ wire# создаёт wire_gen.go// wire_gen.go (generated, без build tag)package main
func InitializeApp() (*App, error) { pool, err := NewPostgresPool() if err != nil { return nil, err } repo := postgres.NewOrderRepo(pool) useCase := usecase.NewCreateOrderUseCase(repo) handler := httpapi.NewOrderHandler(useCase) app := NewApp(handler) return app, nil}Плюсы wire:
- Ошибки графа — на этапе компиляции (нет
panic at startup). - Нет рефлексии, нет runtime-оверхеда.
- Сгенерированный код — обычный Go, читаемый.
Минусы:
- Нужно перегенерировать при изменении графа.
- Provider sets могут быть громоздкими.
- Не управляет lifecycle (Start/Stop).
2.4 uber-go/fx — runtime DI с lifecycle
Заголовок раздела «2.4 uber-go/fx — runtime DI с lifecycle»fx — runtime-фреймворк, основанный на dig (uber-go/dig).
package main
import ( "context"
"go.uber.org/fx")
func main() { fx.New( fx.Provide( NewPostgresPool, postgres.NewOrderRepo, // привязка интерфейса fx.Annotate( postgres.NewOrderRepo, fx.As(new(usecase.OrderRepository)), ), usecase.NewCreateOrderUseCase, httpapi.NewOrderHandler, NewHTTPServer, ), fx.Invoke(StartHTTPServer), ).Run()}
func NewHTTPServer(lc fx.Lifecycle, h *httpapi.OrderHandler) *http.Server { srv := &http.Server{Addr: ":8080", Handler: h.Routes()} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { go srv.ListenAndServe() return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv}Плюсы fx:
- Lifecycle management (OnStart/OnStop) — graceful shutdown в фреймворке.
- Модули (
fx.Module) для крупных приложений. - Параметры через struct с тегами (
group,name). - Hot path для тестов:
fxtest.New(...).RequireStart().Stop().
Минусы:
- Ошибки графа — только в runtime (panic при старте, если не хватило provider).
- Рефлексия → медленнее старт, сложнее отладка.
- Учебная кривая (теги, модули, hooks).
2.5 uber-go/dig — DI-контейнер
Заголовок раздела «2.5 uber-go/dig — DI-контейнер»dig — низкоуровневая основа fx. Без lifecycle, просто граф зависимостей.
c := dig.New()c.Provide(NewPostgresPool)c.Provide(postgres.NewOrderRepo)c.Provide(func(r *postgres.OrderRepo) usecase.OrderRepository { return r })c.Provide(usecase.NewCreateOrderUseCase)
c.Invoke(func(uc *usecase.CreateOrderUseCase) { // граф собран, uc — готов})Используется редко напрямую — обычно через fx.
2.6 Сравнение
Заголовок раздела «2.6 Сравнение»| Параметр | Manual | wire | fx |
|---|---|---|---|
| Время старта | мгновенно | мгновенно | заметно медленнее |
| Тип проверки | compile | compile | runtime (panic) |
| Generated code | нет | да | нет |
| Lifecycle | сами | сами | встроенный |
| Кривая обучения | плоская | средняя | крутая |
| Когда выбирать | до 30-50 зависимостей | средние/большие проекты | большие проекты с lifecycle |
Рекомендация для middle 1: начать с manual, выучить wire для среднего проекта, fx — когда реально нужен lifecycle (десятки сервисов в монорепе, hot-reload, и т.п.).
2.7 DI в тестах
Заголовок раздела «2.7 DI в тестах»Главная польза DI — лёгкая подмена зависимостей в тестах.
func TestCreateOrder(t *testing.T) { repo := &fakeOrderRepo{} pub := &fakePublisher{} uc := usecase.NewCreateOrderUseCase(repo, pub) // тест use case без БД и Kafka}С wire/fx — то же самое: в тестах вы НЕ используете DI-фреймворк, а собираете зависимости вручную, через mock/fake.
2.8 Constructor pattern: соглашения
Заголовок раздела «2.8 Constructor pattern: соглашения»- Конструктор называется
New<Type>илиNew<Type>WithConfigдля variant. - Возвращает
*<Type>, errorили просто*<Type>. - Принимает зависимости первыми, конфигурацию — последней.
- Не делает heavy work (BD-коннект, HTTP-вызовы) — это в
Start()/Run().
// Стандарт:func NewOrderRepo(pool *pgxpool.Pool) *OrderRepo
// Variants:func NewOrderRepoWithLogger(pool *pgxpool.Pool, log *slog.Logger) *OrderRepofunc NewOrderRepoFromEnv() (*OrderRepo, error)2.9 Functional Options vs Config Struct
Заголовок раздела «2.9 Functional Options vs Config Struct»Альтернатива опциям — config struct:
type Config struct { Addr string Timeout time.Duration Logger *slog.Logger}
func NewServer(cfg Config) *Server { ... }
srv := NewServer(Config{Addr: ":8080", Timeout: 10*time.Second})Сравнение:
| Options | Config struct | |
|---|---|---|
| Defaults | в New | заранее не понятно |
| Расширяемость | да, без breaking changes | да, но новое поле — zero value |
| Читаемость | WithX(...) явный | поля struct явные |
| Required vs Optional | сложно различить | сложно различить |
Многие либы используют ОБА: required → аргумент, optional → options.
2.10 Lifecycle management
Заголовок раздела «2.10 Lifecycle management»В fx уже встроено. Вручную:
type Lifecycle interface { Start(ctx context.Context) error Stop(ctx context.Context) error}
type App struct { components []Lifecycle}
func (a *App) Start(ctx context.Context) error { for _, c := range a.components { if err := c.Start(ctx); err != nil { return err } } return nil}
func (a *App) Stop(ctx context.Context) error { // reverse order for i := len(a.components) - 1; i >= 0; i-- { a.components[i].Stop(ctx) } return nil}3. Gotchas
Заголовок раздела «3. Gotchas»3.1 Anemic domain model
Заголовок раздела «3.1 Anemic domain model»Самая частая ошибка в Go из-за привычки писать struct+function.
// ПЛОХО: анемияtype Order struct { Status string}
func CancelOrder(o *Order) error { if o.Status == "shipped" { return errors.New("...") } o.Status = "canceled" return nil}
// ХОРОШО: поведение на entityfunc (o *Order) Cancel() error { ... }3.2 Service Locator — anti-pattern
Заголовок раздела «3.2 Service Locator — anti-pattern»// ПЛОХО: глобальный containervar Container = di.New()
type OrderService struct{}func (s *OrderService) Create() { repo := Container.Get("OrderRepo").(*OrderRepo) // ...}Это нарушает explicit dependencies, скрывает связи, ломает тестирование. В Go — категорически не делать.
3.3 Один Repository на несколько aggregates
Заголовок раздела «3.3 Один Repository на несколько aggregates»// ПЛОХО:type Repository interface { SaveOrder(...) SaveUser(...) SaveProduct(...)}Это либо «бог-репозиторий», либо нарушение aggregate root правил. Каждому aggregate — свой repository.
3.4 Aggregate слишком большой
Заголовок раздела «3.4 Aggregate слишком большой»Если Order содержит User, History, Reviews, Comments, всё в одной транзакции — это монолитный aggregate. Производительность страдает, конкуренция за блокировки растёт. Делите по консистентности: если данные могут быть eventually consistent — отдельный aggregate.
3.5 fx panic at startup
Заголовок раздела «3.5 fx panic at startup»fx.New( fx.Provide(NewOrderRepo), // зависит от *pgxpool.Pool // забыли fx.Provide(NewPgPool)).Run()// panic: missing dependencies for func ...Решение: продумывайте граф заранее, тестируйте startup в CI:
func TestApp_Startup(t *testing.T) { app := fxtest.New(t, AppOptions...).RequireStart() defer app.RequireStop()}3.6 wire: forgot to regenerate
Заголовок раздела «3.6 wire: forgot to regenerate»После добавления зависимости в wire.Build нужно перегенерировать wire_gen.go. Часто забывают, ловят compile error в неожиданном месте. Решение — go generate ./... в CI и pre-commit hook.
3.7 Циркулярные зависимости в use case
Заголовок раздела «3.7 Циркулярные зависимости в use case»Use case A зависит от use case B, B — от A. Решение:
- вынести общую логику в domain service;
- использовать domain event (A публикует событие, B реагирует).
3.8 Конструктор делает heavy work
Заголовок раздела «3.8 Конструктор делает heavy work»// ПЛОХО:func NewOrderRepo(dsn string) *OrderRepo { pool, _ := pgxpool.New(context.Background(), dsn) // блокирует return &OrderRepo{pool: pool}}Конструктор должен быть быстрым и НЕ должен делать сеть/диск. Это делает Start() или явный init:
func NewOrderRepo(pool *pgxpool.Pool) *OrderRepo { ... } // pool создан снаружи3.9 Использование DI-фреймворка для библиотеки
Заголовок раздела «3.9 Использование DI-фреймворка для библиотеки»Не используйте wire/fx в публичных Go-библиотеках. Пользователь не должен зависеть от вашего фреймворка. Только в приложениях (main module).
3.10 Передача всего через одну Config struct
Заголовок раздела «3.10 Передача всего через одну Config struct»// ПЛОХО:type Config struct { DB *pgxpool.Pool Redis *redis.Client Logger *slog.Logger Kafka *kafka.Conn // ...}
func NewOrderService(cfg Config) *OrderService { ... }OrderService нужен только pool, но получил весь Config. Это god object. Передавайте только то, что используете:
func NewOrderService(pool *pgxpool.Pool, log *slog.Logger) *OrderService3.11 Repository возвращает entity или DTO?
Заголовок раздела «3.11 Repository возвращает entity или DTO?»В классическом DDD — entity. Но если БД-схема сильно отличается от domain, удобно иметь промежуточный DTO внутри репозитория:
func (r *OrderRepo) GetByID(...) (*domain.Order, error) { var row orderRow // SELECT ... INTO row return row.toDomain(), nil // конвертация в entity}3.12 DDD для CRUD
Заголовок раздела «3.12 DDD для CRUD»Сервис: GET /users/{id} → SELECT * FROM users WHERE id=$1Если домен такой — DDD не нужен. Делайте transaction script.
3.13 Event sourcing на каждый чих
Заголовок раздела «3.13 Event sourcing на каждый чих»DDD НЕ обязан быть event-sourced. Event sourcing — отдельный паттерн, и он подходит только для специфических задач (audit, time-travel). Не путайте domain events (для интеграции) с event sourcing (как persistence).
4. Best practices
Заголовок раздела «4. Best practices»- Entity = данные + поведение. Никаких анемичных моделей.
- Aggregate Root — единственный entry point. Не достаём item напрямую, только через root.
- Один Repository на один Aggregate Root.
- VO immutable, methods возвращают новые значения.
- Domain service — когда логика не принадлежит entity.
- Ubiquitous language. Имена в коде = имена в требованиях.
- Bounded context = модуль/сервис. Не смешивайте Sales::Customer и Billing::Customer.
- DI через конструктор. Никакого Service Locator.
- Functional options для большого числа опциональных параметров.
- Composition root — main.go. Граф собирается в одном месте.
- wire — для среднего проекта, fx — для большого с lifecycle.
- Manual DI — пока работает. Не тащите фреймворк ради «правильно».
- Test без DI-фреймворка. Подменяйте зависимости вручную в тестах.
- Конструктор не делает heavy work.
- Domain events накапливаются в entity, публикуются use case.
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»-
Чем Entity отличается от Value Object? Entity имеет identity (ID), equality by reference. VO immutable, equality by value.
-
Что такое Aggregate Root и зачем он? Единственный entity, через который доступен aggregate. Гарантирует инварианты, ограничивает зону транзакционной консистентности.
-
Сколько Repository должно быть в системе с 10 entity? По числу Aggregate Roots, не entity. Если 3 aggregate (Order, User, Catalog) — 3 repository.
-
Что такое Anemic Domain Model? Entity без поведения, только поля. Логика «размазана» по сервисам. Антипаттерн в DDD/CleanArch.
-
Что такое Ubiquitous Language? Единый язык домена между разработчиками и бизнесом. Имена в коде = имена в требованиях.
-
Что такое Bounded Context? Граница, в которой термины однозначны. Один
Customerв Sales и Billing — разные сущности. Обычно = микросервис. -
Domain Service vs Application Service? Domain Service — чистая бизнес-логика, без БД/HTTP (например, Pricer). Application Service (use case) — оркестратор, вызывает domain + repositories.
-
Когда DDD не нужен? CRUD-сервис, прототип/MVP, lookup-сервис без правил.
-
Что такое Dependency Injection? Паттерн, где зависимости передаются извне (через конструктор/setter), а не создаются внутри.
-
Чем wire отличается от fx? wire — compile-time codegen, проверки на этапе компиляции. fx — runtime DI с reflection и встроенным lifecycle.
-
Когда нужен DI-фреймворк? Когда граф зависимостей становится сложным (50+ компонентов), нужен lifecycle management или модули. До этого manual DI достаточно.
-
Что такое functional options? Паттерн:
func(*Type)для опциональных параметров. КонструкторNew(req, opts...). Расширяется без breaking changes. -
Что такое Service Locator и почему это antipattern в Go? Глобальный контейнер, откуда код «достаёт» зависимости. Скрывает связи, ломает тестирование, нарушает explicit dependencies.
-
Где должны жить интерфейсы repository — в domain или infrastructure? Domain (или use case). Реализация — в infrastructure. Это Dependency Inversion: high-level не зависит от low-level.
-
Что такое Domain Event? Факт, произошедший в домене (OrderCreated). Используется для интеграции и saga.
-
Чем Aggregate отличается от Entity? Aggregate — кластер entities+VO с одной границей транзакционной консистентности. Aggregate Root — entity-точка входа.
-
Можно ли в Aggregate ссылаться на другой Aggregate по объекту? Нет, только по ID. Иначе теряется граница aggregates.
-
Как DI помогает тестированию? Подменяем реальные зависимости на mock/fake, тест use case не требует БД/сети.
-
Можно ли в use case вызывать другой use case? Не рекомендуется. Пересекающаяся логика — в domain service.
-
Что такое
fx.LifecycleиOnStop? Хук для graceful shutdown компонента. fx вызывает OnStop в обратном порядке при остановке. -
Чем wire отличается от dig? dig — runtime DI-контейнер (основа fx). wire — генератор compile-time кода.
-
Что лучше — config struct или functional options? Зависит от: required+несколько → struct; много optional с defaults → options. Часто комбинация.
-
Как обработать domain event? Entity накапливает события (
order.events), use case достаёт черезPullEvents()и публикует через EventPublisher (Kafka, in-memory bus). -
Что такое Repository в DDD? Абстракция доступа к persistence для Aggregate Root.
Save,GetByID, иногда поиск по бизнес-критериям. -
Чем DDD отличается от Clean Architecture? DDD — про моделирование домена (entity, aggregate, BC). Clean Arch — про слои и зависимости. Часто используются вместе.
6. Practice
Заголовок раздела «6. Practice»-
Опишите aggregate Order с инвариантами:
- нельзя создать пустой заказ;
- нельзя отменить отгруженный;
- сумма заказа = сумма (price * qty) по items. Покройте unit-тестами.
-
Сделайте Value Object
Moneyс операциямиAdd,Subtract,Multiply(int),Equal. Покажите, что Money immutable. -
Возьмите проект на manual DI и переведите его на wire. Сравните
main.goдо/после. -
То же — на fx. Добавьте lifecycle для HTTP-сервера, БД-пула и Kafka producer.
-
Напишите DI-обёртку с functional options для HTTP-сервера:
WithReadTimeout,WithLogger,WithMiddleware(...). -
Реализуйте Pricer как domain service: бесплатная доставка при сумме >5000, скидка 10% premium-пользователю. Тест без БД/HTTP.
7. Источники
Заголовок раздела «7. Источники»- Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
- Vaughn Vernon. Implementing Domain-Driven Design. Addison-Wesley, 2013.
- Vaughn Vernon. Domain-Driven Design Distilled. Addison-Wesley, 2016.
- Martin Fowler. AnemicDomainModel — https://martinfowler.com/bliki/AnemicDomainModel.html
- google/wire — https://github.com/google/wire
- uber-go/fx — https://uber-go.github.io/fx/
- uber-go/dig — https://pkg.go.dev/go.uber.org/dig
- Dave Cheney. SOLID Go Design — https://dave.cheney.net/2016/08/20/solid-go-design
- Rob Pike. Self-referential functions and the design of options — https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html