Цели модуля

После модуля студент сможет:

  • Различать основные архитектурные стили организации Go-проектов
  • Понимать плюсы и минусы каждого подхода
  • Осознанно выбирать архитектуру под конкретную задачу
  • Объяснять Dependency Rule и направление зависимостей
  • Рефакторить flat-проект в слоистую архитектуру
  • Обосновывать архитектурный выбор перед командой

2.1 Flat structure

Что это

Все файлы в одной директории или в минимальной структуре. Нет разделения на слои.

Пример

text
myapp/
├── main.go
├── handlers.go
├── models.go
├── db.go
├── utils.go
├── go.mod
└── Makefile

Когда использовать

  • CLI-утилиты (один файл или несколько)
  • Маленькие скрипты и инструменты
  • Прототипы и эксперименты
  • Проекты до 500 строк кода

Плюсы

  • Максимальная простота
  • Не нужно думать о структуре
  • Легко начать

Минусы

  • Не масштабируется
  • Всё связано со всем
  • Сложно тестировать
  • Невозможно разделить ответственность
  • Новый разработчик теряется

Когда пора менять

Сигналы что flat уже не работает:

  • Файлы больше 300 строк
  • Появились взаимозависимости между "модулями"
  • Тесты требуют запуска всей системы
  • Два разработчика конфликтуют в одном файле

2.2 Layered architecture (слоистая)

Что это

Код разделён на слои по ответственности. Каждый слой зависит только от слоя ниже.

Классические слои

text
Handlers (транспорт)
    |
Services (бизнес-логика)
    |
Repositories (доступ к данным)
    |
Database

Пример структуры

text
myapp/
├── cmd/
│   └── app/
│       └── main.go
├── handlers/
│   ├── repo_handler.go
│   └── user_handler.go
├── services/
│   ├── repo_service.go
│   └── user_service.go
├── repositories/
│   ├── repo_repository.go
│   └── user_repository.go
├── models/
│   ├── repo.go
│   └── user.go
├── go.mod
└── Makefile

Направление зависимостей

text
handlers -> services -> repositories -> database

Каждый слой знает только о слое ниже. Handlers не знают о repositories.

Когда использовать

  • Небольшие API сервисы (до 10-15 эндпоинтов)
  • Команда 1-3 разработчика
  • Простая бизнес-логика
  • Одна база данных

Плюсы

  • Понятная структура
  • Разделение ответственности
  • Легко тестировать сервисы (мок репозитория)
  • Стандартный паттерн, все знают

Минусы

  • Сервисы знают о конкретных репозиториях (не об интерфейсах)
  • Сложно менять инфраструктуру
  • При росте бизнеса сервисы распухают
  • Нет чёткой границы домена

Пример кода

go
// handlers/repo_handler.go
type RepoHandler struct {
    service *services.RepoService  // конкретный тип, не интерфейс
}

// services/repo_service.go
type RepoService struct {
    repo *repositories.RepoRepository  // конкретный тип
}

// repositories/repo_repository.go
type RepoRepository struct {
    db *sql.DB
}

Проблема: если захотим поменять PostgreSQL на MySQL, придётся менять сервисы. В Clean Architecture это решается интерфейсами.


2.3 Clean Architecture

Что это

Архитектура предложенная Robert C. Martin (Uncle Bob). Ключевая идея: зависимости направлены внутрь, к домену.

Круги Clean Architecture

text
┌─────────────────────────────────────────────┐
│  Frameworks & Drivers (внешний слой)        │
│  HTTP, DB, UI, External Services            │
├─────────────────────────────────────────────┤
│  Interface Adapters                          │
│  Controllers, Gateways, Presenters          │
├─────────────────────────────────────────────┤
│  Application Business Rules                  │
│  Use Cases                                    │
├─────────────────────────────────────────────┤
│  Enterprise Business Rules (ядро)            │
│  Entities (Domain Models)                    │
└─────────────────────────────────────────────┘

Dependency Rule

Зависимости направлены только внутрь. Внешние слои зависят от внутренних, но не наоборот.

