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.

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.
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.
// 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.
<!-- 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>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.javaTa 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.
// 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);
}Repozytorium pozostaje wewnętrzne, ponieważ dostęp do danych musi przechodzić przez publiczny serwis, co gwarantuje hermetyzację logiki biznesowej.
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.
// 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;
}
}Pozostałe moduły konsumują te eventy bez znajomości szczegółów implementacyjnych modułu 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())
);
}
}Gotowy na rozmowy o Spring Boot?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
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.
// 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
}
}Tabela EVENT_PUBLICATION tworzona automatycznie przez 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;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.
// 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()
));
}
}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.
// 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
internalz 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.
// 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
// 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
});
}
}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.
// 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;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.
// 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);
}
}Obserwowalność i Monitoring
Tracing Eventów
Spring Modulith integruje się z Micrometer dla rozproszonego śledzenia eventów między modułami.
// 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 ARCHIVEEndpoint Actuator dla Modułów
Spring Modulith udostępnia endpoint Actuator do podglądu stanu modułów na produkcji.
// 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:
{
"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.
// 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.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

Rozmowa Spring Batch 5: Partycjonowanie, Chunki i Tolerancja Błędów
Opanuj rozmowy o pracę ze Spring Batch 5: 15 kluczowych pytań o partycjonowanie, przetwarzanie chunkowe i tolerancję błędów z przykładami w Java 21.

Rozmowa Spring Boot: Propagacja Transakcji
Opanuj propagację transakcji w Spring Boot: REQUIRED, REQUIRES_NEW, NESTED i więcej. 12 pytań rekrutacyjnych z kodem i typowymi pułapkami.

Spring Security 6: Pełne uwierzytelnianie JWT
Praktyczny przewodnik wdrażania uwierzytelniania JWT w Spring Security 6: konfiguracja, generowanie tokenów, walidacja i najlepsze praktyki bezpieczeństwa.