# Spring Modulith: สถาปัตยกรรม Monolith แบบโมดูลาร์ > เรียนรู้ Spring Modulith เพื่อสร้าง monolith แบบโมดูลาร์ใน Java สถาปัตยกรรม โมดูล อีเวนต์อะซิงโครนัส และการทดสอบด้วย 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 นำเสนอแนวทางที่เป็นรูปธรรมในการจัดโครงสร้างแอปพลิเคชัน Spring Boot ให้แบ่งเป็นโมดูลทางธุรกิจที่เกาะกลุ่มกัน สถาปัตยกรรมนี้วางตำแหน่ง monolith แบบโมดูลาร์ไว้ระหว่าง monolith ดั้งเดิมกับ microservice โดยให้ความเป็นโมดูลที่แข็งแกร่งโดยไม่มีความซับซ้อนในการดูแลระบบกระจายศูนย์ > **แนวคิดสำคัญ** > > Spring Modulith ทำให้แนวปฏิบัติของสถาปัตยกรรมแบบ hexagonal และ Domain-Driven Design กลายเป็นสิ่งที่กำหนดอย่างเป็นทางการภายใน Spring Boot พร้อมตรวจสอบการพึ่งพาระหว่างโมดูลโดยอัตโนมัติ ## ทำไมถึงเลือก Monolith แบบโมดูลาร์? ### ปัญหาของ Monolith แบบดั้งเดิม Monolith แบบดั้งเดิมประสบปัญหาการพึ่งพากันมากเกินไประหว่างคอมโพเนนต์ เมื่อเวลาผ่านไปการพึ่งพาแบบไขว้สะสมขึ้นและเปลี่ยนแอปพลิเคชันให้กลายเป็น "big ball of mud" ที่ดูแลไม่ได้ การเปลี่ยนแปลงในโมดูลการเรียกเก็บเงินส่งผลถึงโมดูลผู้ใช้แล้วลามไปยังโมดูลแจ้งเตือน ทำให้เกิดผลข้างเคียงที่คาดเดาไม่ได้ ```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; } } ``` รูปแบบนี้สร้างปัญหาที่จับต้องได้: เทสต์อินทิเกรชันเปราะบาง การประเมินผลกระทบของการเปลี่ยนแปลงทำได้ยาก และไม่สามารถดีพลอยหรือพัฒนาแต่ละโมดูลแยกอิสระได้ ### Microservice ไม่ใช่คำตอบเสมอไป Microservice แก้ปัญหาการพึ่งพากันได้ แต่ก็เพิ่มความซับซ้อนในการดูแลระบบอย่างมาก: การสื่อสารผ่านเครือข่าย eventual consistency การดีพลอยแบบกระจาย และการสังเกตการณ์ระหว่างหลายเซอร์วิส สำหรับหลายทีม ความซับซ้อนนี้ไม่คุ้มกับประโยชน์ที่ได้รับ Monolith แบบโมดูลาร์มอบทางเลือก: หน่วยดีพลอยเดียวที่มีขอบเขตของโมดูลซึ่งกำหนดและบังคับใช้อย่างชัดเจน Spring Modulith ทำให้การตรวจสอบขอบเขตเหล่านั้นเป็นอัตโนมัติ ## เริ่มต้นใช้งาน Spring Modulith ### การตั้งค่าโปรเจกต์ การรวม Spring Modulith เข้ากับโปรเจกต์ Spring Boot 3 ต้องใช้ dependency ของ Maven บางตัว starter หลักจะเปิดใช้งานการตรวจจับโมดูลอัตโนมัติ ```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 ``` ### โครงสร้างของโมดูล Spring Modulith ตรวจจับโมดูลอัตโนมัติจาก package ที่อยู่ใต้ package หลักของแอปพลิเคชันโดยตรง แต่ละ sub-package แทนโมดูลที่แยกกันโดยมีหน้าที่ของตัวเอง ``` 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 ``` แนวปฏิบัตินี้กำหนดกฎพื้นฐาน: เฉพาะคลาสที่อยู่ใน package ระดับรากของโมดูล (ไม่ใช่ใน `internal/`) เท่านั้นที่ประกอบเป็น API สาธารณะที่โมดูลอื่นเข้าถึงได้ ```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 ยังคงเป็นภายในเพราะการเข้าถึงข้อมูลต้องผ่านเซอร์วิสสาธารณะ ทำให้มั่นใจว่าตรรกะทางธุรกิจถูกห่อหุ้มไว้ > **แนวการตั้งชื่อ** > > Package `internal` ไม่มีความพิเศษอะไรในมุมของ Java แต่เป็นแนวปฏิบัติที่ Spring Modulith รู้จักและยืนยันโดยอัตโนมัติระหว่างการทดสอบ การละเมิดทุกครั้งจะทำให้เกิดข้อผิดพลาดที่ชัดเจน ## การสื่อสารระหว่างโมดูล ### Domain Event การสื่อสารระหว่างโมดูลใช้ domain event แทนการเรียกตรง รูปแบบนี้แยกโมดูลผู้ส่งออกจากโมดูลผู้รับ ```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; } } ``` โมดูลอื่นบริโภคอีเวนต์เหล่านี้โดยไม่จำเป็นต้องรู้รายละเอียดการพัฒนาภายในของโมดูล 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()) ); } } ``` ### อีเวนต์แบบ Persisted และ Async Spring Modulith มอบฟีเจอร์ที่ทรงพลัง: การคงอยู่ของอีเวนต์ อีเวนต์ถูกบันทึกในฐานข้อมูลก่อนเผยแพร่ ทำให้รับประกันการประมวลผลแม้แอปพลิเคชันจะแครช ```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 } } ``` ตาราง `EVENT_PUBLICATION` ที่ 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; ``` ### อินเทอร์เฟซที่เปิดเผยระหว่างโมดูล เมื่อโมดูลหนึ่งต้องการข้อมูลจากอีกโมดูลโดยไม่ใช้อีเวนต์ การมีอินเทอร์เฟซสาธารณะในโมดูลต้นทางช่วยให้รักษาการพึ่งพาให้น้อยที่สุด ```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() )); } } ``` แนวทางนี้ช่วยให้โมดูล Notification เข้าถึงข้อมูลลูกค้าโดยไม่ต้องพึ่งพา repository หรือ entity Customer โดยตรง ## การทดสอบโครงสร้างโมดูล ### การตรวจสอบ Dependency อัตโนมัติ Spring Modulith ให้เครื่องมือทดสอบเพื่อยืนยันว่ากฎสถาปัตยกรรมถูกปฏิบัติตาม การทดสอบเหล่านี้จะล้มเหลวเมื่อโมดูลหนึ่งเข้าถึงคลาสภายในของอีกโมดูล ```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(); } } ``` การรัน `modules.verify()` วิเคราะห์ bytecode และตรวจจับ: - การเข้าถึง package `internal` จากโมดูลอื่น - การพึ่งพาแบบวงรอบระหว่างโมดูล - การละเมิดกฎการห่อหุ้ม ### การทดสอบอินทิเกรชันต่อโมดูล Spring Modulith ช่วยให้ทดสอบแต่ละโมดูลได้แบบแยกอิสระ โดยโหลดเฉพาะ bean ที่จำเป็น ```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()); }); } } ``` แอนโนเทชัน `@ApplicationModuleTest` ตั้งค่าอัตโนมัติ: - โหลดเฉพาะ bean ของโมดูล Order - mock สำหรับการพึ่งพาไปยังโมดูลอื่น - โครงสร้างพื้นฐานสำหรับทดสอบอีเวนต์ ```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 }); } } ``` > **การแยกอิสระของเทสต์** > > ใช้ `BootstrapMode.STANDALONE` (ค่าเริ่มต้น) สำหรับ unit test ของโมดูล สงวน `BootstrapMode.ALL_DEPENDENCIES` ไว้สำหรับเทสต์อินทิเกรชันแบบ end-to-end เพื่อหลีกเลี่ยงการพึ่งพาแฝง ## การตั้งค่าโมดูลขั้นสูง ### โมดูลแบบ Explicit ด้วย @ApplicationModule สำหรับกรณีที่ซับซ้อน แอนโนเทชัน `@ApplicationModule` ช่วยให้ตั้งค่ากฎของโมดูลได้อย่างชัดเจน ```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; ``` ### การจัดการการพึ่งพาแบบวงรอบ การพึ่งพาแบบวงรอบระหว่างโมดูลมักบ่งชี้ปัญหาด้านการออกแบบ Spring Modulith ตรวจพบและทำให้การยืนยันล้มเหลว วิธีแก้โดยทั่วไปคือแยกโมดูลใหม่ออกมาหรือใช้อีเวนต์ ```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 และ Monitoring ### การ Trace อีเวนต์ Spring Modulith ผสานรวมกับ Micrometer สำหรับการ trace แบบกระจายของอีเวนต์ระหว่างโมดูล ```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 ``` ### Actuator Endpoint สำหรับโมดูล Spring Modulith เปิดเผย Actuator endpoint เพื่อแสดงสถานะของโมดูลในโปรดักชัน ```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` คืนค่า: ```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" ] } ] } ``` ## การย้ายไปสู่ Microservice ### การเตรียมการแยก สถาปัตยกรรมแบบโมดูลาร์ช่วยให้การแยกไปสู่ microservice ในอนาคตทำได้ง่าย แต่ละโมดูลกลายเป็นผู้สมัครตามธรรมชาติสำหรับการแยกออก ```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 } } ``` โมดูลที่สื่อสารผ่านอีเวนต์เพียงอย่างเดียวสามารถแยกเป็น microservice ได้ด้วยการแก้ไขเล็กน้อย: เพียงแทนที่ event bus ภายในด้วย message broker (Kafka, RabbitMQ) ## สรุป Spring Modulith มอบทางออกที่เป็นรูปธรรมในการจัดโครงสร้างแอปพลิเคชัน Spring Boot แบบ monolith: ✅ **โครงสร้างตามแนวปฏิบัติ**: package = โมดูล, `internal` = การห่อหุ้ม ✅ **การสื่อสารแบบลดการพึ่งพา**: domain event ระหว่างโมดูล ✅ **การตรวจสอบอัตโนมัติ**: เทสต์โครงสร้างที่ตรวจจับการละเมิด ✅ **อีเวนต์แบบคงทน**: รับประกันการประมวลผลด้วย `@ApplicationModuleListener` ✅ **เทสต์แบบแยกอิสระ**: `@ApplicationModuleTest` สำหรับยืนยันแต่ละโมดูล ✅ **เอกสารที่สร้างให้**: ไดอะแกรม PlantUML อัตโนมัติ ✅ **Observability**: การผสานรวม Micrometer และ Actuator endpoint ✅ **เส้นทางสู่ microservice**: การแยกออกง่ายขึ้นจากการลดการพึ่งพา สถาปัตยกรรมนี้เหมาะอย่างยิ่งสำหรับทีมที่ต้องการจัดโครงสร้าง monolith โดยไม่ต้องรับความซับซ้อนในการดูแลของ microservice แต่ยังคงเปิดทางเลือกในการพัฒนาไปสู่สถาปัตยกรรมแบบกระจายเมื่อจำเป็น --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/spring-boot/spring-modulith-modular-monolith-architecture