Spring Modulith – модульный монолит на Spring Boot

Оглавление

  1. Введение
  2. Проблемы классического монолита и вызовы микросервисов
  3. Spring Modulith: модульный монолит на практике
  4. Взаимодействие модулей через события
  5. Миграция от монолита к модульному монолиту (и дальше)
  6. Когда Spring Modulith – лучший выбор?
  7. Заключение

Введение

Представьте команду, поддерживающую крупное финансовое приложение – например, систему интернет-банкинга. Со временем монолитный код этой системы оброс “спагетти”-зависимостями: изменения в одном компоненте вызывают неожиданные поломки в другом, для каждого релиза приходится тестировать все приложение целиком. Возникает соблазн раздробить систему на микросервисы ради лучшей управляемости. Действительно, в последние годы архитектура микросервисов стала повсеместной. Однако прямой переход от запутанного монолита к десяткам микросервисов может обернуться еще большей сложностью и операционными затратами. Команда рискует получить распределенный “spaghetti”, но уже с сетью и DevOps-проблемами.

Альтернативный подход – эволюционировать через модульный монолит. Идея не новая: модульная (component-based) архитектура монолита снова набирает популярность. Проект Spring Modulith, призван поддержать разработчиков в построении domain-driven монолитов, разделенных на логические модули. Иными словами, Spring Modulith помогает “выровнять” структуру кода по доменным областям (например, модуль “Платежи”, модуль “Счета”, модуль “Отчеты” и т.д.), что ведет к более понятному и поддерживаемому приложению. Такой подход позволяет постепенно декомпозировать систему и ослабить связность компонентов, не прыгая сразу в омут микросервисов. В этой статье мы подробно разберем, как работает Spring Modulith, какие проблемы он решает и почему в некоторых случаях модульный монолит выгоднее микросервисной архитектуры.

Проблемы классического монолита и вызовы микросервисов

Монолитная архитектура исторически страдает от высокой связности компонентов. В одном приложении все части напрямую вызывают друг друга, часто минуя четкие интерфейсы. Это затрудняет сопровождение: изменение в одном модуле требует пересборки и повторного тестирования всего приложения, а рост проекта приводит к неуправляемому клубку зависимостей (“спагетти-коду”). Классический монолит накладывает и технические ограничения – единая кодовая база, единый стек технологий, единый цикл деплоя. Зачастую команды переходят на микросервисы именно из-за усталости от таких ограничений монолита.

Микросервисы обещают решать эти боли, разбивая систему на независимые сервисы по бизнес-капсулам. Каждый сервис можно развивать и масштабировать отдельно, выбирать для него подходящие технологии. В теории это дает слабо связанное, гибкое решение. Однако на практике микросервисы привносят свои сложности:

  • Сложность деплоя: теперь нужно развертывать и оркестрировать десятки сервисов, поддерживать для каждого конвейер CI/CD.
  • Коммуникации по сети: вызовы между сервисами идут через сеть, что добавляет задержки и точки отказа, требует устойчивости к сетевым сбоям.
  • Консистентность данных: данные разнесены по разным сервисам и БД. Глобальные транзакции затруднены, приходится мириться с eventual consistency или реализовывать распределенные паттерны (сага, outbox) для согласованности.
  • Оверхед в эксплуатации: микросервисный ландшафт требует продвинутого мониторинга, логирования, трассировки, а также автоматизации инфраструктуры. Без зрелых DevOps-практик и ресурсов небольшим командам сложно управлять такой системой.

Таким образом, цена микросервисов высока. Не каждой команде и не каждому проекту она по карману – особенно, если приложение среднего размера, требующее скорее целостности данных, чем бесконечной горизонтальной масштабируемости. Spring Modulith предлагает компромисс: разбить систему на модули, сохранив единое приложение. Вы получите многие плюсы декомпозиции (логическая изоляция, параллельная разработка, понятная структура) без накладных расходов распределенной системы. А при росте требований отдельные модули всегда можно эволюционно выделить в микросервисы – когда организация будет к этому готова.

Spring Modulith: модульный монолит на практике

