Phoenix (Elixir)SeniorExperience

Проект на 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.Conn throttling или 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 и безопасность
  • Умеет ранжировать риски по вероятности и влиянию

Related topics