В предыдущей статье по архитектуре мы говорили о том, что такое модульный монолит. На практике он хорошо подходит для проектов, где предметная область еще недостаточно изучена, а требования продолжают меняться.
На первом этапе нам важно найти баланс между скоростью разработки и качеством архитектуры. С одной стороны, не стоит заранее платить цену за архитектурные решения, пока структура системы и границы модулей продолжают меняться. С другой — не хочется позже прийти к необходимости переписывать значительную часть системы.
Ниже — рекомендации, которые помогают сохранить этот баланс.
1. Слои Разделяем модуль на логические слои. Обычно это Domain (бизнес-логика), Application (сценарии использования), Presentation (точки входа и взаимодействие с внешним миром) и Infrastructure (код фреймворка, библиотек и другие детали реализации). Зависимости между слоями всегда направлены в сторону бизнес-логики. Мы явно разделяем назначение кода внутри модуля.
2. Взаимодействие между модулями Организуем взаимодействие модулей через контракты (интерфейсы). На первом этапе их реализации напрямую вызывают другие модули, не добавляя преждевременно сетевое взаимодействие и связанные с ним издержки. Мы уменьшаем связанность системы. В дальнейшем модуль выделяется в отдельный микросервис с минимальными изменениями.
3. Владение данными Используем одну базу данных, разделив таблицы между модулями, например с помощью префиксов. Каждый модуль изменяет только свои данные. Мы не усложняем систему несколькими хранилищами данных, но сохраняем возможность в дальнейшем выделить данные каждого модуля отдельно.
4. Тестирование Пишем подтверждающие сквозные тесты на основные бизнес-сценарии. При необходимости дополнительно покрываем unit-тестами критичную для бизнеса логику с большим количеством бизнес-правил. Мы убеждаемся, что функциональность работает, фиксируем границы модулей и получаем возможность безопасно развивать и рефакторить систему.
5. Shared-модуль Общую функциональность выделяем в отдельный Shared-модуль. В него выносятся переиспользуемый код и контракты. Мы уменьшаем дублирование кода и избавляемся от необходимости повторной реализации. В дальнейшем Shared может быть вынесен в отдельный пакет и использоваться несколькими сервисами.
6. Read-модуль Реализуем отдельный Read-модуль для выдачи данных пользовательским интерфейсам. Формируем примитивную read-модель непосредственно на существующей базе данных, а повторяющиеся запросы группируем в сервисах. Мы отделяем чтение от изменения данных и получаем возможность отдельно оптимизировать выдачу данных.
7. Gateway-модуль Выделяем отдельный Gateway-модуль для взаимодействия с пользовательскими интерфейсами. Он становится единой точкой входа для клиентов и местом реализации кросс-функциональной логики, например аутентификации и кеширования. Мы отделяем представление данных от внутренней архитектуры системы и при необходимости можем перейти к отдельным BFF (Backend for Frontend).