Spring Modulith – это набор библиотек и соглашений поверх Spring Boot, помогающий организовать приложение в виде набора логических модулей. Под “модулем” понимается группа связанных по смыслу компонентов (спринговых бинов, классов, репозиториев), инкапсулированных в пределах пакета. Каждому доменному контексту – свой модуль. Например, в банковской системе могут быть модули: accounts (операции со счетами), transactions (платежи/транзакции), loans (кредиты), notifications (уведомления), каждый со своим внутренним устройством. Все они работают в одном приложении, но Spring Modulith задает строгие границы между ними на уровне кода и взаимодействия. Рассмотрим ключевые механизмы Modulith:

  • Структурирование пакетов – “Convention over Configuration”: По умолчанию Spring Modulith трактует каждый подпакет в корневом пакете приложения как отдельный модуль. Если ваш класс Application (аннотированный @SpringBootApplication) лежит в пакете com.myapp, то com.myapp.accounts, com.myapp.transactions и т.д. будут считаться отдельными модулями. API модуля – это все публичные классы в его базовом пакете, а внутренняя реализация – классы в подпакетах (например, com.myapp.transactions.internal) или пакеты, объявленные как internal. Код внутренних пакетов не должен напрямую использоваться извне модуля. Spring Modulith вводит это правило, поскольку Java не запрещает доступ к классам подпакетов (нет иерархической приватности пакетов). Например, если модуль transactions имеет пакет transactions.internal, то классы внутри него можно пометить обычным package-private уровнем доступа – Modulith будет считать их недоступными другим модулям. Все публичные компоненты, оставленные в самом пакете transactions, образуют публичный интерфейс модуля. Такое разделение повышает инкапсуляцию – внешний код не “видит” внутренностей чужого модуля и не сможет на них сослаться.
  • Проверка архитектурных границ: Чтобы убедиться, что разработчики не нарушают модульные границы, Spring Modulith предлагает механизм верификации. Достаточно написать простой тест с ApplicationModules.of(Application.class).verify() – и при запуске он проанализирует зависимости между пакетами. Если обнаружится, что какой-то модуль обращается к классу, не входящему в публичный API другого модуля, тест упадет с понятным сообщением. Внутри Modulith использует библиотеку ArchUnit, которая на уровне байткода проверяет отсутствие недопустимых зависимостей: циклических зависимостей между модулями, прямого доступа к внутренним классам, и т.п. Кроме того, вы можете явно задать допустимые зависимости: например, модуль transactions может быть разрешено вызывать модуль accounts, но не наоборот. Для этого служит аннотация @ApplicationModule(allowedDependencies = {…}) на файле package-info.java модуля. Таким образом, Modulith позволяет задать архитектурные правила и гарантирует, что код им следует – иначе сборка просто не пройдет тесты. Это защищает проект от постепенного размывания границ модулей по мере добавления нового функционала.
  • Модульное тестирование в изоляции: Еще одна отличительная особенность – возможность запускать интеграционные тесты, ограниченные конкретным модулем. Аннотация @ApplicationModuleTest заставляет Spring Boot контекст загрузить только указанный модуль (и его разрешенные зависимости) вместо всего приложения. Это похоже на slice-тесты в Spring (тесты с @WebMvcTest, @DataJpaTest и т.д.), но заточено под ваши бизнес-модули. Логика такая: Modulith находит модуль, в пакете которого лежит класс теста, и переконфигурирует сканирование компонентов и JPA-репозиториев только на этот пакет. В результате, тесты одного модуля не подтягивают бины из других – они изолированы от побочных эффектов соседей. Это ускоряет тестирование и упрощает отладку: падающий тест сразу указывает на проблему внутри конкретного контекста. При необходимости можно настроить загрузку нескольких модулей вместе или всего приложения Modulith дает гибкость. Но основной посыл каждый модуль можно протестировать как отдельную единицу, как если бы это был отдельный микросервис, только без поднятия множества процессов и моков.
  • Документация и диаграммы архитектуры: Spring Modulith не останавливается на проверке – он еще и помогает наглядно представить вашу модульную архитектуру. Встроенный класс Documenter генерирует артефакты документации: диаграммы связей модулей и так называемый Application Module Canvas – сводную таблицу по каждому модулю. Диаграммы могут быть в нотации UML или по модели C4. Например, Modulith способен сгенерировать диаграмму контейнеров C4, где контейнером является все приложение, а внутри него показаны компоненты-модули и зависимости между ними. Также можно построить диаграммы для каждого модуля в отдельности – с его ближайшими соседями. В Canvas-таблице перечисляются ключевые детали каждого модуля: компоненты (сервисы, репозитории), публикуемые и потребляемые события, используемые агрегаты данных, настройки и т.д. своего рода паспорт модуля. Все это собирается автоматически на основе кода! Вам достаточно вызвать new Documenter(modules).writeDocumentation() в тесте и Modulith сгенерирует PlantUML-диаграммы и AsciiDoc-файлы описаний, которые вы можете включить в проектную документацию. Такая генерация снижает трудозатраты на актуализацию архитектурных схем и помогает новым участникам команды быстрее понять устройство приложения.

