# 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