After building both, here's my honest take — microservices are not always better. The decision framework I use, the hidden costs nobody talks about, and when a modular monolith wins.
I've built systems as monoliths and as microservices. The honest truth is that most teams jump to microservices too early and pay a massive operational tax for years. Here's the decision framework I actually use.
Split on team boundaries, not on data boundaries. If two features are owned by different teams who need to deploy independently, split them. If one feature has dramatically different scaling needs (e.g., a video encoder vs a CRUD API), split it. Otherwise, don't.
Start with a well-structured monolith with clear package/module boundaries. You can always split a clean monolith into services later. Merging two microservices that should have been one module is a nightmare.
My default recommendation for teams under 20 engineers: a modular monolith. Strict package boundaries, no cross-module direct calls (use internal events via Spring ApplicationEvent), separate data schemas per module. You get 80% of the benefits with 20% of the operational complexity.
1@Service
2public class PaymentService {
3
4 @CircuitBreaker(name = "payment", fallbackMethod = "paymentFallback")
5 @TimeLimiter(name = "payment")
6 @Retry(name = "payment")
7 public CompletableFuture<PaymentResult> processPayment(PaymentRequest req) {
8 return CompletableFuture.supplyAsync(() -> externalPaymentGateway.process(req));
9 }
10
11 public CompletableFuture<PaymentResult> paymentFallback(
12 PaymentRequest req, Exception ex) {
13 log.error("Payment gateway unavailable, queuing for retry", ex);
14 paymentRetryQueue.enqueue(req);
15 return CompletableFuture.completedFuture(PaymentResult.pending(req.getId()));
16 }
17}More in System Design