Рис. 1: пример C4-диаграммы модульного монолита

На ней один Spring Boot контейнер (монолитное приложение) содержит четыре модуля: gateway, department, employee, organization. Стрелками показаны зависимости: например, модуль gateway предоставляет REST API и обращается к внутренним сервисам модулей department, employee, organization. Модули взаимодействуют между собой через события – это обозначено логическими связями между ними. Подобные диаграммы Spring Modulith может генерировать автоматически.

  • Отслеживание взаимодействия модулей (Observability): В распределенных системах принято трассировать запросы через микросервисы. Modulith предлагает аналогичную возможность внутри монолита – трассировка вызовов между модулями. Добавив специальный модуль наблюдения, вы получите автоматическое создание спанов Micrometer/Zipkin для всех межмодульных вызовов. Технически это реализовано через аспект, оборачивающий публичные бины каждого модуля. Когда, к примеру, модуль Payments вызывает модуль Accounts (через публикацию события или прямой вызов разрешенного бина), Modulith засечет это и создаст трассировочный span. В результате на графике в Zipkin вы увидите последовательность шагов внутри одного процесса, словно у вас действительно несколько сервисов. Это чрезвычайно полезно для отладки и мониторинга – можно понять, какой модуль сколько времени обрабатывал событие, где узкие места. Причем все остается внутри одного приложения, без overhead распределенной системы.
  • Domain Events как основной способ связи: Главный принцип modulith – модули должны быть как можно слабее связаны между собой. Поэтому прямые вызовы сервисов из модуля в модуль не поощряются. Вместо этого Spring Modulith стимулирует использование событий приложения в качестве основного метода взаимодействия модулей. Публикация и обработка доменных событий позволяет одному модулю оповестить других о наступившем событии, не зная ничего о тех, кто будет реагировать. Это ключевой момент для достижения низкой связности и для организации модульных тестов (слушатели событий можно подменять или изолировать в тестах). Рассмотрим подробнее событийный подход.

Взаимодействие модулей через события

Событийная связь – краеугольный камень Spring Modulith. Идея в том, что вместо прямого вызова чужого компонента модуль публикует событие (Java-класс события), а другой модуль подписывается на него и реагирует асинхронно. Таким образом отправитель не зависит напрямую от получателя, и модули могут развиваться независимо.

Например, в традиционном монолите метод модуля Транзакции мог бы напрямую вызвать метод модуля Счета, чтобы обновить баланс счета после проведения платежа. Это создает жесткую зависимость: модуль транзакций знает о классе и API модуля счетов, и без него не сможет работать. В модульном монолите мы делаем иначе: модуль Транзакции публикует событие TransactionCompletedEvent с деталями проведенной транзакции, а модуль Счета подписывается на это событие и в своем обработчике обновляет баланс счета. Теперь модуль транзакций не знает ничего о реализации модуля счетов – ни классов, ни методов. Добавится новый модуль, заинтересованный в событии (например, модуль Уведомления, рассылающий уведомление о платеже) – ему достаточно начать слушать то же событие, не трогая код отправителя.

Реализуется это средствами Spring: любой компонент может воспользоваться ApplicationEventPublisher чтобы опубликовать событие, а слушатели обозначаются аннотацией @EventListener или специальной аннотацией от Modulith @ApplicationModuleListener. Последняя расширяет функциональность слушателя: например, позволяет выполнять обработчик асинхронно (@Async) и в отдельной транзакции. Spring Modulith рекомендует использовать именно асинхронные, транзакционно-изолированные слушатели, чтобы один модуль не задерживал и не влиял на выполнение другого. Тем не менее, по умолчанию события в Spring – синхронные (публикация блокирует поток до завершения всех слушателей). Modulith предлагает механизм повышения надежности событий, сохраняя их в специальном журнале Event Publication.

