# Spring Modulith: Architektura Modularnego Monolitu Wyjaśniona
> Naucz się Spring Modulith do budowy modularnych monolitów w Javie. Architektura, moduły, eventy asynchroniczne i testy z przykładami 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 proponuje pragmatyczne podejście do strukturyzowania aplikacji Spring Boot w spójne moduły biznesowe. Architektura ta plasuje modularny monolit pomiędzy tradycyjnym monolitem a mikroserwisami, oferując silną modularność bez złożoności operacyjnej systemów rozproszonych.
> **Kluczowy Wniosek**
>
> Spring Modulith formalizuje dobre praktyki architektury heksagonalnej i Domain-Driven Design bezpośrednio w Spring Boot, z automatyczną weryfikacją zależności między modułami.
## Dlaczego Wybrać Modularny Monolit?
### Problem Klasycznego Monolitu
Tradycyjne monolity cierpią na nadmierne sprzężenie pomiędzy komponentami. Z czasem zależności krzyżowe nawarstwiają się i zmieniają aplikację w niemożliwy do utrzymania "big ball of mud". Zmiana w module fakturowania wpływa na moduł użytkowników, a następnie na moduł powiadomień, generując nieprzewidywalne efekty uboczne.
```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;
}
}
```
Ten wzorzec rodzi konkretne problemy: kruche testy integracyjne, trudności z oceną wpływu zmiany oraz brak możliwości niezależnego wdrożenia czy rozwoju modułu.
### Mikroserwisy Nie Zawsze Są Odpowiedzią
Mikroserwisy rozwiązują problem sprzężenia, ale wprowadzają znaczną złożoność operacyjną: komunikację sieciową, eventual consistency, rozproszone wdrożenia, wieloserwisową obserwowalność. Dla wielu zespołów ta złożoność nie jest uzasadniona uzyskanymi korzyściami.
Modularny monolit oferuje alternatywę: jedną jednostkę wdrożeniową z jasno zdefiniowanymi i wymuszanymi granicami modułów. Spring Modulith automatyzuje weryfikację tych granic.
## Pierwsze Kroki ze Spring Modulith
### Konfiguracja Projektu
Integracja Spring Modulith z projektem Spring Boot 3 wymaga kilku zależności Maven. Główny starter włącza automatyczne wykrywanie modułów.
```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
```
### Struktura Modułów
Spring Modulith wykrywa moduły automatycznie na podstawie pakietów bezpośrednio pod głównym pakietem aplikacji. Każdy podpakiet stanowi osobny moduł z własną odpowiedzialnością.
```
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
```
Ta konwencja ustala fundamentalną regułę: jedynie klasy znajdujące się w pakiecie głównym modułu (a nie w `internal/`) tworzą publiczne API dostępne dla innych modułów.
```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);
}
```
Repozytorium pozostaje wewnętrzne, ponieważ dostęp do danych musi przechodzić przez publiczny serwis, co gwarantuje hermetyzację logiki biznesowej.
> **Konwencja Nazewnictwa**
>
> Pakiet `internal` nie ma dla Javy nic magicznego. To konwencja, którą Spring Modulith rozpoznaje i automatycznie weryfikuje podczas testów. Każde naruszenie generuje jawny błąd.
## Komunikacja Między Modułami
### Eventy Domenowe
Komunikacja między modułami odbywa się poprzez eventy domenowe zamiast bezpośrednich wywołań. Wzorzec ten odsprzęga moduły emitujące od odbierających.
```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;
}
}
```
Pozostałe moduły konsumują te eventy bez znajomości szczegółów implementacyjnych modułu 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())
);
}
}
```
### Eventy Persystowane i Asynchroniczne
Spring Modulith oferuje potężną funkcjonalność: persystencję eventów. Eventy są zapisywane w bazie przed publikacją, co gwarantuje ich przetworzenie nawet w razie awarii aplikacji.
```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
}
}
```
Tabela `EVENT_PUBLICATION` tworzona automatycznie przez 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;
```
### Interfejsy Wystawiane Pomiędzy Modułami
Gdy moduł potrzebuje informacji z innego bez korzystania z eventu, publiczny interfejs w module źródłowym pozwala utrzymać minimalne sprzężenie.
```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()
));
}
}
```
To podejście pozwala modułowi Notification uzyskać informacje o kliencie bez bezpośredniej zależności od repozytorium lub encji Customer.
## Testy Struktury Modularnej
### Automatyczna Weryfikacja Zależności
Spring Modulith dostarcza narzędzia testowe weryfikujące przestrzeganie reguł architektonicznych. Testy te zawodzą, gdy moduł sięga po klasy wewnętrzne innego modułu.
```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();
}
}
```
Wywołanie `modules.verify()` analizuje bytecode i wykrywa:
- Dostępy do pakietów `internal` z innych modułów
- Cykliczne zależności między modułami
- Naruszenia reguł hermetyzacji
### Testy Integracyjne Modułu
Spring Modulith pozwala testować każdy moduł w izolacji, ładując wyłącznie potrzebne beany.
```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());
});
}
}
```
Adnotacja `@ApplicationModuleTest` automatycznie konfiguruje:
- Załadowanie wyłącznie beanów modułu Order
- Mocki dla zależności do innych modułów
- Infrastrukturę testową dla eventów
```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
});
}
}
```
> **Izolacja Testów**
>
> Używaj `BootstrapMode.STANDALONE` (domyślnego) dla testów jednostkowych modułu. `BootstrapMode.ALL_DEPENDENCIES` zachowaj dla testów integracyjnych end-to-end, by uniknąć ukrytych zależności.
## Zaawansowana Konfiguracja Modułów
### Jawne Moduły z @ApplicationModule
W złożonych przypadkach adnotacja `@ApplicationModule` pozwala jawnie skonfigurować reguły modułu.
```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;
```
### Obsługa Zależności Cyklicznych
Cykliczne zależności między modułami zwykle wskazują na problem projektowy. Spring Modulith je wykrywa i powoduje niepowodzenie weryfikacji. Rozwiązaniem jest zwykle wyodrębnienie nowego modułu lub użycie eventów.
```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);
}
}
```
## Obserwowalność i Monitoring
### Tracing Eventów
Spring Modulith integruje się z Micrometer dla rozproszonego śledzenia eventów między modułami.
```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
```
### Endpoint Actuator dla Modułów
Spring Modulith udostępnia endpoint Actuator do podglądu stanu modułów na produkcji.
```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` zwraca:
```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"
]
}
]
}
```
## Migracja do Mikroserwisów
### Przygotowanie do Wyodrębnienia
Architektura modularna ułatwia przyszłe wyodrębnienie do mikroserwisów. Każdy moduł staje się naturalnym kandydatem do wyodrębnienia.
```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
}
}
```
Moduły komunikujące się wyłącznie przez eventy można wyodrębnić jako mikroserwisy minimalnym kosztem: wystarczy zastąpić lokalną szynę eventów brokerem komunikatów (Kafka, RabbitMQ).
## Podsumowanie
Spring Modulith dostarcza pragmatyczne rozwiązanie do strukturyzowania monolitycznych aplikacji Spring Boot:
✅ **Struktura przez konwencję**: pakiety = moduły, `internal` = hermetyzacja
✅ **Komunikacja odsprzęgnięta**: eventy domenowe między modułami
✅ **Automatyczna weryfikacja**: testy struktury wykrywające naruszenia
✅ **Persystowane eventy**: gwarancja przetworzenia z `@ApplicationModuleListener`
✅ **Izolowane testy**: `@ApplicationModuleTest` do walidacji każdego modułu
✅ **Generowana dokumentacja**: automatyczne diagramy PlantUML
✅ **Obserwowalność**: integracja z Micrometer i endpoint Actuator
✅ **Droga do mikroserwisów**: wyodrębnienie ułatwione przez odsprzęganie
Architektura ta szczególnie pasuje zespołom, które chcą uporządkować swój monolit bez przyjmowania złożoności operacyjnej mikroserwisów, zachowując możliwość ewolucji w stronę architektury rozproszonej, gdy zajdzie taka potrzeba.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/pl/blog/spring-boot/spring-modulith-modular-monolith-architecture