Skip to content

When Should You Choose Microservices vs Monolith for Spring Boot? A Decision Framework

Problem

I made a mistake that cost my team six months of wasted effort. We started a new Spring Boot project, and someone said: “Let’s use microservices. That’s what modern architecture looks like.”

So we did. We split our application into five services before writing our first feature. We spent weeks setting up:

  • Service discovery (Eureka)
  • API gateway (Spring Cloud Gateway)
  • Configuration server
  • Distributed tracing (Zipkin)
  • Message queue (RabbitMQ)
  • Docker and Kubernetes

Six months later, we had:

  • Zero production features deployed
  • Three developers who quit
  • A distributed system that nobody understood
  • Debugging sessions that lasted hours

The real question I should have asked was: “When should I choose microservices vs monolith for my Spring Boot application?”

Not “What’s the modern architecture?” But “What architecture solves MY actual problems?”

Why This Decision Is Hard

Every developer I talk to faces this dilemma. The industry hype around microservices creates pressure:

Industry Pressure Examples
"What Netflix does" → "We should do it too"
"What Amazon does" → "Microservices are the future"
"Spring Cloud exists" → "Spring Boot makes microservices easy"

But here’s what I learned: Netflix and Amazon have thousands of developers and billions of users. You probably don’t.

The truth is:

  • Spring Boot supports both monoliths and microservices equally well
  • Microservices are NOT the default modern choice
  • Most Spring Boot applications should start as a monolith
  • The decision is about business needs, not technology trends

My Failed Attempt: The Premature Microservices

Let me show you what went wrong. We were building a simple order management system with:

  • 3 developers
  • Uncertain requirements (startup phase)
  • Zero DevOps experience
  • Need to ship fast

But we built this instead:

Our Premature Microservices Architecture
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Order │────→│ Inventory │────→│ Payment │
│ Service │ │ Service │ │ Service │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
↓ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Order DB │ │ Inventory │ │ Payment │
│ (MySQL) │ │ DB │ │ DB │
└─────────────┘ └─────────────┘ └─────────────┘
All connected via:
- Eureka (service discovery)
- RabbitMQ (async messaging)
- Spring Cloud Gateway
- Zipkin (tracing)
- Kubernetes

This was wrong because:

  1. No domain boundaries existed - Order, Inventory, and Payment were tightly coupled
  2. Same scaling needs - All services scaled together anyway
  3. Same team - One team owned all services, no autonomy benefit
  4. No DevOps maturity - We couldn’t debug distributed failures
  5. No independent deployment need - All features deployed together

The Decision Framework I Now Use

After that failure, I created a framework for deciding:

Start with Monolith When:

  • New project or startup with uncertain requirements
  • Team size is small (2-5 developers)
  • Domain is not yet well-understood
  • Time to market is critical
  • No dedicated DevOps/infrastructure resources
  • Deployment frequency is moderate

This is 90% of Spring Boot projects.

Consider Modular Monolith When:

  • Monolith is growing but microservices seem like overkill
  • Want clear boundaries without distributed system complexity
  • Medium-sized team (5-15 developers)
  • Better organization needed but still single deployment unit
  • Preparing for potential future microservices migration

Move to Microservices ONLY When:

  • Clear domain boundaries exist with different scaling needs
  • Multiple teams need independent deployment cycles
  • Specific services need different technology stacks
  • Mature DevOps practices and infrastructure exist
  • Fault isolation between services is critical
  • Team size is large (15+ developers across multiple domains)

Visual Decision Flow

Architecture Decision Flow
┌─────────────────────────────┐
│ New Spring Boot Project │
└──────────────────┬──────────┘
┌───────────────▼───────────────┐
│ Do you have 15+ developers │
│ across multiple domains? │
└───────────────┬───────────────┘
┌───────┴───────┐
│ │
NO YES
│ │
┌────────────▼────────────┐ │
│ Do you need independent │ │
│ deployment per domain? │ │
└────────────┬────────────┘ │
┌──────┴──────┐ │
│ │ │
NO YES │
│ │ │
┌──────────▼──────────┐ │ ┌────▼────┐
│ Do domains have │ │ │ START │
│ different scaling │ │ │ MICRO- │
│ requirements? │ │ │SERVICES │
└──────────┬──────────┘ │ └────┬────┘
┌─────┴─────┐ │ │
│ │ │ │
NO YES │ │
│ │ │ │
┌──────────▼──────┐ │ │ │
│ START MONOLITH │ │ │ │
│ (or Modular │ │ │ │
│ Monolith) │ │ │ │
└──────┬──────────┘ │ │ │
│ │ │ │
│ ┌────────────▼───────┴───────┴─────┐
│ │ EVOLVE TO MICROSERVICES │
│ │ ONLY WHEN SPECIFIC BENEFITS │
│ │ ARE CLEAR AND MEASUREABLE │
│ └─────────────────────────────────┘
"Start simple, evolve when needed"

The Right Approach: Monolith First

For the same order management system, here’s what I should have built:

Monolith Structure (Package by Feature)
src/main/java/com/example/
├── order/ // Order domain
│ ├── controller/
│ │ └── OrderController.java
│ ├── service/
│ │ └── OrderService.java
│ ├── repository/
│ │ └── OrderRepository.java
│ └── domain/
│ │ └── Order.java, OrderItem.java
├── inventory/ // Inventory domain
│ ├── controller/
│ ├── service/
│ ├── repository/
│ └── domain/
├── payment/ // Payment domain
│ └── ...
├── shared/ // Shared utilities
│ └── exception/
│ └── config/
└── Application.java // Single entry point

