Stop Chasing Trends: Monolith is NOT Bad
In recent years, the software industry made it sound like Monoliths are obsolete legacy garbage, and that every new project must start as microservices on Day 1.
“Don't build microservices just because Netflix, Uber, or Amazon do. They have 10,000 engineers and problems you don't have yet. For 90% of software products, a well-structured Monolith is faster to build, easier to maintain, and far cheaper to host.”
Giant platforms like Shopify, GitHub, Basecamp, and Stack Overflow serve millions of requests every day running on monolithic architectures. Microservices solve organizational scaling problems, but they introduce massive operational complexity.
Let's break down what both architectures look like in code, how they work in Java, and where their strengths and weaknesses lie.
What is a Monolithic Architecture?
In a monolithic architecture, your entire software application is built, packaged, and deployed as a single executable unit (for example, one app.jar file in Spring Boot).
│
▼ (HTTP Request)
┌─────────────────────────────────────────────────────────────┐
│ SINGLE SPRING BOOT APPLICATION (app.jar) │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Order Module │ │ Payment Module│ │Inventory Module│ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ (Direct In-Memory Method Calls - 0.01ms) │ │
└──────────┴───────────────────┴───────────────────┴──────────┘
│
▼ (Single ACID Connection Pool)
┌─────────────────────────────────────────────────────────────┐
│ ONE SHARED DATABASE (PostgreSQL / MySQL) │
└─────────────────────────────────────────────────────────────┘
Key Advantages of a Monolith:
- ✓Super Simple Development: Clone one repository, press “Run” in IntelliJ IDEA, and your entire system is running.
- ✓Easy Deployment: Copy one JAR file to a server or Docker container and execute
java -jar app.jar. - ✓Zero Network Overhead: Modules talk to each other via direct Java in-memory method calls (measured in nanoseconds).
- ✓True ACID Transactions: Spring's
@Transactionalguarantees that order creation, inventory deduction, and payment records commit atomically or roll back cleanly.
• Coupled Deployments: A small fix in the notification module requires re-testing and redeploying the entire application.
• Shared Failure: A memory leak or infinite loop in one unoptimized background task can consume all heap memory and crash the whole application for all users.
• Scaling Inefficiency: If only image processing is CPU-heavy, you cannot scale just that component; you must replicate the entire heavy monolith.
What is Microservices Architecture?
In a microservices architecture, the application is divided into a collection of small, independently deployable services. Each service owns its domain logic and has its own dedicated database (Database-per-Service pattern).
│
▼
┌─────────────────────────────────┐
│ API GATEWAY (Spring Cloud) │
└────────────────┬────────────────┘
│ (Network REST / gRPC)
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ Order Microservice │ │ Payment Microservice │ │Inventory Microservice│
│ (Spring Boot 1) │ │ (Spring Boot 2) │ │ (Spring Boot 3) │
└──────────┬───────────┘ └──────────┬───────────┘ └──────────┬───────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ Orders DB (Postgres)│ │ Payments DB (MySQL) │ │ Inventory DB (Mongo) │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
Key Advantages of Microservices:
- ✓Independent Deployments: The checkout team can deploy
order-service5 times a day without coordinating with or touchingpayment-service. - ✓Granular Scalability: If the payment service experiences a 10x traffic spike during a flash sale, you scale only the payment pods in Kubernetes, keeping costs optimal.
- ✓Fault Isolation: If the recommendation service crashes or throws OutOfMemory errors, users can still add items to their cart and checkout.
- ✓Team Autonomy: Large companies with 50+ engineers can divide into autonomous two-pizza teams owning specific services end-to-end.
• The Network is Unreliable: Calls over HTTP/gRPC can fail, time out, or suffer packet loss. You must implement circuit breakers, retries, and fallbacks.
• No Simple ACID Transactions: You cannot use
@Transactional across 3 separate databases. You must implement complex patterns like Sagas (orchestration or choreography) and deal with eventual consistency.• High Operational Overhead: Requires Docker, Kubernetes, Service Discovery, Distributed Tracing (Zipkin/Jaeger), Centralized Logging (ELK), and API Gateways.
Real Java Example: Placing an Order
Look at the massive difference in code complexity when placing an order in an E-Commerce system:
Approach 1: Monolith (Clean, In-Memory, ACID Transaction)
Notice how simple and clean this is. Direct method calls, zero network timeouts, and single-database rollback:
// 1. MONOLITH: Simple, In-Memory Method Calls in One Spring Boot App
package com.example.ecommerce.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final PaymentService paymentService; // In-memory bean
private final InventoryService inventoryService; // In-memory bean
private final OrderRepository orderRepository;
public OrderService(PaymentService paymentService,
InventoryService inventoryService,
OrderRepository orderRepository) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
this.orderRepository = orderRepository;
}
// Single Database Transaction: Either everything succeeds or rolls back!
@Transactional
public Order placeOrder(OrderRequest request) {
// 1. In-memory check (0.01ms latency, no network)
inventoryService.reserveStock(request.productId(), request.quantity());
// 2. Process payment (in-memory call)
paymentService.chargeCustomer(request.customerId(), request.totalAmount());
// 3. Save order to the shared database
Order order = new Order(request);
return orderRepository.save(order);
}
}Approach 2: Microservices (REST Calls, Resilience4j Circuit Breaker)
Notice how much defensive code is required to handle network calls, timeouts, and fallbacks:
// 2. MICROSERVICES: Distributed REST Call with Circuit Breaker (Resilience4j)
package com.example.orderservice.service;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@Service
public class OrderService {
private final RestClient paymentClient;
private final RestClient inventoryClient;
private final OrderRepository orderRepository;
public OrderService(RestClient.Builder builder, OrderRepository orderRepository) {
this.paymentClient = builder.baseUrl("http://payment-service").build();
this.inventoryClient = builder.baseUrl("http://inventory-service").build();
this.orderRepository = orderRepository;
}
// CANNOT use simple @Transactional across separate databases!
// Must handle network timeouts, retries, and partial failure fallbacks.
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public Order placeOrder(OrderRequest request) {
// 1. Remote HTTP REST call over network to Inventory Microservice
inventoryClient.post()
.uri("/api/inventory/reserve")
.body(new ReserveStockDTO(request.productId(), request.quantity()))
.retrieve()
.toBodilessEntity();
// 2. Remote HTTP REST call over network to Payment Microservice
PaymentResponse payment = paymentClient.post()
.uri("/api/payments/charge")
.body(new ChargeDTO(request.customerId(), request.totalAmount()))
.retrieve()
.body(PaymentResponse.class);
// 3. Save in Order Microservice's own dedicated database
Order order = new Order(request, payment.transactionId());
return orderRepository.save(order);
}
// Fallback if Payment Service is down or timing out
public Order paymentFallback(OrderRequest request, Throwable ex) {
System.err.println("Payment Service unavailable: " + ex.getMessage());
throw new ServiceUnavailableException("Payments temporarily down, please retry later.");
}
}Monolith vs Microservices Comparison Table
| Criteria | Monolithic Architecture | Microservices Architecture |
|---|---|---|
| Deployment Unit | Single artifact (one .jar / .war) | Multiple independent containerized services |
| Communication | In-memory calls (nanoseconds, zero network) | Network calls (REST, gRPC, Kafka - milliseconds) |
| Transactions | Simple ACID via @Transactional | Eventual consistency (Sagas, 2-phase commit) |
| Scalability | Scale the entire application instance | Granular scaling of individual high-load services |
| Fault Isolation | One crash can bring down the entire app | Isolated (one service failure doesn't crash others) |
| Dev & Testing | Easy local setup & fast end-to-end debugging | Complex (requires Docker Compose / K8s mocks) |
| Infrastructure Cost | Low (single server or 2 load-balanced nodes) | High (Kubernetes, registries, monitoring clusters) |
| Ideal Team Size | 1 to 20 engineers | 30+ engineers across multiple squads |
The Practical Decision Framework
How do you decide between a Monolith and Microservices for your project? Follow this practical rule:
- •You are building a new startup, MVP, or early-stage product.
- •Your engineering team has fewer than 20 developers.
- •Business requirements and domain boundaries are still changing frequently.
- •You want maximum development speed with minimal DevOps overhead.
- •Multiple development teams (30+ devs) are constantly blocking each other's deployments.
- •Specific modules have drastically different scaling profiles (e.g. video processing vs. user settings).
- •You have a dedicated Platform/DevOps team to manage Kubernetes, CI/CD, and distributed observability.
- •Your domain boundaries are crystal clear and battle-tested.
The Senior Engineer's Conclusion
Architecture is always about trade-offs. Microservices are not a badge of honor, and Monoliths are not a sign of failure.
Key Takeaway:
The best architecture is the one that allows your team to ship value to users quickly, safely, and reliably. Start with a clean, well-factored Modular Monolith. If and when your scale demands it, you can easily carve out microservices along clean domain boundaries without burning time and budget on unnecessary distributed headaches.
Did you find this architecture breakdown useful?
Share your thoughts with other developers: what architecture does your team currently run in production?