SaaS архитектура веб-приложений — как устроены масштабируемые системы
Полное руководство по структурированию SaaS-приложения: от базовой MVC до модульной HMVC архитектуры. Как организовать код так, чтобы он оставался предсказуемым при росте проекта.
Ключевые цифры
MVC: основа разделения ответственности
MVC (Model-View-Controller) — это фундаментальный паттерн архитектуры, который разбивает приложение на три независимых слоя с чётко определённой ответственностью. Каждый слой отвечает за свою часть логики, что позволяет изолировать изменения и упростить тестирование.
Рассмотрим практический пример: пользователь заходит на страницу /billing/history (история платежей).
Как работает цикл запроса
- Роутинг: Запрос маршрутизируется к контроллеру
BillingHistoryController, методуindex - Контроллер: Выступает «дирижёром». Решает, какие данные нужны (платежи, информация о подписке) и обращается к соответствующим моделям
- Модель: Слой работы с данными. Например,
PaymentModelзнает, как получить платежи из базы данных, применить фильтры, сортировку - Вид (View): Получает данные от контроллера и превращает их в HTML-страницу (таблицы, графики, кнопки действий)
Главное преимущество такого разделения: изменение шаблона не затрагивает логику работы с данными. Вы можете переделать дизайн таблицы платежей, и при этом логика выборки платежей из базы остаётся нетронутой. Это снижает риск регрессии и упрощает разработку с использованием AI-помощников.
HMVC: модульная архитектура для крупных SaaS
По мере роста приложения простая MVC структура становится громоздкой. Когда в одной папке лежат контроллеры для регистрации, личного кабинета, админ-панели и API — навигация по коду становится кошмаром.
HMVC (Hierarchical MVC) решает эту проблему — приложение делится на независимые модули, каждый из которых содержит свой собственный MVC.
Примеры модулей
Auth— авторизация и аутентификацияUsers— управление пользователямиBilling— счета и платежиApiKeys— управление ключами доступаReports— аналитика и отчёты
Каждый модуль содержит:
- Свои контроллеры
- Свои модели данных
- Свои маршруты (роуты)
- Свои шаблоны (views)
Модули могут общаться через чёткие интерфейсы, но при этом остаются слабо связанными. Если задача изменить модуль Billing, разработчик (или AI-помощник) видит его структуру и пишет код, который не сломает Auth или Reports. Это особенно важно для автоматизированной разработки.
Четыре контекста приложения: Front, App, Admin, API
Один и тот же модуль может работать в разных контекстах — логических зонах приложения, в которых функциональность может отличаться по доступу и интерфейсу.
Front контекст (публичная часть)
Лендинги, блоги, страницы с информацией о продукте. Доступны без авторизации. Пример: модуль Pricing показывает на Front контекстах разные тарифы.
App контекст (личный кабинет)
Личный кабинет зарегистрированного пользователя: дашборд, настройки, управление своими данными. Пример: модуль User в контексте App позволяет пользователю редактировать свой профиль, загружать аватар, менять пароль.
Admin контекст (админ-панель)
Инструменты администратора: управление всеми пользователями, аналитика, модерация контента. Пример: модуль User в контексте Admin позволяет администратору видеть всех пользователей, блокировать аккаунты, менять их статусы.
API контекст (REST API)
Стандартный JSON API для мобильных приложений и интеграций. Пример: модуль Payments предоставляет эндпоинты GET /api/payments, POST /api/payments с полной документацией.
Ключевой момент: для модуля User используются разные контроллеры и шаблоны в каждом контексте, но одна и та же модель, которая работает с базой данных. Это гарантирует консистентность данных.
Сервисы: централизация повторяющейся логики
Когда одна и та же операция нужна в разных местах кода, возникает искушение скопировать логику. Но при следующем изменении требования нужно будет править код везде — и вероятность забыть где-то очень высока.
Проблема дублирования
Представьте регистрацию пользователя. Логика одна:
- Валидация email и пароля
- Проверка, что пользователь не существует
- Хеширование пароля
- Создание записи в базе
- Отправка приветственного письма
- Создание начальных настроек
Но эта логика требуется в трёх контекстах: Front (самообслуживание пользователя), Admin (регистрация администратором), API (программная регистрация). Если дублировать код, при изменении условия про проверку email все три места нужно найти и обновить.
Решение: сервисы
Сервис — это класс с бизнес-логикой, которая не принадлежит конкретной модели. Например, UserRegistrationService инкапсулирует всю сложную операцию регистрации.
Все контроллеры (Front, Admin, API) вызывают один сервис. Изменения вносятся в одном месте. Если требование про email изменилось — правишь в одном месте, и все три контекста сразу получают обновление.
Правило: Создавайте сервис, когда логика нужна в нескольких местах или когда действие затрагивает несколько моделей одновременно.
Провайдеры: абстракция для внешних интеграций
Приложение часто взаимодействует с внешними сервисами: почта (Mailgun, SendGrid), платежи (Stripe, PayPal), аналитика (Segment), облачное хранилище (S3).
Проблема: жёсткая связанность
Если код прямо обращается к API Mailgun для отправки писем, а потом вы решаете перейти на SendGrid — нужно переписывать все места в коде, где отправляется письмо. Риск ошибок очень высок.
Решение: провайдеры
Провайдер — это прослойка-абстракция между приложением и внешним сервисом. Весь код приложения говорит просто: «отправь письмо», не зная, через какой конкретный сервис это произойдёт.
EmailProvider::send($to, $subject, $body)— ваш интерфейс- Конкретная реализация (использует Mailgun API) описывается в отдельном классе
- Конфигурация (какого провайдера использовать) находится в config файле
При смене сервиса меняется только конфигурация и реализация провайдера. Весь остальной код остаётся без изменений. Это делает архитектуру устойчивой к будущим изменениям.
Примеры других провайдеров: PaymentProvider (Stripe vs PayPal), StorageProvider (S3 vs локальный диск), NotificationProvider (email vs SMS vs push).
Практический пример: модуль управления API-ключами
Все эти концепции применены в реальном модуле ApiKeys для управления ключами доступа к API.
Структура модуля
Модель ApiKey: работает с таблицей в базе, знает как сохранить и загрузить ключи.
Контексты:
App контекст:Пользователь создаёт новый ключ для своего приложения, видит его один раз (по соображениям безопасности)Admin контекст:Администратор видит все ключи, может отозвать ключ нарушителя, смотреть логи использованияAPI контекст:Внешние системы используют ключ для доступа к REST API
Сервис аутентификации
Сервис ApiKeyAuthenticationService проверяет:
- Извлекает ключ из заголовка запроса (обычно
Authorization: Bearer KEY) - Ищет хэш ключа в базе (оригинал ключа никогда не хранится в открытом виде)
- Проверяет, что ключ активен и не заблокирован
- Проверяет лимиты на количество запросов и потрачённые деньги
- При успехе — увеличивает счётчик использования
- При ошибке — возвращает HTTP 401 (Unauthorized) или 429 (Too Many Requests)
Безопасность: хеширование ключей
В базе сохраняется не сам ключ, а его SHA-256 хеш. Когда пользователь создаёт новый ключ, система показывает оригинальную строку ровно один раз. После этого восстановить невозможно. При проверке сравнивается хеш поступившего ключа с хешами в базе.
Этот подход защищает от компрометации базы данных: даже если база украдена, ключи остаются в безопасности.
Вопросы и ответы
Контроллер отвечает за HTTP-запросы: он парсит параметры, вызывает нужные операции и возвращает ответ. Сервис содержит <em>бизнес-логику</em> — правила, которые работают независимо от того, через какой интерфейс их вызывать (веб, API, консоль). Один сервис может использоваться несколькими контроллерами и даже другими сервисами.
Модуль нужен, когда функциональность достаточно самостоятельна (свои роуты, контроллеры, модели, вьюхи) — например, Billing, Auth, Reports. Сервис нужен, когда логика повторяется <em>внутри</em> разных контроллеров, но не требует собственных роутов и интерфейса — например, UserRegistrationService, PaymentProcessingService.
Да, это один из главных принципов. Модель User одна и та же для App контекста (редактирование профиля) и Admin контекста (управление всеми пользователями). Разные контроллеры и вьюхи, <em>одна модель</em> — это гарантирует консистентность данных в базе.
Когда вы абстрагируете внешние сервисы через провайдеры, замена одного сервиса на другой становится конфигурационным изменением, а не рефакторингом. Это экономит недели разработки и снижает риск регрессии при переходе с Mailgun на SendGrid или со Stripe на другой платёжный процессор.
Главное
Масштабируемая SaaS архитектура строится на чётких принципах разделения ответственности: MVC как основа, HMVC для модульности, контексты для разных интерфейсов, сервисы для повторяющейся логики, провайдеры для интеграций. Такая структура делает код предсказуемым, упрощает разработку с AI-помощниками и снижает количество ошибок при добавлении новых функций.