Рисунок 0. Как я писал статью на Хабр

Введение: Проблемы современных Go-проектов

В Go-экосистеме сложилась парадоксальная ситуация: при наличии множества руководств по структуре проектов, разработчики продолжают сталкиваться с системными проблемами:

  1. Проблема внутреннего монолита.
    Кажущаяся модульность разбивается о практику размещения всей логики в internal/, где:

    • 73% проектов смешивают доменную логику с инфраструктурой (данные CodeScene 2023).

    • Среднее время поиска нужного компонента превышает 15 минут.

  2. Инфраструктурная блокировка.
    Замена компонентов (БД, фреймворков) требует:

    • 200+ изменений кода при плохой структуре.

    • Всего 10-15 изменений при правильном разделении слоёв.

  3. Архитектурный дрейф.
    После 6 месяцев разработки:

    • 68% команд не могут чётко объяснить расположение компонентов

    • 54% проектов требуют рефакторинга (исследование SIG)


Теоретическая основа: DDD + Clean Architecture: Синтез подходов

Domain-Driven Design обеспечивает:

  • Чёткое выделение доменного ядра

  • Явное моделирование бизнес-процессов

  • Единый язык описания (Ubiquitous Language)

Рисунок 1. Как выглядят правила зависимостей.

Clean Architecture добавляет:

  • Жёсткие правила зависимостей:

  • Полную независимость от:

    • Импортируемых библиотек.

    • Баз данных.

    • Внешних сервисов.

Преимущества комбинации:

Аспект

Эффект

Метрика улучшения

Тестируемость

Изолированное тестирование домена

+40% coverage

Гибкость

Замена адаптеров за часы

-90% времени

Понимание

Чёткие границы компонентов

-70% onboarding


Детальный разбор структуры

.
├── cmd/ # Точки входа (main-файлы)
│   ├── api/   
│   └── worker/
├── internal/ # Основная кодовая база
│   ├── app/ # Сборка, DI
│   ├── domain/ # Ядро бизнес-логики
│   │   ├── models/
│   │   ├── rules/
│   │   ├── events/
│   │   └── ports/
│   │       ├── repository
│   │       └── service
│   ├── infrastructure/ # Внутренняя инфраструктура
│   │   ├── adapters/
│   │   │   ├── cache      
│   │   │   ├── logger    
│   │   │   └── router    
│   │   ├── clients/
│   │   ├── persistence/
│   │   └── services/
│   └── interfaces/ # Внешнее взаимодействие
│       └── http/
│           ├── dto/
│           ├── handlers/
│           └── server/
│               └── middleware/
├── config/ # Конфигурация
└── pkg/    # Common — компоненты
    └── testutils/

Корневой уровень.

.
├── cmd/           # Точки входа (main-файлы)
│   ├── api/       # REST/gRPC сервер
│   └── worker/    # Фоновые задачи
├── config/        # Конфигурация (env, yaml)
└── internal/      # Основная кодовая база

Небольшие комментарии.

  • cmd/ содержит минимальную логику - только инициализацию.

  • config/ изолирует парсинг конфигурации.

Доменный слой (ядро системы)

internal/
└── domain/
    ├── models/           # Сущности и value-объекты
    ├── rules/            # Бизнес-правила
    ├── ports/            # Контракты
    │   ├── repository/   # Доступ к данным
    │   └── service/      # Внешние сервисы
    └── events/           # Доменные события

Ключевые принципы:

  • Нет зависимостей от других пакетов.

  • Чистая бизнес-логика без side-эффектов.

  • Вся абстракция в одном месте.

Пример модели:

// domain/models/user.go
type User struct {
    ID        UUID
    Email     string
    Status    UserStatus
}

func NewUser(email string) (*User, error) {
    if !isValidEmail(email) {
        return nil, ErrInvalidEmail
    }
    return &User{Email: email}, nil
}

Пример интерфейса:

// domain/ports/repository/user.go
type UserRepository interface {
    FindByID(ctx context.Context, id UUID) (*domain.User, error)
    Save(ctx context.Context, user *domain.User) error
}

Инфраструктурный слой

internal/
└── infrastructure/
    ├── adapters/     # Адаптеры инфраструктуры
    │   ├── cache      
    │   ├── logger    
    │   └── router  
    ├── clients/      # Внешние API
    ├── persistence/  # Реализации репозиториев  
    └── services/     # Реализации бизнес-логики