Когда модуль публикует событие, Modulith может автоматически зафиксировать его в базе данных (например, в таблице EVENT_PUBLICATION). Это работает аналогично шаблону Transaction Outbox: если транзакция основного действия откатывается, запись о событии тоже откатится. Зато если транзакция успешна, но обработчик события упал или приложение перезагрузилось, запись в журнале позволит повторно доставить событие, как только слушатель восстановится. Spring Modulith предоставляет готовые реализации EventPublicationRepository для популярных СУБД (тот же Postgres), а также встроенные стартеры, чтобы легко подключить поддержку транзакционного лога событий. С точки зрения разработчика это прозрачно: вы публикуете событие как обычно, помечаете свой listener как транзакционный (@TransactionalEventListener), а Modulith позаботится, чтобы событие не потерялось даже при сбоях.

Пример кода (публикация события и обработчик):

// Модуль Transactions
@Service
public class TransactionService {
    private final ApplicationEventPublisher publisher;
    // конструктор...

    public Transaction processPayment(Transaction tx) {
        // логика сохранения транзакции в базе PostgreSQL
        txRepository.save(tx);
        // публикация доменного события об успешной транзакции
        publisher.publishEvent(new TransactionCompletedEvent(tx.getId(), tx.getAmount()));
        return tx;
    }
}

// Событие о завершении транзакции
public class TransactionCompletedEvent {
    private final UUID transactionId;
    private final BigDecimal amount;
    // getters ...
}

// Модуль Accounts
@Service
public class AccountService {
    @TransactionalEventListener
    public void onTransactionCompleted(TransactionCompletedEvent event) {
        // найти счет по транзакции и обновить баланс
        accountRepository.updateBalance(event.getTransactionId(), event.getAmount());
    }
}

В этом примере TransactionService ничего не знает о AccountService – он просто публикует событие. AccountService получает его благодаря механизму Spring Events и выполняет свою логику. Заметим, метод помечен @TransactionalEventListener без атрибута fallbackExecution. Это значит, что обработчик сработает только если транзакция публикующего метода успешно закоммитилась, и сам выполняется в новой транзакции. Таким образом, модуль Accounts видит только завершенные транзакции. При падении обработчика Modulith занесет информацию в журнал и позволит повторить обработку позже – основной поток не пострадает.

Последовательность взаимодействия модулей через событие на рисунке ниже иллюстрирует этот процесс:

Рис. 2: Последовательность обработки финансовой транзакции в модульном монолите.

Здесь после запроса пользователя модуль Transactions создает запись в своей базе (PostgreSQL) и генерирует событие о платеже. Spring Modulith обеспечивает доставку события в модуль Accounts (который обновляет баланс в той же транзакции БД) и модуль Notifications (который может опубликовать сообщение в Kafka для внешней системы или отправить email). Обратите внимание: модули отделены друг от друга – они общаются асинхронно через события, но все происходит внутри одного процесса. Нет сетевых вызовов между сервисами, нет проблем с согласованностью – баланс счета и запись транзакции можно обновить атомарно в рамках одной базы данных. В то же время достигается слабая связанность – новые модули могут подписываться на события или убираются без влияния на остальные. При возросшей нагрузке проблемный модуль (например, Notifications) можно вынести в отдельное приложение, слушающее Kafka – благодаря изначально событийной интеграции миграция пройдет гладко.

Миграция от монолита к модульному монолиту (и дальше)