This is a monolith with clean internal boundaries. Benefits:

  • Simple: Single deployable unit, one database
  • Fast: Local method calls, no network latency
  • Easy debugging: Stack traces make sense
  • ACID transactions: No distributed transaction complexity
  • Quick deployment: One container, one pipeline
Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Single deployable JAR
// Package by feature for clear boundaries
// Easy to extract modules later if needed

When to Extract: Real Indicators

After running the monolith for a year, I saw clear indicators:

Indicator 1: Different Scaling Needs

PaymentService.java
@Service
public class PaymentService {
// This service handles 10x traffic at month end
// Needs dedicated resources
// Has completely different performance profile
// -> Candidate for extraction
}

When Payment needed to handle batch processing at month-end (thousands of transactions per minute) while Order had steady low traffic, the scaling mismatch became real.

Indicator 2: Compliance Isolation

Payment processing required PCI-DSS compliance. Keeping it in the monolith meant the entire application needed stricter security controls. Extracting Payment into its own service reduced compliance scope.

Indicator 3: Dedicated Team

A dedicated payments team joined. They wanted to:

  • Deploy on their schedule
  • Use their own CI/CD pipeline
  • Make changes without coordinating with the order team

Indicator 4: Different Technology Stack

Payment needed integration with a third-party payment gateway that required Node.js. We couldn’t use Java for that integration.

The Correct Extraction Process

When these indicators aligned, I extracted Payment:

Extraction Process
Phase 1: Define Clear API Boundary
────────────────────────────────
// Define the contract first
interface PaymentGateway {
PaymentResult processPayment(Order order);
RefundResult processRefund(String paymentId);
}
Phase 2: Build New Service
──────────────────────────
// Create payment-service project
// Implement PaymentGateway
// Deploy independently
Phase 3: Integration
────────────────────
// Order service calls Payment via HTTP
// Use circuit breaker for resilience
// Monitor separately
Phase 4: Database Separation
────────────────────────────
// Payment has its own database
// No shared tables
// Clear data ownership
OrderService.java
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
// Initially: paymentGateway was local implementation
// After extraction: paymentGateway is HTTP client
public Order createOrder(CreateOrderRequest request) {
Order order = new Order(request);
PaymentResult result = paymentGateway.processPayment(order);
if (result.isSuccessful()) {
order.confirm();
return orderRepository.save(order);
}
throw new PaymentFailedException(result.getError());
}
}

Trade-offs I Learned

Monolith Trade-offs

AspectMonolith AdvantageMonolith Risk
DevelopmentFast iteration, simple debuggingCan become messy if not structured
DeploymentSingle pipeline, one containerAll-or-nothing scaling
TransactionsACID, simple consistencySingle database
TeamEasy coordinationPotential bottlenecks

Microservices Trade-offs

AspectMicroservices AdvantageMicroservices Risk
ScalingIndependent scaling per serviceOperational complexity
DeploymentIndependent deployment cyclesCoordination overhead
TeamsTeam autonomyCommunication overhead
TechnologyTechnology flexibilityIntegration complexity

Key insight: The cost of starting with monolith and evolving is usually lower than starting with microservices.

Modular Monolith: The Middle Ground

For many projects, modular monolith is the sweet spot:

settings.gradle
// Each module as separate Gradle project
// but deployed as single Spring Boot JAR
include 'order-module'
include 'inventory-module'
include 'payment-module'
include 'shared-kernel'
order-module/build.gradle
dependencies {
implementation project(':shared-kernel')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
Application.java
@SpringBootApplication
@ComponentScan(basePackages = {
"com.example.order",
"com.example.inventory",
"com.example.payment"
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

This gives you:

  • Clear module boundaries
  • Enforced dependencies (modules can’t access each other’s internals)
  • Single deployment unit
  • Easy extraction to microservices when needed

Common Mistakes to Avoid

Based on my experience:

Mistake 1: “Netflix/Amazon/Google Do It”

Reality: They have:

  • Thousands of developers
  • Billions of users
  • Mature DevOps teams
  • Years of operational experience

You probably don’t.

Mistake 2: “Microservices = Modern”

Reality: Architecture should solve problems, not follow trends. A well-structured monolith beats a poorly-designed microservices system every time.

Mistake 3: “Spring Cloud Makes It Easy”

Reality: Spring Cloud simplifies implementation, but doesn’t solve operational complexity. You still need:

  • Service discovery debugging
  • Distributed tracing analysis
  • Network failure handling
  • Data consistency management

Mistake 4: Skipping Modular Monolith

Reality: Modular monolith gives you boundary discipline without distributed system complexity. It’s often the best first step.

Mistake 5: Extracting Before Understanding

Reality: You can’t define good service boundaries without understanding the domain. Start with monolith, let boundaries emerge naturally.

The Questions I Now Ask

Before deciding, I ask these questions:

  1. Team size: Can one team own the entire system?
  2. Domain clarity: Do I understand where boundaries should be?
  3. Scaling needs: Do different parts scale differently?
  4. Deployment needs: Do parts need independent deployment?
  5. DevOps maturity: Can we debug distributed failures?
  6. Technology needs: Do parts need different tech stacks?

If 4+ answers favor microservices, consider extraction. Otherwise, stay with monolith.

Summary

In this post, I shared my experience of choosing microservices too early and the six-month disaster it caused. I provided a decision framework based on real indicators: team size, domain clarity, scaling needs, deployment needs, DevOps maturity, and technology requirements.

The key point: Start with monolith (or modular monolith) and evolve to microservices only when specific benefits are clear and measurable.

The right architecture is the simplest one that solves your actual problems, not the one that matches industry trends.

I spent six months building infrastructure instead of features. Don’t make my mistake. Start simple, understand your domain, and extract services when real pain points emerge.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments