Проект на Phoenix (Elixir) вырос из MVP в большой production-сервис. Какие проблемы архитектуры и сопровождения вы ожидаете увидеть?
При росте MVP главные проблемы: god context с N+1 запросами, синхронные тяжёлые операции вместо Oban, несовместимые DB-миграции при rolling deploy, stateful LiveView без стратегии reconnect и отсутствие структурированного логирования.
Рост Phoenix-проекта от MVP до production
Проект, выросший из MVP, несёт технический долг в архитектуре, структуре кода и операционных процессах. Ниже — конкретные проблемы, с которыми сталкиваются команды.
1. Монолитный Context
MVP часто имеет один-два Phoenix Context (Accounts, Core), которые со временем становятся «мусорными корзинами» с сотнями функций. Симптомы: circular dependencies между контекстами, невозможность вынести логику в отдельный сервис.
- Разбейте по bounded contexts:
Billing,Notifications,Catalog,Analytics. - Введите явные публичные API контекстов; запретите вызовы внутренних схем из соседних контекстов.
- Используйте
mix xref graph --format dotдля визуализации зависимостей и обнаружения циклов.
2. N+1 запросы в Ecto
В MVP часто забывают о preload. При росте данных latency резко возрастает.
# Проблема:
users = Repo.all(User)
users |> Enum.map(& &1.posts) # N+1
# Решение:
users = Repo.all(User) |> Repo.preload(:posts)
# Или явный join:
from(u in User, preload: [:posts]) |> Repo.all()
Включите Ecto.DevLogger или :telemetry-хук для подсчёта запросов на endpoint.
3. Отсутствие фоновых задач
MVP часто делает тяжёлые операции (email, PDF, внешние API) синхронно в контроллере. При росте это создаёт timeout-ы и user-facing latency. Решение — Oban:
defmodule MyApp.Workers.SendEmail do
use Oban.Worker, queue: :emails, max_attempts: 3
@impl true
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
user = Accounts.get_user!(user_id)
Mailer.send_welcome(user)
:ok
end
end
# В контроллере:
%{user_id: user.id}
|> MyApp.Workers.SendEmail.new()
|> Oban.insert()
4. Миграции без обратной совместимости
Добавление NOT NULL колонки без default значения ломает rolling deploy: новый код ожидает колонку, старый инстанс пишет без неё. Стратегия: expand-migrate-contract в три деплоя.
5. Stateful LiveView при масштабировании
LiveView хранит state в памяти процесса. При горизонтальном масштабировании reconnect клиента к другой ноде теряет state. Решения: хранить state в БД/Redis, использовать sticky sessions на балансировщике, минимизировать state в assigns.
6. Отсутствие структурированного логирования
В MVP логи — строки. В production без structured logging невозможно строить алерты и корреляцию. Настройте Logger с JSON-форматтером:
config :logger,
backends: [:console],
level: :info
config :logger, :console,
format: {MyApp.LogFormatter, :format},
metadata: [:request_id, :user_id, :trace_id]
7. Тесты без изоляции
MVP тесты часто используют глобальный state или не оборачивают тесты в транзакции. При росте — flaky tests. Решение: DataCase с async: true и Ecto.Adapters.SQL.Sandbox.
8. Отсутствие feature flags
Большие фичи нельзя выкатить безопасно без feature flags. Используйте FunWithFlags (хранит флаги в Redis/PostgreSQL) для постепенного rollout.
Подводные камни
- God Context. Единый контекст
MyApp.Coreс 500+ функциями делает code review и тестирование невозможными. Декомпозиция требует времени, но откладывать её дорого. - Прямые вызовы Repo из LiveView. LiveView не должен делать DB-запросы напрямую — только через Context API. Иначе нарушается разделение ответственности и невозможно мокировать в тестах.
- Отсутствие индексов на foreign keys. В MVP индексы часто забывают. Ecto не создаёт их автоматически при
references/2; добавляйтеcreate index(:orders, [:user_id])явно. - Secrets в application.ex. Хардкод токенов в коде при MVP — критическая уязвимость в production. Переходите на
runtime.exsи vault-интеграцию. - Нет rate limiting. MVP без
Plug.Connthrottling илиhammer-библиотеки открыт для брутфорса и DDoS. ДобавьтеHammerилиPlugAttackна ранних стадиях. - Накопленные down-migrations. Команды часто не пишут
downв миграциях. При rollback это критично. Договоритесь о стандарте миграций с первого дня.
What hurts your answer
- Говорить только о запуске Phoenix (Elixir), но не об эксплуатации
- Не упоминать observability, обновления, безопасность и rollback
- Описывать риски абстрактно, без способов их снижать
What they're listening for
- Видит production-риски Phoenix (Elixir)
- Говорит про monitoring, rollout, rollback и безопасность
- Умеет ранжировать риски по вероятности и влиянию