# Spring Modulith: Arsitektur Monolit Modular Dijelaskan > Pelajari Spring Modulith untuk membangun monolit modular di Java. Arsitektur, modul, event asinkron, dan testing dengan contoh 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 menawarkan pendekatan pragmatis untuk menyusun aplikasi Spring Boot menjadi modul bisnis yang kohesif. Arsitektur ini menempatkan monolit modular di antara monolit tradisional dan microservice, memberikan modularitas yang kuat tanpa kompleksitas operasional sistem terdistribusi. > **Wawasan Kunci** > > Spring Modulith memformalkan praktik terbaik arsitektur heksagonal dan Domain-Driven Design langsung di dalam Spring Boot, dengan verifikasi otomatis dependensi antar modul. ## Mengapa Memilih Monolit Modular? ### Masalah Monolit Klasik Monolit tradisional mengalami sambungan berlebihan antar komponen. Seiring waktu, ketergantungan silang menumpuk dan mengubah aplikasi menjadi "big ball of mud" yang tidak bisa dipelihara. Perubahan pada modul tagihan memengaruhi modul pengguna, lalu modul notifikasi, menciptakan efek samping yang tidak terduga. ```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; } } ``` Pola ini menimbulkan masalah konkret: tes integrasi yang rapuh, kesulitan menalar dampak perubahan, dan ketidakmampuan men-deploy atau mengembangkan modul secara independen. ### Microservice Tidak Selalu Jawabannya Microservice menyelesaikan masalah sambungan, tetapi memperkenalkan kompleksitas operasional yang besar: komunikasi jaringan, eventual consistency, deployment terdistribusi, dan observability multi-service. Bagi banyak tim, kompleksitas ini tidak sebanding dengan manfaat yang diperoleh. Monolit modular menawarkan alternatif: satu unit deployment dengan batas modul yang jelas dan ditegakkan. Spring Modulith mengotomatiskan verifikasi batas-batas tersebut. ## Memulai dengan Spring Modulith ### Konfigurasi Proyek Mengintegrasikan Spring Modulith ke proyek Spring Boot 3 membutuhkan beberapa dependency Maven. Starter utama mengaktifkan deteksi modul otomatis. ```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 ``` ### Struktur Modul Spring Modulith mendeteksi modul secara otomatis berdasarkan paket langsung di bawah paket utama aplikasi. Setiap subpaket merepresentasikan modul tersendiri dengan tanggung jawabnya sendiri. ``` 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 ``` Konvensi ini menetapkan aturan mendasar: hanya kelas di paket akar modul (bukan di `internal/`) yang membentuk API publik yang dapat diakses modul lain. ```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); } ``` Repository tetap internal karena akses data harus melalui service publik, sehingga enkapsulasi logika bisnis terjamin. > **Konvensi Penamaan** > > Paket `internal` tidak memiliki keajaiban khusus bagi Java. Ini adalah konvensi yang dikenali Spring Modulith dan diverifikasi otomatis selama testing. Setiap pelanggaran menghasilkan error eksplisit. ## Komunikasi Antar Modul ### Domain Event Komunikasi antar modul dilakukan melalui domain event alih-alih panggilan langsung. Pola ini memisahkan modul pengirim dari modul penerima. ```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; } } ``` Modul lain mengonsumsi event tersebut tanpa mengetahui detail implementasi modul 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()) ); } } ``` ### Event Persisten dan Asinkron Spring Modulith menawarkan fitur kuat: persistensi event. Event disimpan ke database sebelum dipublikasikan, menjamin pemrosesan bahkan bila aplikasi crash. ```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 } } ``` Tabel `EVENT_PUBLICATION` yang dibuat otomatis oleh 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; ``` ### Antarmuka yang Diekspos Antar Modul Ketika sebuah modul membutuhkan informasi dari modul lain tanpa menggunakan event, antarmuka publik di modul sumber memungkinkan sambungan yang minimal. ```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() )); } } ``` Pendekatan ini memungkinkan modul Notification mengakses informasi pelanggan tanpa bergantung langsung pada repository atau entitas Customer. ## Tes Struktur Modular ### Verifikasi Dependensi Otomatis Spring Modulith menyediakan alat testing untuk memverifikasi bahwa aturan arsitektur dipatuhi. Tes ini gagal jika sebuah modul mengakses kelas internal modul lain. ```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(); } } ``` Menjalankan `modules.verify()` menganalisis bytecode dan mendeteksi: - Akses ke paket `internal` dari modul lain - Dependensi siklik antar modul - Pelanggaran aturan enkapsulasi ### Tes Integrasi per Modul Spring Modulith memungkinkan setiap modul diuji secara terisolasi dengan memuat hanya bean yang diperlukan. ```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()); }); } } ``` Anotasi `@ApplicationModuleTest` mengonfigurasi secara otomatis: - Memuat hanya bean modul Order - Mock untuk dependensi ke modul lain - Infrastruktur testing untuk event ```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 }); } } ``` > **Isolasi Tes** > > Gunakan `BootstrapMode.STANDALONE` (default) untuk unit test modul. Sisakan `BootstrapMode.ALL_DEPENDENCIES` untuk tes integrasi end-to-end agar terhindar dari dependensi tersembunyi. ## Konfigurasi Modul Tingkat Lanjut ### Modul Eksplisit dengan @ApplicationModule Untuk kasus kompleks, anotasi `@ApplicationModule` memungkinkan konfigurasi aturan modul secara eksplisit. ```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; ``` ### Menangani Dependensi Siklik Dependensi siklik antar modul biasanya menandakan masalah desain. Spring Modulith mendeteksinya dan menggagalkan verifikasi. Solusinya umumnya berupa mengekstrak modul baru atau menggunakan event. ```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); } } ``` ## Observability dan Monitoring ### Tracing Event Spring Modulith terintegrasi dengan Micrometer untuk distributed tracing event antar modul. ```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 Modul Spring Modulith menyediakan endpoint Actuator untuk memvisualisasikan status modul di production. ```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` mengembalikan: ```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" ] } ] } ``` ## Migrasi ke Microservice ### Persiapan Ekstraksi Arsitektur modular memudahkan ekstraksi ke microservice di masa depan. Setiap modul menjadi kandidat alami untuk diekstrak. ```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 } } ``` Modul yang berkomunikasi murni melalui event dapat diekstrak menjadi microservice dengan perubahan minimal: cukup mengganti event bus lokal dengan message broker (Kafka, RabbitMQ). ## Kesimpulan Spring Modulith menyediakan solusi pragmatis untuk menyusun aplikasi Spring Boot monolitik: ✅ **Struktur konvensi**: paket = modul, `internal` = enkapsulasi ✅ **Komunikasi terlepas**: domain event antar modul ✅ **Verifikasi otomatis**: tes struktur yang menangkap pelanggaran ✅ **Event persisten**: jaminan pemrosesan dengan `@ApplicationModuleListener` ✅ **Tes terisolasi**: `@ApplicationModuleTest` untuk memvalidasi setiap modul ✅ **Dokumentasi terbangkit**: diagram PlantUML otomatis ✅ **Observability**: integrasi Micrometer dan endpoint Actuator ✅ **Jalan menuju microservice**: ekstraksi dipermudah oleh decoupling Arsitektur ini sangat cocok bagi tim yang ingin menyusun monolitnya tanpa kompleksitas operasional microservice, sambil tetap membuka opsi berkembang ke arsitektur terdistribusi saat dibutuhkan. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/spring-boot/spring-modulith-modular-monolith-architecture