# Spring Modulith: Архітектура модульного моноліта > Опануйте Spring Modulith для побудови модульних монолітів на Java. Архітектура, модулі, асинхронні події та тестування зі Spring Boot 3. - Published: 2026-03-23 - Updated: 2026-05-03 - Author: SharpSkill - Tags: spring modulith, modular architecture, spring boot, java, modular monolith - Reading time: 18 min --- Spring Modulith пропонує прагматичний підхід до структурування Spring Boot застосунків у згуртовані бізнес-модулі. Ця архітектура розташовує модульний моноліт між традиційним монолітом і мікросервісами, забезпечуючи сильну модульність без операційної складності розподілених систем. > **Ключове розуміння** > > Spring Modulith формалізує найкращі практики гексагональної архітектури та Domain-Driven Design безпосередньо у Spring Boot, з автоматичною перевіркою залежностей між модулями. ## Чому обирати модульний моноліт? ### Проблема класичного моноліта Традиційні моноліти страждають від надмірної зв'язаності між компонентами. З часом перехресні залежності накопичуються і перетворюють застосунок на некерований "big ball of mud". Зміна в модулі білінгу впливає на модуль користувачів, а потім на модуль сповіщень, породжуючи непередбачувані побічні ефекти. ```java // AntiPattern.java // Direct coupling between modules - AVOID THIS @Service public class OrderService { // Direct dependencies to other modules // Creates tight coupling and dependency cycles private final UserRepository userRepository; private final InventoryService inventoryService; private final PaymentProcessor paymentProcessor; private final NotificationService notificationService; private final ShippingCalculator shippingCalculator; public OrderService(UserRepository userRepository, InventoryService inventoryService, PaymentProcessor paymentProcessor, NotificationService notificationService, ShippingCalculator shippingCalculator) { this.userRepository = userRepository; this.inventoryService = inventoryService; this.paymentProcessor = paymentProcessor; this.notificationService = notificationService; this.shippingCalculator = shippingCalculator; } public Order createOrder(OrderRequest request) { // This service knows too many implementation details User user = userRepository.findById(request.userId()).orElseThrow(); inventoryService.reserveItems(request.items()); BigDecimal shipping = shippingCalculator.calculate(user.getAddress()); paymentProcessor.charge(user, request.total().add(shipping)); notificationService.sendOrderConfirmation(user, request); // ... return null; } } ``` Цей патерн породжує конкретні проблеми: крихкі інтеграційні тести, складність оцінити вплив зміни та неможливість незалежного розгортання чи розвитку модуля. ### Мікросервіси не завжди є відповіддю Мікросервіси розв'язують проблему зв'язаності, але вносять значну операційну складність: мережеву комунікацію, eventual consistency, розподілене розгортання, мультисервісну спостережуваність. Для багатьох команд ця складність не виправдовується отриманими перевагами. Модульний моноліт пропонує альтернативу: одну одиницю розгортання з чітко визначеними та забезпеченими межами модулів. Spring Modulith автоматизує перевірку цих меж. ## Перші кроки зі Spring Modulith ### Конфігурація проєкту Інтеграція Spring Modulith у проєкт Spring Boot 3 потребує кількох Maven залежностей. Основний стартер вмикає автоматичне виявлення модулів. ```xml org.springframework.modulith spring-modulith-starter-core org.springframework.modulith spring-modulith-starter-jpa org.springframework.modulith spring-modulith-starter-test test org.springframework.modulith spring-modulith-docs test org.springframework.modulith spring-modulith-bom 1.3.0 pom import ``` ### Структура модулів Spring Modulith автоматично виявляє модулі на основі прямих пакетів під головним пакетом застосунку. Кожен підпакет представляє окремий модуль зі своїми обов'язками. ``` com.example.shop/ ├── ShopApplication.java # Spring Boot entry point ├── order/ # Order Module │ ├── Order.java # Public entity (module API) │ ├── OrderService.java # Public service │ ├── internal/ # Module-internal package │ │ ├── OrderRepository.java │ │ └── OrderValidator.java │ └── OrderCreatedEvent.java # Published event ├── inventory/ # Inventory Module │ ├── InventoryService.java │ ├── Product.java │ └── internal/ │ └── StockRepository.java ├── customer/ # Customer Module │ ├── Customer.java │ ├── CustomerService.java │ └── internal/ │ └── CustomerRepository.java └── notification/ # Notification Module ├── NotificationService.java └── internal/ └── EmailSender.java ``` Ця конвенція встановлює фундаментальне правило: лише класи в кореневому пакеті модуля (а не в `internal/`) утворюють публічний API, доступний іншим модулям. ```java // Order.java // Public entity of Order module - accessible from other modules package com.example.shop.order; import jakarta.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.UUID; @Entity @Table(name = "orders") public class Order { @Id private UUID id; // Reference by ID rather than entity // Avoids direct coupling with Customer module private UUID customerId; private BigDecimal totalAmount; @Enumerated(EnumType.STRING) private OrderStatus status; private LocalDateTime createdAt; protected Order() { // JPA constructor } public Order(UUID customerId, BigDecimal totalAmount) { this.id = UUID.randomUUID(); this.customerId = customerId; this.totalAmount = totalAmount; this.status = OrderStatus.PENDING; this.createdAt = LocalDateTime.now(); } // Public getters - part of module API public UUID getId() { return id; } public UUID getCustomerId() { return customerId; } public BigDecimal getTotalAmount() { return totalAmount; } public OrderStatus getStatus() { return status; } public LocalDateTime getCreatedAt() { return createdAt; } // Encapsulated business methods void confirm() { if (this.status != OrderStatus.PENDING) { throw new IllegalStateException("Only pending orders can be confirmed"); } this.status = OrderStatus.CONFIRMED; } } ``` ```java // OrderRepository.java // Internal repository - NOT accessible from other modules package com.example.shop.order.internal; import com.example.shop.order.Order; import org.springframework.data.jpa.repository.JpaRepository; import java.util.UUID; // This repository is in the internal package // Spring Modulith prohibits access from other modules interface OrderRepository extends JpaRepository { // Methods specific to the Order module List findByCustomerIdAndStatus(UUID customerId, OrderStatus status); } ``` Репозиторій лишається внутрішнім, оскільки доступ до даних має проходити через публічний сервіс, що гарантує інкапсуляцію бізнес-логіки. > **Конвенція іменування** > > Пакет `internal` не має нічого магічного для Java. Це конвенція, яку Spring Modulith розпізнає та автоматично перевіряє під час тестів. Будь-яке порушення породжує явну помилку. ## Комунікація між модулями ### Доменні події Комунікація між модулями відбувається через доменні події, а не прямі виклики. Цей патерн розчіплює модулі-видавці від модулів-отримувачів. ```java // OrderCreatedEvent.java // Event published by Order module package com.example.shop.order; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.UUID; // Immutable record representing the event // Contains only information needed by consumers public record OrderCreatedEvent( UUID orderId, UUID customerId, BigDecimal totalAmount, LocalDateTime createdAt ) { // Factory method to create event from entity public static OrderCreatedEvent from(Order order) { return new OrderCreatedEvent( order.getId(), order.getCustomerId(), order.getTotalAmount(), order.getCreatedAt() ); } } ``` ```java // OrderService.java // Public service that publishes events package com.example.shop.order; import com.example.shop.order.internal.OrderRepository; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.UUID; @Service @Transactional public class OrderService { private final OrderRepository orderRepository; private final ApplicationEventPublisher eventPublisher; public OrderService(OrderRepository orderRepository, ApplicationEventPublisher eventPublisher) { this.orderRepository = orderRepository; this.eventPublisher = eventPublisher; } public Order createOrder(UUID customerId, BigDecimal amount) { // Create the order Order order = new Order(customerId, amount); order = orderRepository.save(order); // Publish the event // Interested modules will react asynchronously eventPublisher.publishEvent(OrderCreatedEvent.from(order)); return order; } public Order confirmOrder(UUID orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); order.confirm(); // Confirmation event eventPublisher.publishEvent(new OrderConfirmedEvent( order.getId(), order.getCustomerId() )); return order; } } ``` Інші модулі споживають ці події, не знаючи деталей реалізації модуля Order. ```java // NotificationEventListener.java // Notification module consuming Order events package com.example.shop.notification.internal; import com.example.shop.order.OrderCreatedEvent; import com.example.shop.order.OrderConfirmedEvent; import org.springframework.modulith.events.ApplicationModuleListener; import org.springframework.stereotype.Component; @Component class NotificationEventListener { private final EmailSender emailSender; private final CustomerLookup customerLookup; NotificationEventListener(EmailSender emailSender, CustomerLookup customerLookup) { this.emailSender = emailSender; this.customerLookup = customerLookup; } // @ApplicationModuleListener guarantees async processing // and event persistence for retry on failure @ApplicationModuleListener void onOrderCreated(OrderCreatedEvent event) { // Retrieve email via local interface // Avoids direct dependency on Customer module String email = customerLookup.getEmailByCustomerId(event.customerId()); emailSender.send( email, "Order Received", "Your order #%s has been received.".formatted(event.orderId()) ); } @ApplicationModuleListener void onOrderConfirmed(OrderConfirmedEvent event) { String email = customerLookup.getEmailByCustomerId(event.customerId()); emailSender.send( email, "Order Confirmed", "Your order #%s is confirmed and being prepared." .formatted(event.orderId()) ); } } ``` ### Персистовані та асинхронні події Spring Modulith пропонує потужну можливість: персистенцію подій. Події зберігаються у базі даних до публікації, що гарантує їх обробку навіть у разі краху застосунку. ```java // EventPublicationConfig.java // Persisted events configuration package com.example.shop.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.modulith.events.config.EnablePersistentDomainEvents; import org.springframework.scheduling.annotation.EnableAsync; @Configuration @EnableAsync @EnablePersistentDomainEvents // Enables event persistence public class EventPublicationConfig { // Spring Modulith automatically creates required tables // EVENT_PUBLICATION stores pending events // Processed events are marked as completed } ``` ```java // InventoryEventListener.java // Listener with transactional event handling package com.example.shop.inventory.internal; import com.example.shop.order.OrderCreatedEvent; import com.example.shop.order.OrderCancelledEvent; import org.springframework.modulith.events.ApplicationModuleListener; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component class InventoryEventListener { private final StockRepository stockRepository; InventoryEventListener(StockRepository stockRepository) { this.stockRepository = stockRepository; } // Transactional processing - if exception thrown, // event will be retried automatically @ApplicationModuleListener @Transactional void onOrderCreated(OrderCreatedEvent event) { // Reserve stock for this order // On failure, event remains in EVENT_PUBLICATION // and will be reprocessed in next cycle reserveStockForOrder(event.orderId(), event.items()); } @ApplicationModuleListener @Transactional void onOrderCancelled(OrderCancelledEvent event) { // Release reserved stock releaseStockForOrder(event.orderId()); } private void reserveStockForOrder(UUID orderId, List items) { for (OrderItem item : items) { Stock stock = stockRepository.findByProductId(item.productId()) .orElseThrow(() -> new StockNotFoundException(item.productId())); stock.reserve(item.quantity()); stockRepository.save(stock); } } private void releaseStockForOrder(UUID orderId) { // Stock release implementation } } ``` Таблиця `EVENT_PUBLICATION`, що автоматично створюється Spring Modulith: ```sql -- EVENT_PUBLICATION table structure (PostgreSQL) CREATE TABLE event_publication ( id UUID PRIMARY KEY, listener_id VARCHAR(512) NOT NULL, event_type VARCHAR(512) NOT NULL, serialized_event TEXT NOT NULL, publication_date TIMESTAMP NOT NULL, completion_date TIMESTAMP ); -- Index for retry queries CREATE INDEX idx_event_publication_incomplete ON event_publication (completion_date) WHERE completion_date IS NULL; ``` ### Інтерфейси, відкриті між модулями Коли модулю потрібна інформація з іншого без використання події, публічний інтерфейс у вихідному модулі дозволяє зберегти мінімальну зв'язаність. ```java // CustomerLookup.java // Public interface of Customer module package com.example.shop.customer; import java.util.Optional; import java.util.UUID; // Interface exposed to other modules // Defines contract without exposing implementation details public interface CustomerLookup { Optional findEmailById(UUID customerId); boolean exists(UUID customerId); // Specific DTO for shared information Optional findInfoById(UUID customerId); record CustomerInfo( UUID id, String email, String fullName, String preferredLanguage ) {} } ``` ```java // CustomerLookupImpl.java // Internal implementation package com.example.shop.customer.internal; import com.example.shop.customer.CustomerLookup; import org.springframework.stereotype.Component; import java.util.Optional; import java.util.UUID; @Component class CustomerLookupImpl implements CustomerLookup { private final CustomerRepository customerRepository; CustomerLookupImpl(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } @Override public Optional findEmailById(UUID customerId) { return customerRepository.findById(customerId) .map(Customer::getEmail); } @Override public boolean exists(UUID customerId) { return customerRepository.existsById(customerId); } @Override public Optional findInfoById(UUID customerId) { return customerRepository.findById(customerId) .map(customer -> new CustomerInfo( customer.getId(), customer.getEmail(), customer.getFullName(), customer.getPreferredLanguage() )); } } ``` Цей підхід дає змогу модулю Notification отримувати інформацію про клієнта без прямої залежності від репозиторію чи сутності Customer. ## Тести модульної структури ### Автоматична перевірка залежностей Spring Modulith надає інструменти тестування для перевірки дотримання архітектурних правил. Ці тести зазнають невдачі, коли модуль звертається до внутрішніх класів іншого. ```java // ModularityTests.java // Modular architecture verification tests package com.example.shop; import org.junit.jupiter.api.Test; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.docs.Documenter; class ModularityTests { // Load application module structure private final ApplicationModules modules = ApplicationModules.of(ShopApplication.class); @Test void verifyModularStructure() { // Verify all modules are correctly structured // Fails if a module accesses internal packages of another modules.verify(); } @Test void printModuleOverview() { // Print module structure to console // Useful for understanding dependencies modules.forEach(System.out::println); } @Test void createModuleDocumentation() { // Generate automatic module documentation // Includes dependency diagrams new Documenter(modules) .writeModulesAsPlantUml() .writeIndividualModulesAsPlantUml(); } @Test void detectCyclicDependencies() { // The verify() method also detects cycles // Module A → Module B → Module C → Module A = failure modules.verify(); } } ``` Запуск `modules.verify()` аналізує bytecode і виявляє: - Доступи до пакетів `internal` з інших модулів - Циклічні залежності між модулями - Порушення правил інкапсуляції ### Інтеграційні тести по модулях Spring Modulith дозволяє тестувати кожен модуль ізольовано, завантажуючи лише необхідні bean'и. ```java // OrderModuleIntegrationTests.java // Order module integration test in isolation package com.example.shop.order; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.modulith.test.ApplicationModuleTest; import org.springframework.modulith.test.Scenario; import java.math.BigDecimal; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @ApplicationModuleTest // Load only Order module and its dependencies class OrderModuleIntegrationTests { @Autowired private OrderService orderService; @Test void shouldCreateOrder() { // Given UUID customerId = UUID.randomUUID(); BigDecimal amount = new BigDecimal("99.99"); // When Order order = orderService.createOrder(customerId, amount); // Then assertThat(order.getId()).isNotNull(); assertThat(order.getCustomerId()).isEqualTo(customerId); assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING); } @Test void shouldPublishEventOnOrderCreation(Scenario scenario) { // Given UUID customerId = UUID.randomUUID(); // When / Then - verify event is published scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN)) .andWaitForEventOfType(OrderCreatedEvent.class) .matching(event -> event.customerId().equals(customerId)) .toArriveAndVerify(event -> { assertThat(event.orderId()).isNotNull(); assertThat(event.totalAmount()).isEqualTo(BigDecimal.TEN); }); } @Test void shouldHandleOrderConfirmation(Scenario scenario) { // Given - create an order Order order = orderService.createOrder(UUID.randomUUID(), BigDecimal.TEN); // When / Then - confirm and verify event scenario.stimulate(() -> orderService.confirmOrder(order.getId())) .andWaitForEventOfType(OrderConfirmedEvent.class) .toArriveAndVerify(event -> { assertThat(event.orderId()).isEqualTo(order.getId()); }); } } ``` Анотація `@ApplicationModuleTest` автоматично налаштовує: - Завантаження виключно bean'ів модуля Order - Mock'и для залежностей до інших модулів - Інфраструктуру тестування подій ```java // OrderNotificationIntegrationTest.java // Inter-module integration test package com.example.shop; import com.example.shop.order.OrderService; import com.example.shop.order.OrderCreatedEvent; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.modulith.test.ApplicationModuleTest; import org.springframework.modulith.test.Scenario; import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode; import java.math.BigDecimal; import java.util.UUID; // DIRECT loads all directly dependent modules @ApplicationModuleTest(BootstrapMode.DIRECT) class OrderNotificationIntegrationTest { @Autowired private OrderService orderService; @Test void shouldTriggerNotificationOnOrderCreated(Scenario scenario) { // This test verifies Order → Notification integration UUID customerId = UUID.randomUUID(); scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN)) .andWaitForEventOfType(OrderCreatedEvent.class) .toArriveAndVerify(event -> { // Event was processed by NotificationEventListener // Test verifies email was sent }); } } ``` > **Ізоляція тестів** > > Використовуйте `BootstrapMode.STANDALONE` (за замовчуванням) для модульних unit-тестів. `BootstrapMode.ALL_DEPENDENCIES` залишайте для наскрізних інтеграційних тестів, щоб уникати прихованих залежностей. ## Розширена конфігурація модулів ### Явні модулі за допомогою @ApplicationModule Для складних випадків анотація `@ApplicationModule` дозволяє явно налаштувати правила модуля. ```java // package-info.java // Explicit Order module configuration @org.springframework.modulith.ApplicationModule( // Modules allowed to depend on this one allowedDependencies = {"customer", "inventory"}, // Module type: OPEN (free access) or CLOSED (explicit API) type = Type.CLOSED ) package com.example.shop.order; import org.springframework.modulith.ApplicationModule.Type; ``` ```java // NamedInterface.java // Named interface definition for finer API control package com.example.shop.order; import org.springframework.modulith.NamedInterface; // Exposes only certain classes as public API @NamedInterface("order-api") public class OrderApi { // Classes in this package are accessible via "order-api" } ``` ```java // package-info.java // Module depending on a specific named interface @org.springframework.modulith.ApplicationModule( allowedDependencies = "order::order-api" // Access limited to named API ) package com.example.shop.shipping; ``` ### Обробка циклічних залежностей Циклічні залежності між модулями зазвичай свідчать про проблему дизайну. Spring Modulith їх виявляє і змушує верифікацію зазнати невдачі. Розв'язанням, як правило, є виокремлення нового модуля або використання подій. ```java // BEFORE - Circular dependency // Order → Inventory (to check stock) // Inventory → Order (to know current orders) // AFTER - Resolution through events // Order publishes OrderCreatedEvent // Inventory listens and reserves stock // Inventory publishes StockReservedEvent // Order listens and confirms availability ``` ```java // StockReservedEvent.java // Event published by Inventory package com.example.shop.inventory; import java.util.UUID; public record StockReservedEvent( UUID orderId, UUID productId, int quantity, boolean success, String failureReason ) {} ``` ```java // OrderStockListener.java // Order module listens to Inventory events package com.example.shop.order.internal; import com.example.shop.inventory.StockReservedEvent; import org.springframework.modulith.events.ApplicationModuleListener; import org.springframework.stereotype.Component; @Component class OrderStockListener { private final OrderRepository orderRepository; OrderStockListener(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @ApplicationModuleListener void onStockReserved(StockReservedEvent event) { Order order = orderRepository.findById(event.orderId()) .orElseThrow(); if (event.success()) { order.markStockReserved(); } else { order.markStockUnavailable(event.failureReason()); } orderRepository.save(order); } } ``` ## Спостережуваність та моніторинг ### Трасування подій Spring Modulith інтегрується з Micrometer для розподіленого трасування подій між модулями. ```java // ObservabilityConfig.java // Module observability configuration package com.example.shop.config; import io.micrometer.observation.ObservationRegistry; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.modulith.observability.ModuleEventListener; @Configuration public class ObservabilityConfig { @Bean ModuleEventListener moduleEventListener(ObservationRegistry registry) { // Adds spans for each processed event return new ModuleEventListener(registry); } } ``` ```yaml # application.yml # Observability configuration management: tracing: sampling: probability: 1.0 # Trace all events in dev endpoints: web: exposure: include: health,info,metrics,modulith spring: modulith: events: # Retry interval for failed events republish-outstanding-events-on-restart: true # Retention duration for completed events completion-mode: DELETE # or ARCHIVE ``` ### Actuator endpoint модулів Spring Modulith надає Actuator endpoint для перегляду стану модулів у продакшені. ```java // ModulithActuatorConfig.java // Actuator endpoint activation package com.example.shop.config; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.modulith.actuator.ApplicationModulesEndpoint; import org.springframework.modulith.core.ApplicationModules; @Configuration public class ModulithActuatorConfig { @Bean @ConditionalOnAvailableEndpoint ApplicationModulesEndpoint modulesEndpoint(ApplicationModules modules) { return new ApplicationModulesEndpoint(modules); } } ``` Endpoint `/actuator/modulith` повертає: ```json { "modules": [ { "name": "order", "basePackage": "com.example.shop.order", "dependencies": ["customer"], "publishedEvents": [ "com.example.shop.order.OrderCreatedEvent", "com.example.shop.order.OrderConfirmedEvent" ], "listenedEvents": [ "com.example.shop.inventory.StockReservedEvent" ] }, { "name": "inventory", "basePackage": "com.example.shop.inventory", "dependencies": [], "publishedEvents": [ "com.example.shop.inventory.StockReservedEvent" ], "listenedEvents": [ "com.example.shop.order.OrderCreatedEvent" ] } ] } ``` ## Міграція до мікросервісів ### Підготовка до виокремлення Модульна архітектура полегшує майбутнє виокремлення в мікросервіси. Кожен модуль стає природним кандидатом на виокремлення. ```java // ExtractionReadinessChecker.java // Extraction readiness verification package com.example.shop; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.core.ApplicationModule; public class ExtractionReadinessChecker { public void checkModule(String moduleName) { ApplicationModules modules = ApplicationModules.of(ShopApplication.class); ApplicationModule module = modules.getModuleByName(moduleName) .orElseThrow(); System.out.println("Module: " + moduleName); System.out.println("Dependencies: " + module.getDependencies()); System.out.println("Published Events: " + module.getPublishedEvents()); System.out.println("Listened Events: " + module.getBootstrapDependencies()); // A module ready for extraction: // - Communicates only through events // - Has no synchronous dependencies to other modules // - Owns its own data tables } } ``` Модулі, що спілкуються виключно через події, можна виокремити в мікросервіси з мінімальними змінами: достатньо замінити локальну шину подій брокером повідомлень (Kafka, RabbitMQ). ## Висновок Spring Modulith надає прагматичне рішення для структурування монолітних Spring Boot застосунків: ✅ **Структура за конвенцією**: пакети = модулі, `internal` = інкапсуляція ✅ **Розчеплена комунікація**: доменні події між модулями ✅ **Автоматична перевірка**: тести структури, що виявляють порушення ✅ **Персистовані події**: гарантія обробки з `@ApplicationModuleListener` ✅ **Ізольовані тести**: `@ApplicationModuleTest` для перевірки кожного модуля ✅ **Згенерована документація**: автоматичні PlantUML діаграми ✅ **Спостережуваність**: інтеграція з Micrometer і Actuator endpoint ✅ **Шлях до мікросервісів**: виокремлення полегшене розчепленням Така архітектура особливо підходить командам, які прагнуть упорядкувати свій моноліт без операційної складності мікросервісів, зберігаючи можливість еволюціонувати до розподіленої архітектури за потреби. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/spring-boot/spring-modulith-modular-monolith-architecture