Monolith vs Microservices: When to Use Each (With Java Examples)
backend
14 min read
I've built both and regretted both. Here's an honest comparison of monolith vs microservices - with real Spring Boot examples and a decision framework so you don't make the same mistakes.

Published By: Nelson Djalo | Date: June 24, 2025 | Updated: April 5, 2026
Ever found yourself stuck choosing between monolithic architecture vs microservices architecture for your next project? I've been there too. Early in my career, I inherited a sprawling monolith that became a nightmare to scale. Later, I overengineered a tiny app with microservices and spent more time debugging network calls than writing features. Let's break down these architectures so you can avoid my mistakes.
Imagine building a house where the kitchen, bedroom, and bathroom are all one room. That's a monolith. In software terms:
Here's a typical Spring Boot monolith structure:
src/main/java/com/amigoscode/
├── controller/
│ ├── UserController.java
│ ├── OrderController.java
│ └── ProductController.java
├── service/
│ ├── UserService.java
│ ├── OrderService.java
│ └── ProductService.java
├── repository/
│ ├── UserRepository.java
│ ├── OrderRepository.java
│ └── ProductRepository.java
├── model/
│ ├── User.java
│ ├── Order.java
│ └── Product.java
└── config/
└── DatabaseConfig.java
Here's a typical Spring Boot monolith:
// UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> getUsers() {
return userService.findAll();
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
}
// OrderController.java in the SAME codebase
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private UserService userService; // Direct dependency injection
@PostMapping
public Order createOrder(@RequestBody OrderRequest request) {
// Direct method call - no network overhead
User user = userService.findById(request.getUserId());
return orderService.createOrder(user, request.getItems());
}
}
// Shared database configuration
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:postgresql://localhost:5432/monolith_db")
.username("user")
.password("password")
.build();
}
}
Advantages of Monolithic Architecture:
Disadvantages of Monolithic Architecture:
Now picture a neighborhood where each house has a dedicated purpose (cafe, gym, home). Microservices work similarly:
Microservices Architecture Structure:
services/
├── user-service/
│ ├── src/main/java/com/amigoscode/users/
│ │ ├── UserController.java
│ │ ├── UserService.java
│ │ └── UserRepository.java
│ ├── Dockerfile
│ └── application.yml
├── order-service/
│ ├── src/main/java/com/amigoscode/orders/
│ │ ├── OrderController.java
│ │ ├── OrderService.java
│ │ └── OrderRepository.java
│ ├── Dockerfile
│ └── application.yml
├── product-service/
│ ├── src/main/java/com/amigoscode/products/
│ │ ├── ProductController.java
│ │ ├── ProductService.java
│ │ └── ProductRepository.java
│ ├── Dockerfile
│ └── application.yml
└── api-gateway/
├── src/main/java/com/amigoscode/gateway/
│ └── GatewayConfig.java
├── Dockerfile
└── application.yml
Same features, but split:
// User Service (separate codebase)
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> getUsers() {
return userService.findAll(); // Connects to USER_DB
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.findById(id);
}
}
// Order Service (another codebase)
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private UserServiceClient userServiceClient; // HTTP client
@PostMapping
public Order createOrder(@RequestBody OrderRequest request) {
// Network call to UserService
User user = userServiceClient.getUserById(request.getUserId());
if (user == null) {
throw new UserNotFoundException("User not found");
}
return orderService.createOrder(user, request.getItems()); // Connects to ORDER_DB
}
}
// User Service Client (HTTP client for inter-service communication)
@Component
public class UserServiceClient {
private final WebClient webClient;
public UserServiceClient(@Value("${user.service.url}") String userServiceUrl) {
this.webClient = WebClient.builder()
.baseUrl(userServiceUrl)
.build();
}
public User getUserById(Long id) {
return webClient.get()
.uri("/api/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.block();
}
}
// Each service has its own database configuration
@Configuration
public class UserDatabaseConfig {
@Bean
@Primary
public DataSource userDataSource() {
return DataSourceBuilder.create()
.url("jdbc:postgresql://localhost:5432/user_db")
.username("user")
.password("password")
.build();
}
}
Advantages of Microservices Architecture:
Disadvantages of Microservices Architecture:
| Factor | Monolithic | Microservices |
|---|---|---|
| Development | Easier to start | Complex setup |
| Scaling | Vertical scaling only | Horizontal scaling per service |
| Deployment | Single deploy | Independent deploys |
| Database | Shared database | Database per service |
| Failure Impact | System-wide crashes | Isolated failures |
| Team Size | Small teams (1-10) | Multiple teams (10+) |
| Technology Stack | Single stack | Multiple stacks possible |
| Testing | Simple integration tests | Complex distributed testing |
| Monitoring | Single application metrics | Distributed tracing needed |
| Data Consistency | ACID transactions | Eventual consistency |
| Network Calls | Direct method calls | HTTP/gRPC/messaging |
| Code Reuse | Shared libraries | Service-specific implementations |
| Deployment Risk | High (affects entire system) | Low (isolated to service) |
| Performance | Fast (no network overhead) | Slower (network latency) |
I once built a local bakery's inventory system as a monolith. It handled 100 requests/day perfectly. The owner could manage inventory, track sales, and generate reports all from one simple interface. When they needed to add online ordering, we simply added new controllers and services to the existing codebase.
Real-world monolith examples:
Netflix migrated to microservices because their recommendation engine needed 100x more resources than user profiles. Their user service might handle millions of requests per day, while their recommendation service processes complex algorithms that require significant computational resources.
Real-world microservices examples:
Microservices Too Soon
Shared Database in Microservices
Ignoring Observability
Service Granularity Issues
Synchronous Communication Overuse
com.app.user, com.app.order/actuator/health in Spring Boot Actuator// Example of good monolith structure
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<User>> getUsers() {
return ResponseEntity.ok(userService.findAll());
}
}
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
return userRepository.findAll();
}
}
// Health check endpoint
@RestController
public class HealthController {
@GetMapping("/health")
public ResponseEntity<Map<String, String>> health() {
Map<String, String> status = new HashMap<>();
status.put("status", "UP");
status.put("timestamp", Instant.now().toString());
return ResponseEntity.ok(status);
}
}
// API Gateway configuration
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/api/users/**")
.uri("lb://user-service"))
.route("order-service", r -> r
.path("/api/orders/**")
.uri("lb://order-service"))
.build();
}
}
// Circuit breaker implementation
@Service
public class OrderService {
private final UserServiceClient userServiceClient;
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public User getUserForOrder(Long userId) {
return userServiceClient.getUserById(userId);
}
public User getUserFallback(Long userId, Exception ex) {
// Return cached user or default user
return User.builder()
.id(userId)
.name("Unknown User")
.build();
}
}
// Event-driven communication
@Component
public class OrderEventHandler {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
OrderEvent orderEvent = new OrderEvent(
event.getOrderId(),
event.getUserId(),
"ORDER_CREATED"
);
kafkaTemplate.send("order-events", orderEvent);
}
}
Modular Monolith: Like a monolith but with clear boundaries (Java 9+ modules)
Serverless: AWS Lambda for event-driven pieces (e.g., image processing)
Service Mesh: Istio for advanced microservice networking
Event Sourcing: Store events instead of state
CQRS (Command Query Responsibility Segregation): Separate read and write models
1. Can I mix both architectures?
Absolutely. Many systems use a monolith for core features and microservices for edge cases (e.g., analytics). This is called the "Strangler Fig Pattern" - gradually replacing parts of a monolith with microservices. For example, you might keep your core business logic in a monolith but extract image processing or analytics into microservices.
2. How do microservices communicate efficiently?
REST is common, but for performance, try gRPC or Kafka for async messaging. gRPC uses Protocol Buffers and HTTP/2 for efficient binary communication, while Kafka provides reliable, scalable messaging for event-driven architectures. Choose based on your latency and throughput requirements.
3. Is Kubernetes mandatory for microservices?
No, but it helps. Start with Docker Compose, then graduate to Kubernetes. Docker Compose is perfect for development and small deployments, while Kubernetes provides orchestration, scaling, and high availability for production environments. Many teams start with simple containerization and evolve to Kubernetes as they scale.
4. What's the biggest cost with microservices?
Operational overhead. You'll need CI/CD, monitoring, and logging for each service. This includes infrastructure costs, development time for operational tools, and ongoing maintenance. The complexity of distributed systems often requires dedicated DevOps teams and sophisticated tooling.
5. Can Spring Boot work for both?
Yes. For monoliths, use standard Spring Boot. For microservices, add Spring Cloud. Spring Boot provides the foundation, while Spring Cloud adds microservices capabilities like service discovery, configuration management, and circuit breakers. The same Spring Boot application can evolve from monolith to microservices.
6. How do I handle data consistency across microservices? Use eventual consistency patterns like Saga, Event Sourcing, or CQRS. Avoid distributed transactions as they don't scale well. Instead, design your services to handle temporary inconsistencies and use compensating transactions to fix issues when they occur.
7. What's the right size for a microservice? Aim for services that can be developed by a small team (2-8 people) in 2-4 weeks. The service should represent a clear business capability and be deployable independently. If a service takes months to develop or requires coordination across multiple teams, it's probably too large.
8. How do I test microservices effectively? Use contract testing (Pact), integration tests, and end-to-end tests. Contract testing ensures services can communicate correctly, integration tests verify service behavior, and end-to-end tests validate the entire system. Also implement comprehensive monitoring and observability.
Monolith vs microservices isn't about trends -- it's about your team size, project complexity, and where you are right now. Start with a monolith, split when you hit real bottlenecks, and invest in observability from day one.
If you're building your first backend system, our Building APIs with Spring Boot course walks through monolith design. For scaling challenges, Spring Microservices covers distributed patterns.

Skip the generic recommendations. These 9 books changed how I write code, lead teams, and think about systems - from Clean Code to books most devs haven't heard of.

The exact skills, tools, and learning order to go from zero to hired as a Java full stack developer. Covers Spring Boot, React, databases, Docker, and what employers actually look for.

Abstract class or interface? Most Java devs get this wrong. Here's a clear breakdown with a side-by-side comparison table, code examples, and a simple decision rule.
Join thousands of developers mastering in-demand skills with Amigoscode. Try it free today.