Внутренние слои не знают о внешних:

  • Domain Models не знают о HTTP, DB, фреймворках
  • Use Cases не знают о конкретных реализациях
  • Сервисы не знают о PostgreSQL, Redis, Kafka

Структура в шаблоне проекта

text
internal/
├── models/          # Доменные модели (ядро)
├── repository/      # Интерфейсы (абстракции)
├── service/         # Бизнес-логика
├── usecase/         # Use Cases (сценарии)
├── adapters/        # Реализации интерфейсов
│   ├── postgres/    # PostgreSQL adapter
│   ├── http_client/ # HTTP client adapter
│   └── scheduler/   # Cron adapter
├── handlers/        # HTTP handlers (внешний слой)
│   ├── http/
│   └── jobs/
└── app/             # DI и сборка

Направление зависимостей в шаблоне

text
handlers/http (внешний слой)
    | вызывает
    v
usecase (бизнес-сценарии)
    | вызывает
    v
service (бизнес-логика)
    | вызывает
    v
repository (интерфейсы)
    | реализуется
    v
adapters/postgres (реализации)

Ключевое отличие от Layered

В Layered сервисы зависят от конкретных репозиториев:

go
// Layered: сервис знает о конкретной реализации
type RepoService struct {
    repo *postgres.RepoRepository
}

В Clean Architecture сервисы зависят от интерфейсов:

go
// Clean Architecture: сервис знает только об интерфейсе
type RepoService struct {
    repo repository.RepoRepository  // интерфейс
}

Это позволяет:

  • Подставить mock в тестах без реальной БД
  • Поменять PostgreSQL на MySQL без изменения сервисов
  • Добавить кэш перед репозиторием

Когда использовать

  • Средние и большие сервисы (10+ эндпоинтов)
  • Команда 3+ разработчика
  • Сложная бизнес-логика
  • Нужно тестировать без реальной инфраструктуры
  • Планируется эволюция инфраструктуры

Плюсы

  • Тестируемость: mock вместо реальной БД
  • Гибкость: легко менять инфраструктуру
  • Разделение: бизнес-логика изолирована
  • Масштабируемость: можно расти

Минусы

  • Больше кода (интерфейсы, DTO, мапперы)
  • Сложнее для простых проектов
  • Нужна дисциплина от команды
  • Кривая обучения

2.4 Hexagonal Architecture (Ports and Adapters)

Что это

Архитектура предложенная Alistair Cockburn. Близка к Clean Architecture, но акцент на портах и адаптерах.

Ключевые концепции

Port (порт) - интерфейс определяющий что сервис делает. Adapter (адаптер) - реализация подключающая конкретную технологию.

Два типа портов:

  • Primary (Driving): кто вызывает сервис (HTTP handler, CLI, gRPC)
  • Secondary (Driven): что сервис вызывает (БД, очередь, внешний API)

Визуализация

text
Primary Adapters          Application           Secondary Adapters
(Driving)                 (Core)                (Driven)

HTTP Handler  ──┐                    ┌──  PostgreSQL Adapter
CLI Command   ──┼──>  Ports    ──┼──  Redis Adapter
gRPC Server   ──┘     & Core       └──  Kafka Adapter

Структура

text
myapp/
├── cmd/
│   └── app/
│       └── main.go
├── core/
│   ├── domain/
│   │   ├── repo.go
│   │   └── user.go
│   ├── ports/
│   │   ├── in/              # Primary ports (входящие)
│   │   │   ├── create_repo.go
│   │   │   └── get_repo.go
│   │   └── out/             # Secondary ports (исходящие)
│   │       ├── repo_repository.go
│   │       └── event_publisher.go
│   └── services/
│       └── repo_service.go
├── adapters/
│   ├── in/
│   │   ├── http/
│   │   └── grpc/
│   └── out/
│       ├── postgres/
│       └── kafka/
├── go.mod
└── Makefile

Отличие от Clean Architecture

Clean Architecture и Hexagonal очень близки. Разница в акцентах:

АспектClean ArchitectureHexagonal
АкцентСлои и зависимостиПорты и адаптеры
ГраницаМежду слоямиМежду ядром и внешним миром
ТерминологияUse Cases, EntitiesPorts, Adapters
Когда использоватьБольшие системыСервисы с множеством интеграций