Допустим, у вас есть унаследованный монолит в области финансов. Он уже использует Spring Boot, базы данных PostgreSQL и MongoDB, интегрируется с Kafka, предоставляет REST API – словом, типичный современный стек. Однако вся логика смешана в одном приложении без четких границ. Spring Modulith позволяет провести рефакторинг монолита в модульную архитектуру поэтапно, минимизируя риски:

  1. Выделение модулей по доменным областям. Вместе с командой проанализируйте бизнес-области приложения. В финансовом ПО это могут быть: Платежи, Счета, Клиенты, Кредиты, Отчетность и т.д. Каждая область станет кандидатом в модуль. В коде переносим соответствующие классы (сервисы, контроллеры, репозитории) в свой пакет. Например, все классы, связанные со счетами – в com.myapp.accounts, с транзакциями – в com.myapp.transactions и т.д. Структурируйте пакеты модулей и их под-пакеты (например, transactions.internal для сугубо внутренних компонентов). На этом этапе можно внедрить DDD-подход: определить границы контекста (bounded contexts) и убедиться, что каждый модуль отвечает за свой кусочек предметной области. Организуя код вокруг бизнес-доменов, вы уже делаете систему понятнее и устойчивее к изменениям требований.
  2. Разрыв прямых зависимостей между модулями. Просмотрите, как модули общаются в старом монолите. Скорее всего, класс из одного пакета напрямую вызывает класс из другого. Решаем, где это уместно заменить взаимодействие на событийное. Например, модуль Отчетность вместо прямого запроса данных из модуля Транзакции может слушать события о проведенных транзакциях и обновлять аналитическую витрину (скажем, в MongoDB) асинхронно. Modulith не требует, чтобы все связи были через события – допускаются и прямые вызовы, но их стоит явно разрешить через @ApplicationModule(allowedDependencies) и держать минимально необходимыми. Идеология такая: бизнес-правила реагирования на событие лучше изолировать в модуле-слушателе, чем связывать два модуля синхронным вызовом. В итоге вы придете к архитектуре “издателей и подписчиков”: один модуль публикует событие, любое количество других могут его получать. Сильно связанных двойных связей не остается, что улучшает модульность.
  3. Внедрение Spring Modulith и настройка ограничений. Добавьте в проект необходимые зависимости Spring Modulith (достаточно подключить BOM и нужные артефакты модулей – ядро, тесты, наблюдение и пр. по необходимости). Пометьте в package-info.java каждого модуля аннотацию @ApplicationModule – можно указать человекочитаемое название модуля и список разрешенных зависимостей. Например, модулю Notifications может быть разрешено зависеть от Accounts (если, скажем, уведомление хочет обогатить данные информацией о счете). Если зависимость не указана, Modulith по умолчанию будет считать, что модуль независим. Настройте Event Publication если хотите надежной доставки событий: достаточно добавить стартер для вашей СУБД (например, spring-modulith-starter-jpa) – и система автоматически заведет таблицу для журнала событий. Теперь, когда у вас есть явная декларация модулей, можно воспользоваться ApplicationModules.verify() – запустить тест, который проверит, что никаких нелегальных проникновений межмодульных больше нет. На этом шаге вы, вероятно, устраните последние проблемные места, подсвеченные отчетом ArchUnit (например, кто-то забыл перестать использовать старый сервис напрямую). После успешной проверки архитектуру можно считать логически разделенной. Монолит превращается в набор модулей, склеенных Spring-контейнером.
  4. Проверка работы и регрессии. Запустите приложение и прогони тесты. Особое внимание – бизнес-функциям, которые были затронуты при разрыве связей. Возможно, где-то придется тонко настроить порядок публикации/прослушивания событий или убедиться, что при старте все бины поднимаются (Spring Modulith автоматически регистрирует слушателей, здесь проблем обычно нет). Зато появится приятный бонус: можно написать отдельные интеграционные тесты на каждый модуль, убедившись, что в изоляции они работают корректно. Например, протестировать модуль Transactions на выполнение платежа, замокав (через @MockBean) внешние компоненты вроде Kafka-продюсера, и проверив через PublishedEvents, что событие действительно публикуется. А тест модуля Accounts может эмулировать получение события TransactionCompletedEvent и проверить обновление баланса. Благодаря Modulith, такие тесты стали возможны без подъема всего приложения и вмешательства деталей других частей.
  5. Документирование новой архитектуры. Настало время зафиксировать новую структуру системы. Запустите генерацию документации Modulith Documenter’ом. Он выпустит диаграмму модулей (например, C4 container diagram всего приложения) и сгенерирует canvas-описание каждого модуля. Эти артефакты можно опубликовать во внутреннем wiki или включить в README проекта – они автоматически отражают актуальное состояние модулей. Например, на диаграмме вы четко увидите, что модуль Transactions связан стрелкой (зависимостью) только с модулем Accounts и Notifications (через события), а модуль Clients вообще ни от кого не зависит. Это подтверждает правильность декомпозиции и служит ориентиром для новых разработчиков. Теперь ваша система выглядит как набор микросервисов, хотя физически остается монолитом.

Когда Spring Modulith – лучший выбор?

