Архитектура MARV
Оглавление
- Обзор архитектуры
- Структура проекта
- Domain-Driven Design
- Слои приложения
- Ключевые паттерны
- Модули системы
- Обработка событий
- Управление конфигурацией
Обзор архитектуры
MARV построен на принципах Domain-Driven Design (DDD) с чётким разделением на слои.
Архитектурные принципы
- Layered Architecture - чёткое разделение ответственности
- Dependency Inversion - зависимости направлены к domain
- Value Objects - типобезопасные идентификаторы
- Event-Driven - асинхронная обработка через EventBus
- Repository Pattern - абстракция доступа к данным
- Factory Pattern - создание сложных объектов
Структура проекта
marv/
├── cmd/ # Точки входа (бинарники)
│ ├── marv/ # Основной сервер
│ │ └── main.go
│ ├── hgoose/ # Утилита миграций БД (goose v3)
│ └── config-migrator/ # Миграция legacy-конфигов
│
├── domain/ # Ядро бизнес-логики (не зависит ни от чего)
│ ├── models/ # Domain модели
│ │ ├── vo/ # Value Objects (22 типа ID, с UnmarshalJSON валидацией)
│ │ ├── user.go
│ │ ├── world.go
│ │ └── ...
│ ├── services/ # Domain сервисы (чистая логика)
│ ├── repositories/ # Интерфейсы репозиториев
│ ├── interfaces/ # Интерфейсы для внешних зависимостей
│ ├── event_bus/ # События и обработчики
│ ├── errors/ # Domain ошибки
│ └── shared/ # Общие типы (ApiType, cachekeys, RawExpr)
│
├── application/ # Оркестрация use-cases
│ ├── services/ # Application сервисы
│ ├── container/ # DI Container
│ ├── dto/ # Data Transfer Objects
│ ├── mappers/ # Domain ↔ DTO преобразования
│ └── interfaces/ # Интерфейсы application слоя
│
├── infrastructure/ # Внешние зависимости (PostgreSQL, Redis, HTTP)
│ ├── database/ # GORM, миграции
│ │ └── postgres/ # PostgreSQL драйвер
│ ├── cache/ # Redis implementation
│ │ └── redis/
│ ├── repositories/ # Реализации репозиториев
│ │ └── postgres/ # PostgreSQL repositories
│ ├── config/ # Конфигурация (Koanf) + JSON Schema
│ ├── logger/ # Логирование (zerolog)
│ └── platform/ # Интеграции платформ
│
├── internal/ # Внутренние реализации (private)
│ ├── infrastructure/
│ │ ├── database/
│ │ │ ├── mapper/ # ORM ↔ Domain преобразования
│ │ │ └── orm/ # GORM модели
│ │ ├── repositories/
│ │ │ └── decorators/ # Cache decorators
│ │ ├── tasks/ # Cron tasks система
│ │ │ ├── factory.go
│ │ │ └── implementations/
│ │ │ ├── data_clean.go
│ │ │ ├── alert_dispatcher.go
│ │ │ └── user_data_batch_update.go
│ │ ├── scheduler/ # Планировщик задач
│ │ └── cloudflare/ # Cloudflare Stream API
│ ├── alerts/ # Alert providers (Telegram, Slack)
│ └── observability/ # Метрики, мониторинг
│
├── interfaces/ # HTTP-слой
│ └── api/
│ ├── controllers/ # HTTP контроллеры
│ │ ├── v1/ # API v1 (deprecated)
│ │ ├── v2/ # API v2 (основной)
│ │ ├── ext/ # External webhooks
│ │ └── acp/ # Admin Control Panel
│ ├── middlewares/ # HTTP middleware (rate_limit, body_limit, timeout)
│ └── router/ # Роутинг (Gin)
│
├── modules/ # Модульная система (config-driven)
│ ├── factory.go # Factory для создания модулей
│ ├── manager.go # Менеджер жизненного цикла (идемпотентный)
│ ├── implementations/ # Реализации модулей (19 шт)
│ │ ├── user.go, world.go, transaction.go, message.go
│ │ ├── stream.go, event.go, traffic_flow.go
│ │ ├── abtest.go, ads.go, alert.go
│ │ └── ...
│ └── types/ # Типы модулей
│
├── pkg/ # Публичные пакеты
│ └── engine/ # Engine для запуска сервера
│
├── config/ # Конфигурационные файлы
│ ├── config.example.yml # Пример конфигурации
│ ├── .env.example # Пример переменных окружения (MARV__*)
│ ├── config.debug.yml
│ ├── config.release.yml
│ ├── config.test.yml
│ ├── bots.*.json
│ └── products.json
│
├── scripts/ # Вспомогательные скрипты
│ └── deploy.sh # Помощник деплоя (migrate, run, config-migrate)
│
├── migrations/ # SQL-миграции (goose v3)
├── docs/ # Документация
│
├── Dockerfile # Multi-stage build (marv + hgoose + config-migrator)
├── docker-compose.yml # Локальная разработка (app + Postgres + Redis)
├── Makefile # Сборка, линт, тесты, бандлы
└── .gitlab-ci.yml # CI/CD (lint → test → security → build/release)Domain-Driven Design
Value Objects
23 типа типобезопасных идентификаторов в domain/models/vo/:
Core Entities:
├── user_id.go - UserId
├── world_id.go - WorldId
├── message_id.go - MessageId
├── transaction_id.go - TransactionId
├── stream_id.go - StreamId
├── stream_input_id.go - StreamInputId
├── event_id.go - EventId
├── event_result_id.go - EventResultId
├── traffic_flow_id.go - TrafficFlowId
├── traffic_flow_entry_id.go - TrafficFlowEntryId
└── rival_id.go - RivalId
System Entities:
├── ab_test_id.go - AbTestId
├── remote_config_id.go - RemoteConfigId
├── cron_task_id.go - CronTaskId
├── alert_id.go - AlertId
├── alert_delivery_log_id.go - AlertDeliveryLogId
├── acp_user_id.go - AcpUserId
└── ad_id.go - AdId
Backup Entities:
├── user_backup_id.go - UserBackupId
├── world_backup_id.go - WorldBackupId
└── user_data_update_id.go - UserDataUpdateIdПреимущества:
- Невозможно перепутать типы ID на этапе компиляции
- Type-safe операции
- Легко расширять (добавлять domain методы)
- Валидация при десериализации (
UnmarshalJSONотклоняет отрицательные значения)
Пример использования:
// Правильно - невозможно перепутать параметры
func CreateMessage(userID UserId, worldID WorldId, eventID EventId)
// Неправильно - легко перепутать
func CreateMessage(userID int64, worldID int64, eventID int64)Domain Abstractions (domain/shared)
RawExpr — абстракция для raw SQL выражений, позволяющая domain-сервисам формировать SQL-выражения (например, counter + 1) без импорта gorm. Infrastructure-слой конвертирует RawExpr в gorm.Expr:
// domain — формирует выражение
updates["execution_count"] = shared.NewExpr("execution_count + ?", 1)
// infrastructure — конвертирует для GORM
if expr, ok := v.(shared.RawExpr); ok {
out[k] = gorm.Expr(expr.SQL, expr.Vars...)
}Rich Domain Models
Модели содержат бизнес-логику, а не только данные:
// Rich Model
type User struct {
ID UserId
Banned bool
// ...
}
func (u *User) Ban() { u.Banned = true }
func (u *User) Unban() { u.Banned = false }
func (u *User) IsBanned() bool { return u.Banned }
func (u *User) CanCreateWorld() bool { return !u.IsBanned() }Модели с rich behavior:
User- ban/unban, attach to ACP, session managementWorld- IsDefault(), BelongsToUser()Message- IsCompleted(), MarkAsCompleted(), IsExpired()Stream- IsFull(), CanAddInput()Event- IsActive(), IsFinished(), CanAddResult()
Слои приложения
1. Domain Layer (Ядро)
Ответственность: Бизнес-логика, инварианты, правила
Компоненты:
- Models - сущности и value objects
- Services - сложная domain логика (координация entities)
- Repositories - интерфейсы (не реализации!)
- Events - доменные события
- Errors - доменные ошибки
Правила:
- НЕ зависит от других слоёв
- НЕ знает о БД, HTTP, кэше
- Только чистая бизнес-логика
- Только Go стандартная библиотека + domain dependencies
Пример:
// domain/services/user.go
func (s *UserService) BanUser(ctx context.Context, userID UserId, reason string) error {
user, err := s.repo.GetByID(ctx, userID)
if err != nil {
return err
}
// Бизнес-логика в domain
user.Ban()
// Сохранение через интерфейс
return s.repo.Update(ctx, user)
}2. Application Layer (Оркестрация)
Ответственность: Use cases, координация domain объектов
Компоненты:
- Services - оркестрация domain сервисов
- Container - Dependency Injection
- DTOs - передача данных между слоями
- Mappers - преобразование Domain ↔ DTO
Правила:
- Зависит от Domain
- НЕ зависит от Infrastructure/Interfaces
- Оркестрирует несколько domain сервисов
- Управляет транзакциями
Пример:
// application/services/user.go
func (s *UserService) BanUserWithCleanup(ctx context.Context, dto *BanUserDTO) error {
// Оркестрация нескольких domain операций
if err := s.domain.BanUser(ctx, dto.UserID, dto.Reason); err != nil {
return err
}
// Очистка связанных данных
if err := s.domain.InvalidateSessions(ctx, dto.UserID); err != nil {
return err
}
// Публикация события
event := NewUserBannedEvent(dto.UserID, dto.Reason)
return s.eventBus.Publish(ctx, event)
}3. Infrastructure Layer (Реализации)
Ответственность: Внешние зависимости (БД, кэш, HTTP)
Компоненты:
- Repositories - PostgreSQL реализации
- Cache - Redis реализации
- Config - Koanf конфигурация
- Logger - zerolog логирование
- Platform - интеграции с платформами
Правила:
- Реализует интерфейсы из Domain
- Конвертирует Domain ↔ ORM
- Использует внешние библиотеки
- НЕ содержит бизнес-логику
Пример:
// infrastructure/repositories/postgres/user.go
func (r *userRepository) GetByID(ctx context.Context, id UserId) (*models.User, error) {
var record orm.UserRecord
err := r.db.Where("id = ?", id.Int64()).First(&record).Error
if err != nil {
return nil, err
}
// Конвертация ORM → Domain
return mapper.UserDomainFromRecord(&record), nil
}4. Interfaces Layer (API)
Ответственность: HTTP endpoints, CLI
Компоненты:
- Controllers - HTTP обработчики
- Middlewares - аутентификация, CORS, metrics
- Router - маршрутизация (Gin)
Правила:
- Зависит от Application/Domain интерфейсов
- Валидация входных данных
- Преобразование HTTP ↔ DTO
- НЕ содержит бизнес-логику
Пример:
// interfaces/api/controllers/v2/users.go
func (c *UsersController) BanUser(ctx *gin.Context) {
var dto dto.BanUserDTO
if err := ctx.ShouldBindJSON(&dto); err != nil {
ctx.Error(apierrors.ErrInvalidInput.WithCause(err))
return
}
// Делегирование в application layer
if err := c.service.BanUser(ctx.Request.Context(), &dto); err != nil {
ctx.Error(err)
return
}
ctx.JSON(200, gin.H{"success": true})
}Ключевые паттерны
1. Repository Pattern
Абстракция доступа к данным:
// domain/repositories/user.go (интерфейс)
type UserRepository interface {
GetByID(ctx context.Context, id UserId) (*User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
}
// infrastructure/repositories/postgres/user.go (реализация)
type userRepository struct {
db database.Store
}
// internal/infrastructure/repositories/decorators/user_cached.go (decorator)
type cachedUserRepository struct {
inner repositories.UserRepository
cache interfaces.CacheStore
}Преимущества:
- Легко подменить реализацию (для тестов)
- Cache decorator прозрачно добавляет кэширование
- Единая точка доступа к данным
2. Factory Pattern
Создание сложных объектов:
// modules/factory.go
type Factory struct {
modules map[types.ModuleType]func(*container.Container) interfaces.Module
}
func (f *Factory) Create(moduleType types.ModuleType, container *container.Container) (interfaces.Module, error) {
creator := f.modules[moduleType]
return creator(container), nil
}Используется для:
- Модули (
modules/factory.go) - Tasks (
internal/infrastructure/tasks/factory.go)
3. Dependency Injection
Центральный контейнер:
// application/container/container.go
type Container struct {
config interfaces.ConfigStore
db database.Store
cache interfaces.CacheStore
eventBus interfaces.EventBus
// ... сервисы
}
func (c *Container) Users() interfaces.UserService {
c.once.Do(func() {
c.users = services.NewUserService(...)
})
return c.users
}Преимущества:
- Ленивая инициализация
- Единая точка создания зависимостей
- Легко подменить для тестов
4. Event-Driven Architecture
EventBus для async операций:
// domain/event_bus/events/user.go
type UserBannedEvent struct {
UserID UserId
Reason string
}
// domain/event_bus/handlers/user.go
type UserEventHandler struct {
repo repositories.UserRepository
}
func (h *UserEventHandler) HandleUserBanned(ctx context.Context, event *events.UserBannedEvent) error {
// Обработка события
log.Info().Msg("User banned")
return nil
}
// Регистрация в container
func (c *Container) setupEventHandlers() {
handler := handlers.NewUserEventHandler(c.Users())
c.eventBus.Subscribe("user.banned", handler.HandleUserBanned)
}Модули системы
Модульная архитектура
19 модулей в modules/implementations/:
| Модуль | Файл | Описание |
|---|---|---|
| User | user.go | Пользователи и профили |
| World | world.go | Игровые миры |
| Transaction | transaction.go | IAP транзакции |
| Message | message.go | Система сообщений |
| Stream | stream.go | Livestream функционал |
| Event | event.go | Игровые события |
| TrafficFlow | traffic_flow.go | Офферная механика |
| Rival | rival.go | Соперники |
| AbTest | abtest.go | A/B тестирование |
| RemoteConfig | remote_config.go | Удалённые конфиги |
| Ads | ads.go | Реклама |
| Alert | alert.go | Система оповещений |
| Product | product.go | Каталог продуктов |
| Merchant | merchant.go | Merchant хендлеры |
| Platform | platform.go | Platform хендлеры |
| Bots | bots.go | Telegram и др. боты |
| CronTask | cron_task.go | Планировщик |
| AcpUser | acp_user.go | ACP пользователи |
| System | system.go | Системные функции |
Загрузка модулей
Модули загружаются из конфига app.modules (рекомендуется) или программно через engine.With...():
app:
modules:
- user
- world
- transaction
- stream
- eventManager.Create() идемпотентен — дублирование безопасно при комбинации конфига и кода.
Интерфейс модуля
type Module interface {
Type() types.ModuleType
}Модули реализуют дополнительные интерфейсы для предоставления контроллеров различных API-слоёв (GetV1Controllers(), GetV2Controllers(), GetAcpControllers(), GetExtControllers()). Роутеры опрашивают модули через Manager и регистрируют контроллеры.
Фазы:
- Create — Factory создаёт модуль по типу из конфига (
app.modules) или программно - Routes — Router'ы запрашивают контроллеры у модулей и регистрируют HTTP endpoints
Обработка событий
EventBus
Два типа шин:
// Синхронная (блокирующая)
c.eventBus = event_bus.NewInMemoryEventBus()
// Асинхронная (non-blocking, workers)
c.asyncEventBus = event_bus.NewAsyncEventBus(10)События в системе
| Событие | Тип | Описание |
|---|---|---|
IapVerifiedEvent | Sync | IAP транзакция проверена |
TrafficFlowCompletedEvent | Async | Оффер завершён |
UserBannedEvent | Async | Пользователь забанен |
Регистрация обработчика
// application/container/container.go
func (c *Container) setupEventHandlers() {
// IAP handler
iapHandler := handlers.NewIapVerificationHandler(
c.Transactions(),
c.Users(),
)
c.eventBus.Subscribe("iap.verified", iapHandler.Handle)
}Управление конфигурацией
Структура конфигов
config/
├── config.example.yml # Пример конфигурации
├── .env.example # Пример env-переменных (MARV__*)
├── config.debug.yml # Dev режим
├── config.release.yml # Production
├── config.test.yml # Тесты
├── bots.debug.json # Telegram боты (dev)
├── bots.release.json # Telegram боты (prod)
└── products.json # Каталог продуктовJSON Schema валидация
infrastructure/config/koanf/schemas/
├── config.schema.json # Основная схема
├── bots.schema.json # Схема ботов
└── products.schema.json # Схема продуктовEnvironment Variables
Формат: MARV__SECTION__SUBSECTION__KEY (только main config)
# Переопределение порта
MARV__APP__SERVER__PORT=9090
# Duration типы
MARV__DOMAIN__CLEANUP__DATA_RETENTION=4320h # 180 днейDuration типы вместо int
# Правильно
domain:
cleanup:
data_retention: 4320h # 180 days
# Неправильно (deprecated)
domain:
cleanup:
user_retention_days: 180Data Flow
Типичный запрос (создание world):
1. HTTP Request
↓
2. Controller (interfaces/api/controllers/v2/worlds.go)
- Валидация JSON
- Создание DTO
↓
3. Application Service (application/services/world.go)
- Оркестрация
- Вызов domain сервисов
↓
4. Domain Service (domain/services/world.go)
- Бизнес-логика
- Валидация invariants
↓
5. Repository (infrastructure/repositories/postgres/world.go)
- Конвертация Domain → ORM
- SQL запрос
↓
6. Cache Decorator (internal/infrastructure/repositories/decorators/world_cached.go)
- Инвалидация кэша
↓
7. EventBus
- Публикация WorldCreatedEvent
↓
8. Response
- Конвертация Domain → DTO
- HTTP 201 CreatedТестирование
Структура тестов
domain/services/*_test.go - Domain logic unit tests
application/services/*_test.go - Application use cases tests
interfaces/api/controllers/*_test.go - HTTP handlers testsПаттерн: Local Interfaces + Stubs
// interfaces/api/controllers/v2/users_test.go
type stubUserService struct {
getUserResponse *models.User
getUserError error
}
func (s *stubUserService) GetUser(ctx, id) (*models.User, error) {
return s.getUserResponse, s.getUserError
}
func TestUsersController_GetUser(t *testing.T) {
service := &stubUserService{
getUserResponse: &models.User{ID: 1},
}
controller := NewUsersController(service)
// ... тестируем контроллер
}Преимущества:
- Не нужна реальная БД/кэш
- Быстрые тесты
- Легко тестировать edge cases
Метрики и мониторинг
Prometheus метрики
GET /acp/system/metricsМетрики:
- HTTP requests (duration, count, errors)
- DB queries (duration, count)
- Cache operations (hits, misses)
- Task executions (duration, success/failure)
Structured Logging
// zerolog
log.Info().
Str("user_id", userID.String()).
Str("action", "ban").
Msg("User banned")Alert система
internal/infrastructure/tasks/implementations/alert_dispatcher.goДоставка через:
- Telegram
Производительность
Cache Strategy
Кэш:
- Redis - distributed
TTL политики:
- User data: 1 hour
- Worlds: 30 minutes
- Products: 24 hours
- Remote configs: 5 minutes
Database Optimization
Индексы:
- Composite indexes для cleanup операций
- Covering indexes для частых запросов
Connection Pooling:
infrastructure:
database:
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 300sБезопасность
Аутентификация
Client API (V1/V2):
- Platform signature validation
X-Api-Keyheader
ACP:
- Google OAuth
X-Api-Access-Tokenheader
External webhooks:
- Signature verification
- IP whitelisting (опционально)
Rate Limiting
In-memory token bucket (per-IP), конфигурируется через:
app:
security:
rate_limit:
enabled: true
rps: 100
burst: 200При превышении — HTTP 429. Старые записи IP автоматически очищаются.
Авторизация
Role-based access control:
router.WithControllerRole(access.RoleAdmin)
router.WithControllerRole(access.RoleModerator)Статический анализ
В CI и линтере включены: gosec (уязвимости), bodyclose (утечка HTTP-ресурсов), noctx (пропущенный context). Сканирование зависимостей: govulncheck.