На практике в Go проектах термины часто используются как синонимы.

Когда использовать

  • Сервис с множеством интеграций (HTTP, gRPC, очередь, БД)
  • Нужно легко менять технологии
  • Сложная доменная логика
  • Высокие требования к тестируемости

2.5 Modular Monolith

Что это

Одно приложение (один деплой), но код разделён на независимые модули по доменным областям. Каждый модуль имеет чёткие границы.

Структура

text
myapp/
├── cmd/
│   └── app/
│       └── main.go
├── modules/
│   ├── repos/
│   │   ├── handler.go
│   │   ├── service.go
│   │   ├── repository.go
│   │   ├── model.go
│   │   └── dto.go
│   ├── users/
│   │   ├── handler.go
│   │   ├── service.go
│   │   ├── repository.go
│   │   ├── model.go
│   │   └── dto.go
│   └── billing/
│       ├── handler.go
│       ├── service.go
│       ├── repository.go
│       ├── model.go
│       └── dto.go
├── shared/
│   ├── database/
│   ├── middleware/
│   └── config/
├── go.mod
└── Makefile

Правила Modular Monolith

  1. Модули не импортируют внутренние пакеты друг друга
  2. Взаимодействие только через публичные интерфейсы модуля
  3. Каждый модуль имеет свою таблицу в БД
  4. Shared-код минимален и стабилен

Когда использовать

  • Один продукт, одна команда (до 10 человек)
  • Несколько доменных областей
  • Не нужно независимое масштабирование
  • Простота деплоя критична

Плюсы

  • Один деплой (просто)
  • Нет сетевых вызовов между модулями (быстро)
  • Легко рефакторить (один репозиторий)
  • Транзакции между модулями возможны
  • Можно эволюционировать в микросервисы

Минусы

  • Один процесс - одна точка отказа
  • Нет независимого масштабирования
  • Требует дисциплины для соблюдения границ
  • Может вырасти в Big Ball of Mud без контроля

Эволюция в микросервисы

Modular Monolith - хорошая стартовая точка. При необходимости модуль можно вынести в отдельный микросервис:

text
Шаг 1: Modular Monolith (все модули в одном процессе)
Шаг 2: Выносим модуль billing в отдельный сервис
Шаг 3: Заменяем прямой вызов на HTTP/gRPC
Шаг 4: Разделяем базу данных

2.6 Microservices

Что это

Каждый сервис - отдельное приложение со своей базой данных. Сервисы общаются через сеть (HTTP, gRPC, очередь).

Структура

text
repo-service/
├── cmd/app/main.go
├── internal/...
├── go.mod
└── Makefile

user-service/
├── cmd/app/main.go
├── internal/...
├── go.mod
└── Makefile

billing-service/
├── cmd/app/main.go
├── internal/...
├── go.mod
└── Makefile

Когда использовать

  • Несколько автономных команд
  • Нужно независимое масштабирование
  • Разные технологические требования
  • Организационная необходимость

Плюсы

  • Независимый деплой и масштабирование
  • Автономия команд
  • Можно использовать разные технологии
  • Изоляция сбоев

Минусы

  • Операционная сложность (мониторинг, логи, трейсинг)
  • Сетевые вызовы (latency, надёжность)
  • Распределённые транзакции (Saga)
  • Сложность тестирования
  • Data consistency (eventual consistency)

Правило

Не начинайте с микросервисов. Начните с Modular Monolith. Выносите в микросервисы когда есть реальная необходимость.

"Microservices are the destination, not the starting point."


2.7 Сравнение архитектур

АрхитектураКогда использоватьПлюсыМинусыСложность
FlatCLI, утилиты, прототипыПростотаНе масштабируетсяНизкая
LayeredНебольшие APIПонятностьСвязность слоёвСредняя
Clean ArchitectureСредние/большие сервисыТестируемость, гибкостьБольше кодаВысокая
HexagonalМножество интеграцийИзоляция от технологийИзбыточность для простыхВысокая
Modular MonolithОдин продукт, одна командаПростота деплояОдна точка отказаСредняя
MicroservicesАвтономные команды, масштабированиеНезависимостьОперационная сложностьОчень высокая

