Spring Modulith: สถาปัตยกรรม Monolith แบบโมดูลาร์

เรียนรู้ Spring Modulith เพื่อสร้าง monolith แบบโมดูลาร์ใน Java สถาปัตยกรรม โมดูล อีเวนต์อะซิงโครนัส และการทดสอบด้วย Spring Boot 3

Spring Modulith: สถาปัตยกรรม monolith แบบโมดูลาร์ด้วย Spring Boot

Spring Modulith นำเสนอแนวทางที่เป็นรูปธรรมในการจัดโครงสร้างแอปพลิเคชัน Spring Boot ให้แบ่งเป็นโมดูลทางธุรกิจที่เกาะกลุ่มกัน สถาปัตยกรรมนี้วางตำแหน่ง monolith แบบโมดูลาร์ไว้ระหว่าง monolith ดั้งเดิมกับ microservice โดยให้ความเป็นโมดูลที่แข็งแกร่งโดยไม่มีความซับซ้อนในการดูแลระบบกระจายศูนย์

แนวคิดสำคัญ

Spring Modulith ทำให้แนวปฏิบัติของสถาปัตยกรรมแบบ hexagonal และ Domain-Driven Design กลายเป็นสิ่งที่กำหนดอย่างเป็นทางการภายใน Spring Boot พร้อมตรวจสอบการพึ่งพาระหว่างโมดูลโดยอัตโนมัติ

ทำไมถึงเลือก Monolith แบบโมดูลาร์?

ปัญหาของ Monolith แบบดั้งเดิม

Monolith แบบดั้งเดิมประสบปัญหาการพึ่งพากันมากเกินไประหว่างคอมโพเนนต์ เมื่อเวลาผ่านไปการพึ่งพาแบบไขว้สะสมขึ้นและเปลี่ยนแอปพลิเคชันให้กลายเป็น "big ball of mud" ที่ดูแลไม่ได้ การเปลี่ยนแปลงในโมดูลการเรียกเก็บเงินส่งผลถึงโมดูลผู้ใช้แล้วลามไปยังโมดูลแจ้งเตือน ทำให้เกิดผลข้างเคียงที่คาดเดาไม่ได้

AntiPattern.javajava
// 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
<!-- 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>

โครงสร้างของโมดูล

Spring Modulith ตรวจจับโมดูลอัตโนมัติจาก package ที่อยู่ใต้ package หลักของแอปพลิเคชันโดยตรง แต่ละ sub-package แทนโมดูลที่แยกกันโดยมีหน้าที่ของตัวเอง

text
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 สาธารณะที่โมดูลอื่นเข้าถึงได้

Order.javajava
// 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;
    }
}
OrderRepository.javajava
// 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);
}

Repository ยังคงเป็นภายในเพราะการเข้าถึงข้อมูลต้องผ่านเซอร์วิสสาธารณะ ทำให้มั่นใจว่าตรรกะทางธุรกิจถูกห่อหุ้มไว้

แนวการตั้งชื่อ

Package internal ไม่มีความพิเศษอะไรในมุมของ Java แต่เป็นแนวปฏิบัติที่ Spring Modulith รู้จักและยืนยันโดยอัตโนมัติระหว่างการทดสอบ การละเมิดทุกครั้งจะทำให้เกิดข้อผิดพลาดที่ชัดเจน

การสื่อสารระหว่างโมดูล

Domain Event

การสื่อสารระหว่างโมดูลใช้ domain event แทนการเรียกตรง รูปแบบนี้แยกโมดูลผู้ส่งออกจากโมดูลผู้รับ

OrderCreatedEvent.javajava
// 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()
        );
    }
}
OrderService.javajava
// 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

NotificationEventListener.javajava
// 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())
        );
    }
}

พร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

อีเวนต์แบบ Persisted และ Async

Spring Modulith มอบฟีเจอร์ที่ทรงพลัง: การคงอยู่ของอีเวนต์ อีเวนต์ถูกบันทึกในฐานข้อมูลก่อนเผยแพร่ ทำให้รับประกันการประมวลผลแม้แอปพลิเคชันจะแครช

EventPublicationConfig.javajava
// 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
}
InventoryEventListener.javajava
// 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
    }
}

ตาราง 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;

อินเทอร์เฟซที่เปิดเผยระหว่างโมดูล

เมื่อโมดูลหนึ่งต้องการข้อมูลจากอีกโมดูลโดยไม่ใช้อีเวนต์ การมีอินเทอร์เฟซสาธารณะในโมดูลต้นทางช่วยให้รักษาการพึ่งพาให้น้อยที่สุด

CustomerLookup.javajava
// 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
    ) {}
}
CustomerLookupImpl.javajava
// 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()
            ));
    }
}

แนวทางนี้ช่วยให้โมดูล Notification เข้าถึงข้อมูลลูกค้าโดยไม่ต้องพึ่งพา repository หรือ entity Customer โดยตรง

การทดสอบโครงสร้างโมดูล

การตรวจสอบ Dependency อัตโนมัติ

Spring Modulith ให้เครื่องมือทดสอบเพื่อยืนยันว่ากฎสถาปัตยกรรมถูกปฏิบัติตาม การทดสอบเหล่านี้จะล้มเหลวเมื่อโมดูลหนึ่งเข้าถึงคลาสภายในของอีกโมดูล

ModularityTests.javajava
// 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 ที่จำเป็น

OrderModuleIntegrationTests.javajava
// 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 สำหรับการพึ่งพาไปยังโมดูลอื่น
  • โครงสร้างพื้นฐานสำหรับทดสอบอีเวนต์
OrderNotificationIntegrationTest.javajava
// 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 ช่วยให้ตั้งค่ากฎของโมดูลได้อย่างชัดเจน

package-info.javajava
// 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;
NamedInterface.javajava
// 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"
}
package-info.javajava
// 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
StockReservedEvent.javajava
// 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
) {}
OrderStockListener.javajava
// 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 แบบกระจายของอีเวนต์ระหว่างโมดูล

ObservabilityConfig.javajava
// 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 เพื่อแสดงสถานะของโมดูลในโปรดักชัน

ModulithActuatorConfig.javajava
// 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 ในอนาคตทำได้ง่าย แต่ละโมดูลกลายเป็นผู้สมัครตามธรรมชาติสำหรับการแยกออก

ExtractionReadinessChecker.javajava
// 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 แต่ยังคงเปิดทางเลือกในการพัฒนาไปสู่สถาปัตยกรรมแบบกระจายเมื่อจำเป็น

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#spring modulith
#modular architecture
#spring boot
#java
#modular monolith

แชร์

บทความที่เกี่ยวข้อง