Модульный монолит, паттерны взаимодействия между модулями
Итак, мы решили начать проект с модульного монолита. Теперь встает вопрос каким образом построить коммуникацию между модулями. Одним из ключевых аспектов модульного монолита - организация правильного взаимодействия между модулями.
Главный принцип: Модуль НЕ ДОЛЖЕН знать о внутренней реализации другого модуля! Все взаимодействие — ТОЛЬКО через строго определенные публичные интерфейсы (контракты).
Существует несколько подходов коммуникации между модулями. Есть варианты синхронного и асинхронного взаимодействия. Разберем несколько из них.
Синхронные вызовы
В этом варианте нам необходим синхронный вызов одного модуля другим. Здесь есть множество вариаций организации такой связи. Вызов метода API напрямую и использование подхода "Ports and Adapters"
Вызов API другого модуля напрямую
Это самый простой вариант взаимодействия между модулями - модуль напрямую вызывает API другого через интерфейс:
Этот вариант прост в реализации и не требует дополнительных обвязок и/или библиотек/фреймворков.
Преимущество такого подхода:
- Скорость вызова
- Легко реализовать
- Нет направленности связей
Синхронное взаимодействие означает, что модули будут сильно связаны друг с другом и при недоступности вызываемого модуля, вызывающий также не сможет работать. При выделении микросервиса из модуля, в этом варианте нужно предусмотреть синхронный протокол взаимодействия. Дополнительная сложность тестирования - нужны моки вызываемого сервиса.
Использование подхода Port/Adapter
Этот вариант отличается от предыдущего дополнительным слоем абстракции. Который позволяет абстрагироваться от вызываемого модуля. В этом случае модуль вызывает абстрактный класс/интерфейс, а непосредственный вызов производится на уровне реализации порта - адаптер.
Помимо преимуществ, которые мы получаем при прямом вызове метода API, мы получаем еще:
- Легко менять реализацию (что будет огромным плюсом при преобразовании в микросервис).
- Упрощается тестирование - можно реализовывать моки на любой вкус.
Но в месте с дополнительным уровнем абстракции возникает и минус такого подхода - больше кода, а следовательно сложнее разобраться новым разработчикам.
Асинхронные вызовы
В этом случае взаимодействие между модулями полностью асинхронное. Модуль публикует событие в шину событий внутри процесса. Модули, заинтересованные в этом событии, подписываются на него и асинхронно обрабатывают его когда смогут. Модуль публиковавший события не знает и не ждет реакции подписчиков. Например, можно реализовать отправку и обработку событий с помощью Spring Events
Преимущество такого подхода:
- Четкое разделение ответственности - вызывающий знает какое событие нужно послать, обработчик знает как обработать это событие.
- Возможность асинхронного выполнения - не блокировать вызывающего.
При использовании данного подхода также можно воспользоваться подходом Ports and Adapters, что позволит абстрагироваться от механизма передачи сообщения и в дальнейшем вместо spring events использовать брокеры/очереди сообщений, например ZeroMQ или тот же RabbitMQ. Стоит учитывать, что передача результатов выполнения команд, также потребует дополнительных механизмов.
Итог
Правильное взаимодействие между модулями — это основа модульного монолита. Используйте:
- Синхронные вызовы для критичных операций.
- События для асинхронной коммуникации.
- Порты и адаптеры для изоляции внешних зависимостей.
Мой канал в telegram