Modular Monolith: The Architecture Between Monolith and Microservices
Microservices solve organizational scaling problems but introduce distributed systems complexity. A modular monolith gives you clean boundaries without the operational overhead.
What Makes It Modular
Each module owns its data, exposes a public API, and communicates with other modules only through defined interfaces. No shared database tables, no reaching into another module's internals.
// Module boundary — only this interface is public
public interface OrderModule {
OrderDTO createOrder(CreateOrderRequest request);
OrderDTO getOrder(String orderId);
List<OrderDTO> getOrdersByCustomer(String customerId);
}
// Internal implementation is package-private
class OrderService implements OrderModule {
private final OrderRepository orderRepo; // Module's own tables
private final PaymentModule paymentModule; // Interface dependency
}
Module Communication
Use in-process events instead of direct method calls for cross-module communication. Spring's ApplicationEventPublisher works perfectly. When you eventually extract a module into a service, replace in-process events with Kafka — the code barely changes.
The Migration Path
Start monolithic. When a module needs independent scaling or a different deployment cadence, extract it. You have the boundaries already defined — extraction is mechanical, not architectural.