Spring Modulith: Архітектура модульного моноліта
Опануйте Spring Modulith для побудови модульних монолітів на Java. Архітектура, модулі, асинхронні події та тестування зі Spring Boot 3.

Spring Modulith пропонує прагматичний підхід до структурування Spring Boot застосунків у згуртовані бізнес-модулі. Ця архітектура розташовує модульний моноліт між традиційним монолітом і мікросервісами, забезпечуючи сильну модульність без операційної складності розподілених систем.
Spring Modulith формалізує найкращі практики гексагональної архітектури та Domain-Driven Design безпосередньо у Spring Boot, з автоматичною перевіркою залежностей між модулями.
Чому обирати модульний моноліт?
Проблема класичного моноліта
Традиційні моноліти страждають від надмірної зв'язаності між компонентами. З часом перехресні залежності накопичуються і перетворюють застосунок на некерований "big ball of mud". Зміна в модулі білінгу впливає на модуль користувачів, а потім на модуль сповіщень, породжуючи непередбачувані побічні ефекти.
// 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 залежностей. Основний стартер вмикає автоматичне виявлення модулів.
<!-- pom.xml -->
<!-- Spring Modulith dependencies for Spring Boot 3.2+ -->
<dependencies>
<!-- Core Spring Modulith -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<!-- Async event support with persistence -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-jpa</artifactId>
</dependency>
<!-- Module structure tests -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Automatic documentation generation -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-docs</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>1.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Структура модулів
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, доступний іншим модулям.
// 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;
}
}// 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<Order, UUID> {
// Methods specific to the Order module
List<Order> findByCustomerIdAndStatus(UUID customerId, OrderStatus status);
}Репозиторій лишається внутрішнім, оскільки доступ до даних має проходити через публічний сервіс, що гарантує інкапсуляцію бізнес-логіки.
Пакет internal не має нічого магічного для Java. Це конвенція, яку Spring Modulith розпізнає та автоматично перевіряє під час тестів. Будь-яке порушення породжує явну помилку.
Комунікація між модулями
Доменні події
Комунікація між модулями відбувається через доменні події, а не прямі виклики. Цей патерн розчіплює модулі-видавці від модулів-отримувачів.
// 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()
);
}
}// 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.
// 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 Boot?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Персистовані та асинхронні події
Spring Modulith пропонує потужну можливість: персистенцію подій. Події зберігаються у базі даних до публікації, що гарантує їх обробку навіть у разі краху застосунку.
// 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
}// 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<OrderItem> 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:
-- 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;Інтерфейси, відкриті між модулями
Коли модулю потрібна інформація з іншого без використання події, публічний інтерфейс у вихідному модулі дозволяє зберегти мінімальну зв'язаність.
// 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<String> findEmailById(UUID customerId);
boolean exists(UUID customerId);
// Specific DTO for shared information
Optional<CustomerInfo> findInfoById(UUID customerId);
record CustomerInfo(
UUID id,
String email,
String fullName,
String preferredLanguage
) {}
}// 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<String> findEmailById(UUID customerId) {
return customerRepository.findById(customerId)
.map(Customer::getEmail);
}
@Override
public boolean exists(UUID customerId) {
return customerRepository.existsById(customerId);
}
@Override
public Optional<CustomerInfo> findInfoById(UUID customerId) {
return customerRepository.findById(customerId)
.map(customer -> new CustomerInfo(
customer.getId(),
customer.getEmail(),
customer.getFullName(),
customer.getPreferredLanguage()
));
}
}Цей підхід дає змогу модулю Notification отримувати інформацію про клієнта без прямої залежності від репозиторію чи сутності Customer.
Тести модульної структури
Автоматична перевірка залежностей
Spring Modulith надає інструменти тестування для перевірки дотримання архітектурних правил. Ці тести зазнають невдачі, коли модуль звертається до внутрішніх класів іншого.
// 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'и.
// 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'и для залежностей до інших модулів
- Інфраструктуру тестування подій
// 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 дозволяє явно налаштувати правила модуля.
// 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;// 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"
}// 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 їх виявляє і змушує верифікацію зазнати невдачі. Розв'язанням, як правило, є виокремлення нового модуля або використання подій.
// 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// 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
) {}// 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 для розподіленого трасування подій між модулями.
// 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);
}
}# 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 ARCHIVEActuator endpoint модулів
Spring Modulith надає Actuator endpoint для перегляду стану модулів у продакшені.
// 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 повертає:
{
"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"
]
}
]
}Міграція до мікросервісів
Підготовка до виокремлення
Модульна архітектура полегшує майбутнє виокремлення в мікросервіси. Кожен модуль стає природним кандидатом на виокремлення.
// 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
✅ Шлях до мікросервісів: виокремлення полегшене розчепленням
Така архітектура особливо підходить командам, які прагнуть упорядкувати свій моноліт без операційної складності мікросервісів, зберігаючи можливість еволюціонувати до розподіленої архітектури за потреби.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Співбесіда Spring Batch 5: Партиціювання, Чанки та Відмовостійкість
Опануйте співбесіди Spring Batch 5: 15 ключових питань про партиціювання, обробку чанків і відмовостійкість з прикладами на Java 21.

Співбесіда Spring Boot: Поширення Транзакцій
Опануйте поширення транзакцій у Spring Boot: REQUIRED, REQUIRES_NEW, NESTED тощо. 12 питань зі співбесід з кодом та поширеними пастками.

Spring Security 6: Повна Автентифікація JWT
Практичний посібник із впровадження автентифікації JWT у Spring Security 6: конфігурація, генерація токенів, валідація та найкращі практики безпеки.