2.8 Критерии выбора

Факторы выбора архитектуры

  1. Размер команды

    • 1-2 человека: Flat или Layered
    • 3-5 человек: Layered или Clean Architecture
    • 5-15 человек: Clean Architecture или Modular Monolith
    • 15+ человек: Microservices (если обосновано)
  2. Сложность домена

    • Простой CRUD: Layered
    • Средняя бизнес-логика: Clean Architecture
    • Сложный домен с множеством правил: Clean Architecture / Hexagonal
  3. Требования к масштабированию

    • Один инстанс: Modular Monolith
    • Независимое масштабирование компонентов: Microservices
  4. Требования к тестируемости

    • Минимальные: Flat / Layered
    • Высокие (mock без БД): Clean Architecture / Hexagonal
  5. Операционные возможности

    • Нет DevOps: Modular Monolith
    • Есть инфраструктура: Microservices допустимы

Таблица решений

СитуацияРекомендация
CLI утилитаFlat
ПрототипFlat или Layered
Маленький API (до 5 эндпоинтов)Layered
Средний API (5-20 эндпоинтов)Clean Architecture
Большой API (20+ эндпоинтов)Clean Architecture
Один продукт, одна командаModular Monolith
Несколько команд, независимый деплойMicroservices
Множество интеграцийHexagonal

Антипаттерны выбора

  1. Resume Driven Development: выбор технологии ради резюме
  2. Microservices First: микросервисы с первого дня без необходимости
  3. Over-engineering: Clean Architecture для простого CRUD
  4. Under-engineering: Flat для сложного бизнеса

Привязка к шаблону проекта

Архитектура шаблона: Clean Architecture

Шаблон проекта использует Clean Architecture. Это обосновано:

  • Средний размер сервиса (10+ эндпоинтов)
  • Нужна тестируемость без реальной инфраструктуры
  • Планируется эволюция (Serverless -> VDS -> Kubernetes)
  • Несколько адаптеров (PostgreSQL, NFS, S3, Lockbox)

Структура шаблона с комментариями

text
internal/
├── models/          # Ядро: доменные модели (Repo, User, PullRequest)
├── repository/      # Абстракции: интерфейсы репозиториев
├── service/         # Бизнес-логика: правила домена
├── usecase/         # Сценарии: CreateRepo, MergePullRequest
├── adapters/        # Инфраструктура: postgres, nfs, s3, lockbox
├── handlers/        # Транспорт: HTTP, jobs
├── observability/   # Кросс-каттинг: логи, метрики, трейсинг
├── security/        # Кросс-каттинг: HMAC, секреты
├── config/          # Кросс-каттинг: конфигурация
└── app/             # Сборка: DI-контейнер

Задание: определите слои

Для каждого файла шаблона определите слой Clean Architecture:

  1. internal/models/repo.go -> ?
  2. internal/repository/repo_repository.go -> ?
  3. internal/service/repo_service.go -> ?
  4. internal/usecase/create_repo_usecase.go -> ?
  5. internal/adapters/postgres/repo_repository.go -> ?
  6. internal/handlers/http/api/v1/handler.go -> ?

Ответы:

  1. Domain Models (ядро)
  2. Interface Adapters (абстракция)
  3. Application Business Rules
  4. Application Business Rules
  5. Frameworks & Drivers (внешний слой)
  6. Frameworks & Drivers (внешний слой)