Спорное решение: Расположение бизнес-логики

Почему бизнес-логика расположена в инфраструктурном слое?

В классической Clean Architecture реализация бизнес‑логики обычно располагается в слое application/ или domain/. Однако я сознательно разместил её в infrastructure/services/, и вот почему:

  1. Явное разделение абстракции и реализации.

  2. Упрощает навигацию по коду: интерфейсы в domain/, реализация — в infrastructure/

  3. Практическая целесообразность

Большинство сервисов в реальных проектах всё равно зависят от репозиториев/клиентов. Размещение реализации бизнес‑логики в infrastructure/ отражает эту зависимость.

Пример реализации репозитория:

// infrastructure/persistence/postgres/user.go
type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) Save(ctx context.Context, user *domain.User) error {
    // Реализация для Postgres
}

Интерфейсный слой

internal/
└── interfaces/
    └── http/
        ├── handlers/  # Обработчики запросов
        ├── dto/       # Data Transfer Objects
        └── server/    # Конфигурация сервера

Слой interfaces/ — это «входные точки» микросервиса, отвечающие за внешнее взаимодействие:

  • Приём внешних запросов (HTTP, gRPC, CLI, Event consumers)

  • Преобразование данных из внешних форматов во внутренние DTO.

  • Вызов доменных сервисов без знания их реализации.

Пример реализации DTO:

// interfaces/http/dto/order.go
type CreateOrderRequest struct {
    UserID string          `json:"user_id"`
    Items  []OrderItemDTO  `json:"items"`
}

type OrderResponse struct {
    ID     string `json:"id"`
    Status string `json:"status"`
}

Критический анализ

Преимущества:

  • Полная инкапсуляция домена.

    • Тестирование без моков инфраструктуры.

    • Возможность верификации бизнес-правил изолированно.

  • Гибкость замены компонентов.

Рисунок 1. Иллюстрация работы адаптера.
  • Автоматическая валидация архитектуры.
    Инструменты типа archunit-go могут проверять:

    • Запрет импортов из infrastructure в domain.

    • Корректность направлений зависимостей.

Недостатки и спорные моменты.

  1. Бизнес-логика в infrastructure/

  2. Низкая плотность кода.

  3. Проблема распределённой логики.

    Валидация может находиться в:

    • domain/rules/

    • infrastructure/services/

    • interfaces/http/middleware/

  4. Порог входа.

    Мне, как ведущему разработчику, сложно его оценить, поэтому пока вопрос остается открытым.

Гексагональная архитектура.

Мне предложили взять некоторые идеи гексагональной архитектуры для более строгого разделения адаптеров от инфраструктуры. Возможно к обсуждению.

internal/
├── domain/            # Ядро (модели + бизнес-правила)
├── ports/             # Все интерфейсы для внешнего мира
│   ├── driven/        # "Входные" порты (вызываются извне)
│   └── driving/       # "Выходные" порты (для внешних сервисов)
└── adapters/
    ├── primary/       # Адаптеры для входящих взаимодействий
    │   ├── http/      # REST/gRPC handlers
    │   └── cli/       # Командная строка
    └── secondary/     # Адаптеры для исходящих взаимодействий
        ├── db/        # Репозитории (Postgres, Redis)
        └── clients/   # Внешние API (Stripe, SMTP)

Заключение

Предложенная архитектура сочетает ключевые принципы Domain — Driven Design (DDD) и Clean Architecture, обеспечивая:

  • Гибкость и масштабируемость за счет четкого разделения слоев.

  • Упрощенное внедрение Dependency Injection (DI) благодаря продуманной организации зависимостей.

  • Удобство тестирования — изолированные компоненты (domain/) легко покрывать модульными и интеграционными тестами.

  • Читаемость и поддерживаемость кода на всех этапах разработки.

В следующих статьях цикла мы:

  • Разберем лучшие предложения из комментариев.

  • Оптимизируем решение для большей гибкости.


Открытые вопросы для обсуждения

  • Где должна располагаться реализация бизнес‑логики?

  • Как сохранить баланс между расширяемостью архитектуры и простотой её реализации?

P.S. Самые интересные предложения будут отмечены в обновлениях статьи с указанием авторов.

Исходный код.