Loading...
Loading...
Spring Modulith for modular architecture in Spring Boot 3.x. Covers module structure, API vs internal packages, inter-module events, module testing, documentation generation, and observability. USE WHEN: user mentions "spring modulith", "modular monolith", "@ApplicationModule", "module boundaries", "inter-module events", "@ApplicationModuleTest", "modular architecture" DO NOT USE FOR: simple applications - unnecessary complexity, microservices - use proper service boundaries, existing tightly coupled monoliths - requires significant refactoring
npx skill4agent add claude-dev-suite/claude-dev-suite spring-modulithFull Reference: See advanced.md for Event Externalization (Outbox), Module API Exposure, @ApplicationModuleTest, Scenario Testing, Architecture Verification, Observability, and Gradual Decomposition.
┌─────────────────────────────────────────────────────────────────┐
│ Spring Modulith Application │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Order │ │ Payment │ │ Inventory │ │
│ │ Module │──▶│ Module │◀──│ Module │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ order/ │ │ payment/ │ │ inventory/ │ │
│ │ ├─ api/ │ │ ├─ api/ │ │ ├─ api/ │ │
│ │ │ (public) │ │ │ (public) │ │ │ (public) │ │
│ │ └─ internal/ │ │ └─ internal/ │ │ └─ internal/ │ │
│ │ (private) │ │ (private) │ │ (private) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └───────────────────┴───────────────────┘ │
│ Event Bus (Async) │
│ │
└─────────────────────────────────────────────────────────────────┘<!-- pom.xml -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>src/main/java/com/example/ecommerce/
├── EcommerceApplication.java # Root package
├── order/ # Order module
│ ├── Order.java # Public API
│ ├── OrderService.java # Public API
│ ├── OrderCreatedEvent.java # Public event
│ └── internal/ # Internal implementation
│ ├── OrderRepository.java
│ └── OrderValidator.java
├── payment/ # Payment module
│ ├── PaymentService.java
│ └── internal/
└── shared/ # Shared kernel (minimal!)
└── Money.java// Package-info to document module
// order/package-info.java
@org.springframework.modulith.ApplicationModule(
displayName = "Order Management",
allowedDependencies = {"payment", "inventory::InventoryService"}
)
package com.example.ecommerce.order;// Public API (root package)
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher events;
public Order createOrder(CreateOrderRequest request) {
Order order = Order.create(request.customerId(), request.items());
order = orderRepository.save(order);
// Publish event for other modules
events.publishEvent(new OrderCreatedEvent(order.getId(), order.getTotal()));
return order;
}
public void confirmOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirm();
orderRepository.save(order);
events.publishEvent(new OrderConfirmedEvent(orderId));
}
}
// Public event
public record OrderCreatedEvent(Long orderId, Money total) {}// Internal implementation (not accessible from other modules)
// order/internal/OrderRepository.java
@Repository
interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerId(Long customerId);
}// Payment module listens to Order module events
// payment/internal/OrderEventHandler.java
@Component
@RequiredArgsConstructor
@Slf4j
class OrderEventHandler {
private final PaymentService paymentService;
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
log.info("Order created: {}, processing payment", event.orderId());
paymentService.initiatePayment(event.orderId(), event.total());
}
}
// payment/PaymentService.java
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentRepository paymentRepository;
private final ApplicationEventPublisher events;
public void initiatePayment(Long orderId, Money amount) {
Payment payment = Payment.create(orderId, amount);
payment = paymentRepository.save(payment);
processPaymentAsync(payment);
}
@Async
void processPaymentAsync(Payment payment) {
try {
payment.confirm();
paymentRepository.save(payment);
events.publishEvent(new PaymentConfirmedEvent(payment.getOrderId(), payment.getId()));
} catch (PaymentFailedException e) {
payment.fail(e.getMessage());
paymentRepository.save(payment);
events.publishEvent(new PaymentFailedEvent(payment.getOrderId(), e.getMessage()));
}
}
}// Order module reacts to Payment events
// order/internal/PaymentEventHandler.java
@Component
@RequiredArgsConstructor
class PaymentEventHandler {
private final OrderService orderService;
@EventListener
public void onPaymentConfirmed(PaymentConfirmedEvent event) {
orderService.confirmOrder(event.orderId());
}
@EventListener
public void onPaymentFailed(PaymentFailedEvent event) {
orderService.cancelOrder(event.orderId(), event.reason());
}
}// ✅ DO: Expose only what's needed
@ApplicationModule(allowedDependencies = {"shared"})
package com.example.ecommerce.order;
// ✅ DO: Communicate via events
events.publishEvent(new OrderCreatedEvent(orderId));
// ✅ DO: Use records for immutable events
public record OrderCreatedEvent(Long orderId, Money total) {}
// ❌ DON'T: Circular dependencies
// order → payment → order // WRONG!
// ❌ DON'T: Expose repositories
public interface OrderRepository { } // Should not be public
// ❌ DON'T: Direct access to internal
@Autowired
OrderValidator validator; // From another module - WRONG!// ✅ DO: Events with all necessary data
public record OrderCreatedEvent(
Long orderId,
Long customerId,
Money total,
List<OrderItem> items,
Instant createdAt
) {}
// ❌ DON'T: Events requiring callback
public record OrderCreatedEvent(Long orderId) {}
// Consumer must call orderService.getOrder(orderId) - WRONG!| Do | Don't |
|---|---|
| One module = one bounded context | Mix unrelated concerns |
| Public API in root package | Expose internal classes |
Implementation in | Access internal from outside |
| Communicate via events | Direct cross-module calls |
| Use immutable events (records) | Mutable event objects |
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Circular dependency | Modules reference each other | Use events or shared kernel |
| Internal class exposed | Wrong package structure | Move to |
| Event not published | Missing transaction | Verify @Transactional |
| Event lost | No persistence | Use spring-modulith-events-jpa |
| Callback events | Events require calling back | Include all data in event |
| Exposing repositories | Tight coupling | Keep repositories internal |
| Problem | Diagnostic | Fix |
|---|---|---|
| Circular dependency | Run modules.verify() | Refactor to use events |
| Internal access violation | Check package structure | Move classes appropriately |
| Event not received | Check listener | Verify @EventListener annotation |
| Test fails in isolation | Check dependencies | Use appropriate BootstrapMode |
| Event publication fails | Check transaction | Ensure @Transactional present |