💱 Компромиссы

  • Flat vs Layered: flat идеален для CLI и прототипов до 500 строк, но всё связано со всем и тесты требуют всей системы; слои дают разделение ответственности и мок репозитория ценой каталогов handlers/, services/, repositories/ и рефакторинга по сигналу «файлы больше 300 строк».
  • Layered vs Clean Architecture: в слоях сервис зависит от конкретного *postgres.RepoRepository — смена БД бьёт по бизнес-логике; Clean развязывает их интерфейсом repository.RepoRepository, но платит интерфейсами, DTO и мапперами — больше кода и дисциплины от команды.
  • Hexagonal vs Clean Architecture: различие только в акцентах: явные порты in//out/ окупаются при десятке интеграций (HTTP, gRPC, Kafka, PostgreSQL); для CRUD с одной БД эти слои абстракции избыточны.
  • Modular Monolith vs Microservices: монолит — один деплой, вызовы функций и транзакции между модулями repos/, users/, billing/, но один процесс — одна точка отказа; микросервисы дают независимый деплой и масштабирование, но платят сетевыми вызовами, Saga и eventual consistency.
  • Over-engineering vs Under-engineering: Clean Architecture для простого CRUD — это Resume Driven Development и раздутый бойлерплейт; Flat для сложного бизнеса не масштабируется и усложняет тестирование.

Практическое задание

Задание 1: Рефакторинг Flat -> Layered

Дан flat-проект:

text
myapp/
├── main.go        # всё в одном файле
└── go.mod

Рефакторите в Layered:

text
myapp/
├── cmd/app/main.go
├── handlers/
├── services/
├── repositories/
└── models/

Задание 2: Рефакторинг Layered -> Clean Architecture

Дан Layered-проект. Рефакторите в Clean Architecture:

  • Вынесите интерфейсы в internal/repository/
  • Реализации в internal/adapters/
  • Добавьте usecase слой
  • Настройте DI

Задание 3: Сравнение подходов

Для каждой ситуации выберите архитектуру и обоснуйте:

  1. CLI-утилита для конвертации файлов
  2. API для блога (5 эндпоинтов, один разработчик)
  3. API для e-commerce (50 эндпоинтов, 5 разработчиков)
  4. Система из 3 команд, каждая владеет своим доменом
  5. Сервис с интеграциями: PostgreSQL, Kafka, S3, внешний API

Задание 4: Dependency Rule

Нарисуйте диаграмму зависимостей для шаблона проекта. Проверьте что нет нарушений Dependency Rule:

  • models не импортируют service
  • service не импортирует adapters
  • usecase не импортирует handlers

Задание 5: Обоснование выбора

Напишите ADR (Architecture Decision Record) для выбора архитектуры вашего проекта:

text
# ADR-001: Выбор Clean Architecture

## Статус
Accepted

## Контекст
- Размер команды: 4 человека
- Количество эндпоинтов: 15
- Нужна тестируемость без БД
- Планируется миграция Serverless -> VDS

## Решение
Используем Clean Architecture потому что...

## Альтернативы
- Layered: проще, но не даёт тестируемости
- Microservices: избыточно для одной команды

## Последствия
- Больше boilerplate кода
- Легче менять инфраструктуру
- Можно тестировать без реальной БД

Quiz "Проверь себя"

1. Какие критерии важны при выборе архитектуры под задачу? — выберите все подходящие
2. Какие проблемы порождает преждевременный переход на микросервисы? — выберите все подходящие
3. Чем Modular Monolith отличается от микросервисов?
4. Какая зависимость здесь соответствует Dependency Rule?
go
type RepoRepository interface {
    GetByID(ctx context.Context, id string) (*models.Repo, error)
}
5. Что не так с этим кодом?
go
type RepoService struct {
    repo *postgres.RepoRepository
}
6. Куда направлены зависимости при соблюдении Dependency Rule?
7. Почему handlers не должны знать о postgres?
8. Чем Clean Architecture отличается от Layered?
9. Что такое Ports and Adapters?
10. Когда Hexagonal Architecture избыточна?

✅ Итоги модуля

Студент должен:

  • Различать 6 архитектурных стилей
  • Объяснять Dependency Rule
  • Осознанно выбирать архитектуру под задачу
  • Рефакторить между стилями
  • Обосновывать выбор через ADR
  • Понимать структуру шаблона проекта
  • Ответить на все вопросы Quiz

🔗 Мостик к Модулю 3

Следующий модуль: Модуль 3. Clean Architecture Deep Dive (ключевой модуль) - Dependency Inversion, Use Cases vs Services, DTO vs Domain Models, DI-контейнер, путь запроса через все слои. Привязка к internal/usecase/, internal/service/, internal/repository/, internal/adapters/ шаблона.