Как понять, что архитектура модульного монолита вам подходит больше, чем полный переход на микросервисы? Основные ситуации следующие:

  • Небольшие и средние по масштабу приложения, которые можно целиком масштабировать как единое целое. Если нагрузку можно выдержать, запуская несколько копий монолита, избыточное дробление на микросервисы не приносит пользы, а Modulith даст нужную модульность.
  • Проекты с ограниченными ресурсами или маленькие команды. Когда нет выделенной команды DevOps и SRE, и каждый сервис добавляет нагрузку на сопровождение, модульный монолит позволяет сосредоточиться на функциональности, а не на инфраструктуре.
  • Высокая требовательность к консистентности данных. В финансовом секторе часто критична ACID-консистентность (например, транзакция и связанная проводка по счету должны сохраниться вместе или откатиться вместе). В монолите это легко обеспечить одной транзакцией в БД. В микросервисах пришлось бы усложнять жизнь распределенными транзакциями или компенсациями. Modulith сохраняет сильную консистентность, избегая проблем eventual consistency.
  • Эволюционный рефакторинг старого монолита. Если вы планируете в будущем миграцию на микросервисы, Spring Modulith может быть промежуточным шагом. Сначала наводим порядок в монолите, вычленяем модули, отлаживаем взаимодействие через события внутри одного процесса. А когда будет нужно – выносите модуль в отдельное приложение. Он уже отделен архитектурно: достаточно заменить вызов локального метода на отправку сообщения через, скажем, Kafka или REST-вызов. По сути, modulith закладывает правильные границы контекстов, благодаря чему переход к микросервисам станет технически тривиальным. Многие эксперты рекомендуют такой подход: “сначала модульный монолит – потом микросервисы при необходимости”.

Разумеется, есть случаи, когда микросервисная архитектура оправдана с самого начала – например, очень большие проекты с разными по логике доменами, требующие независимого масштабирования, или распределенная по разным командам разработка. Однако даже в таких случаях опыт проектирования модульного монолита бесценен. Если команда не сумела выделить грамотные модули внутри одного кода, то и нарезка на микросервисы, скорее всего, окажется неудачной (получатся сервисы-“кривые” вместо сервисов-“красивых”). Spring Modulith учит проектировать правильные границы и интерфейсы между частями системы. Применяя его, вы не только улучшаете текущий монолит, но и растите архитектурную культуру команды.

Заключение

Spring Modulith заполняет важный пробел между традиционным монолитом и микросервисами. Он предоставляет инструменты и правила для наведения порядка внутри приложения: разбиения на доменные модули, контроля зависимостей, асинхронного взаимодействия через события, автономного тестирования модулей и документирования архитектуры. Наше мысленное путешествие показало, как из запутанного монолита может получиться аккуратная модульная система – особенно актуально для финансовых приложений, где целостность данных и надежность стоят на первом месте. С модульным монолитом команда получает более понятный, сопровождаемый код, быстрее доставляет новые фичи (не опасаясь затронуть чужие модули), и при этом избегает многих сложностей, связанных с распределенными системами.

Стоит подчеркнуть: Spring Modulith не закрывает двери к микросервисам, а напротив – держит их открытыми. Когда бизнес вырастет и понадобится масштабировать отдельные части независимо, вы без боли выделите нужный модуль во внешний сервис, ведь он с самого начала разрабатывался обособленно. Таким образом, модульный монолит – это не шаг назад, а осознанный выбор архитектуры, оптимальной для текущих условий, с прицелом на будущее.

В мире разработки нет универсальных рецептов, но принцип “сначала декомпозируй, потом распределяй” оправдывает себя все чаще. Spring Modulith вооружает нас практическими средствами следовать этому принципу. Бережно отделяя части монолита друг от друга, мы наводим порядок здесь и сейчас – и одновременно прокладываем дорогу в будущее, где при необходимости модуль превратится в полноценный сервис. Такой путь эволюции архитектуры часто оказывается более гладким и быстрым, чем прыжок в микросервисы с нуля.

Spring Modulith предоставляет разработчикам замечательную возможность углубить навыки проектирования: вы учитесь видеть систему как набор взаимодействующих контекстов, держать баланс между изоляцией и интеграцией. Попробуйте применить Spring Modulith в своем проекте – возможно, это окажется тем самым “золотым сечением” архитектуры, где достигается гармония между монолитной простотой и микросервисной гибкостью

Loading