Skip to content

Should You Start with Microservices or Build a Monolith First in Spring Boot?

Problem

I started a new Spring Boot project and immediately jumped into microservices. I thought that was the “modern” way to build applications. After three months, I had deployed zero features and spent all my time debugging distributed system issues.

The Reddit discussion I found later confirmed my mistake: experienced developers consistently warned against starting with microservices too soon. One comment hit home: “Don’t start with microservices too soon. It’s often not beneficial.”

I asked myself: Should I have started with a monolith instead?

The Misconception

I believed several myths that led to my wrong decision:

Myths That Led Me Astray
Myth 1: "Microservices = Modern Architecture"
→ Reality: Modern architecture solves actual problems, not follows trends
Myth 2: "Big Companies Use Microservices"
→ Reality: They have thousands of developers and mature DevOps. I had neither.
Myth 3: "Spring Cloud Makes Microservices Easy"
→ Reality: Spring Cloud simplifies implementation, not operational complexity
Myth 4: "Monolith = Spaghetti Code"
→ Reality: Modular monoliths exist and have clean boundaries

I wasn’t alone. Another Reddit comment captured the irony: “The project starts as a micro service but towards the end it becomes monolith.” This happens when teams create tightly coupled services - a distributed monolith that’s worse than either approach.

What I Built (The Wrong Way)

My first Spring Boot project structure looked like this:

My Premature Microservices Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User Service │────▶│ Order Service │────▶│ Product Service │
│ (Port 8081) │ │ (Port 8082) │ │ (Port 8083) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User DB │ │ Order DB │ │ Product DB │
│ PostgreSQL │ │ PostgreSQL │ │ PostgreSQL │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Infrastructure I had to set up:
- Eureka Server (Service Discovery)
- Spring Cloud Gateway (API Gateway)
- Spring Cloud Config (Configuration)
- RabbitMQ (Message Queue)
- Zipkin (Distributed Tracing)
- Kubernetes (Deployment)

This was for a simple e-commerce application with:

  • 2 developers
  • Unknown requirements (early startup)
  • Zero DevOps experience
  • Need to ship features fast

The services were tightly coupled. User Service couldn’t function without Order Service. Order Service called Product Service for every request. They all scaled together. They all deployed together. There was no real benefit to the separation.

The Cost of Premature Microservices

Time Spent on Infrastructure vs Features
Infrastructure Setup:
Eureka, Gateway, Config → 2 weeks
RabbitMQ Integration → 1 week
Distributed Tracing → 1 week
Docker + Kubernetes → 2 weeks
Debugging Network Issues → 3 weeks
Total: 9 weeks on infrastructure
Feature Development:
User Registration → 1 week (with debugging overhead)
Order Creation → 1 week (with consistency issues)
Product Listing → 1 week (with service discovery bugs)
Total: 3 weeks on actual features
Ratio: 75% infrastructure, 25% features

The Reddit comment that resonated: “Monolithic is very simple and easy to implement. Microservices are lengthy for implementation and maintenance perspective.”

I spent 2-3x more time on distributed systems concerns:

  • Network failures (services couldn’t reach each other)
  • Eventual consistency (data sync issues across databases)
  • Distributed transactions (no simple ACID anymore)
  • Debugging (stack traces across multiple services)
  • Service discovery (Eureka registration failures)

What I Should Have Built

The correct approach for my situation was a modular monolith:

Correct Modular Monolith Structure
src/main/java/com/example/app/
├── order/ // Order bounded context
│ ├── controller/
│ │ └── OrderController.java
│ ├── service/
│ │ └── OrderService.java
│ ├── repository/
│ │ └── OrderRepository.java
│ └── domain/
│ │ └── Order.java, OrderItem.java
├── inventory/ // Inventory bounded context
│ ├── controller/
│ ├── service/
│ ├── repository/
│ └── domain/
├── payment/ // Payment bounded context
│ ├── controller/
│ ├── service/
│ ├── repository/
│ └── domain/
├── user/ // User bounded context
│ └── ...
└── shared/ // Shared utilities
├── config/
│ └── SecurityConfig.java
└── exception/
│ └── GlobalExceptionHandler.java
└── Application.java // Single entry point
Single deployment unit
Single database
Clear module boundaries
Easy to understand and debug

This structure gives you:

  • Clear boundaries: Package by feature, not by layer
  • Simple debugging: Stack traces stay in one process
  • ACID transactions: No distributed transaction complexity
  • Fast iteration: Local method calls, no network latency
  • Easy extraction: Modules can become services when needed
Application.java
@SpringBootApplication
@ComponentScan(basePackages = "com.example.app")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// One JAR, one database, one deployment
// Internal modules with clean boundaries
// Ready to extract when scaling needs emerge

Module Boundaries in Spring Boot

The key to a successful monolith is enforcing boundaries. Spring Boot supports this through modular configuration:

order/config/OrderModuleConfig.java
@Configuration
@ComponentScan(basePackages = "com.example.app.order")
public class OrderModuleConfig {
// Explicit module boundary
// Only exposes what other modules need
}
// order/service/OrderService.java
@Service
public class OrderService {
private final InventoryClient inventoryClient;
// InventoryClient is an interface
// Implementation can be local (monolith) or remote (microservice)
// Module doesn't care - only the interface matters
public Order createOrder(CreateOrderRequest request) {
// Business logic focused on order domain
// Calls inventory through interface
// Transaction spans entire operation
}
}
inventory/client/InventoryClient.java
public interface InventoryClient {
boolean checkAvailability(String productId, int quantity);
void reserveStock(String productId, int quantity);
}
// In monolith: InventoryClientImpl is a local service
// In microservices: InventoryClientImpl makes HTTP calls
// OrderService doesn't know the difference

This pattern prepares you for future extraction without the complexity today.

When to Actually Extract

After running the monolith, I learned to watch for real indicators:

Indicator 1: Independent Scaling Needs

Scaling Signal
Metric: Payment Service handles 100x traffic at month end
Order Service has steady, low traffic
Before (Monolith):
Both services share same resources
Payment starves during peak times
Over-provisioning wastes resources
After (Extraction):
Payment Service scales independently
Dedicated resources during peak
Cost optimization possible

If a module needs 10x the resources of other modules, extraction becomes valuable.

Indicator 2: Different Deployment Frequency

Deployment Signal
Payment Service:
→ Regulatory changes every month
→ Needs immediate deployment
→ Can't wait for full application release
Order Service:
→ Changes quarterly
→ Stable, infrequent updates
Solution: Extract Payment for independent deployment cycles

Indicator 3: Dedicated Team Ownership

Team Signal
Organization grows:
→ 15+ developers across multiple domains
→ Payment team forms (5 developers)
→ Order team forms (5 developers)
→ Product team forms (5 developers)
Problem: Single monolith deployment creates bottlenecks
Solution: Extract modules into team-owned services

Indicator 4: Compliance or Security Isolation

Compliance Signal
Payment processing requires PCI-DSS compliance.
Keeping it in monolith means:
→ Entire application under strict controls
→ More audit scope
→ Higher compliance cost
Solution: Extract Payment to isolate compliance scope

Decision Matrix

Monolith vs Microservices Decision Matrix
┌─────────────────────────┬───────────────────┬────────────────────┐
│ Factor │ Start Monolith │ Start Microservices│
├─────────────────────────┼───────────────────┼────────────────────┤
│ Team Size │ < 20 developers │ > 20 developers │
│ Deployment Frequency │ Daily/Weekly │ Multiple per day │
│ Scaling Needs │ Uniform │ Varied by module │
│ Domain Clarity │ Unknown │ Well-defined │
│ DevOps Maturity │ Basic │ Advanced │
│ Time to Market │ Critical │ Can wait │
│ Compliance Isolation │ Not needed │ Required │
└─────────────────────────┴───────────────────┴────────────────────┘
If 5+ factors favor microservices, consider starting with them.
Otherwise, start with monolith.

Common Mistakes I Made

Wrong Approach
"Netflix uses microservices" → "We should too"
Questions I didn't ask:
→ How many developers does Netflix have? (Thousands)
→ What's their DevOps maturity? (Very high)
→ What problems do microservices solve for them? (Independent scaling, team autonomy)
My situation:
→ 2 developers
→ Zero DevOps experience
→ Unknown domain boundaries
→ No scaling requirements yet

Mistake 2: Assuming Monolith Means Spaghetti

Wrong Assumption
I thought: "Monolith = Big ball of mud"
Reality: "Modular Monolith = Clean boundaries without distributed complexity"
Package by feature, not by layer:
→ order/ package contains all order-related code
→ inventory/ package contains all inventory-related code
→ Modules are loosely coupled within single deployable unit

Mistake 3: Not Having DevOps Capabilities

Missing Infrastructure
Microservices require:
✓ CI/CD for each service
✓ Monitoring and alerting
✓ Service discovery debugging
✓ Distributed tracing
✓ Container orchestration (Kubernetes)
✓ Network failure handling
✓ Circuit breakers
✓ Load balancing
I had:
✗ Basic CI only
✗ No monitoring
✗ No tracing experience
✗ No Kubernetes knowledge
Result: Debugging nightmares

Mistake 4: Creating Distributed Monolith

Anti-pattern
My services were tightly coupled:
→ Order Service called Product Service synchronously
→ User Service required Order Service to function
→ All services deployed together anyway
→ No independent scaling benefit
This is worse than a real monolith:
→ Same coupling as monolith
→ Plus network latency
→ Plus failure modes
→ Plus operational complexity

Mistake 5: Underestimating Operational Overhead

Hidden Complexity
Every microservice adds:
+ One deployment pipeline
+ One monitoring dashboard
+ One log aggregation stream
+ One service discovery registration
+ One set of network failure modes
With 5 services, I had 5x operational overhead.
For 2 developers, this was unsustainable.

The Correct Path

Based on my experience, here’s the process I now follow:

Step 1: Assess Your Situation

Assessment Questions
1. Team size: < 20 developers? → Monolith
2. Domain clarity: Unknown boundaries? → Monolith
3. Scaling needs: Uniform across modules? → Monolith
4. Deployment needs: Single deployment unit? → Monolith
5. DevOps maturity: Basic only? → Monolith
6. Time to market: Critical? → Monolith
If 4+ answers favor monolith, start there.

Step 2: Structure for Growth

Modular Structure
src/main/java/com/example/
├── domain1/ // Bounded context 1
│ ├── api/ // Public interfaces
│ ├── application/ // Use cases
│ ├── domain/ // Domain model
│ └── infrastructure/// Repository implementations
├── domain2/ // Bounded context 2
└── shared/ // Shared kernel
// Package by feature
// Define interfaces between modules
// Keep modules loosely coupled

Step 3: Wait for Signals

Extraction Signals
Watch for:
→ Specific module needs 10x scaling
→ Different team forms for a module
→ Different deployment frequency needed
→ Compliance isolation required
→ Different technology stack needed
Don't extract until these signals appear.

Step 4: Extract When Ready

Extraction Checklist
Before extracting a module:
[ ] Clear API boundaries defined
[ ] Team has DevOps maturity
[ ] Scaling benefit is measurable
[ ] Independent deployment is justified
[ ] Circuit breakers implemented
[ ] Monitoring and tracing ready
[ ] Database separation planned

Why This Matters

The Reddit discussion included this insight: “I’ve done both. Depends entirely on the requirements and project.”

But another comment clarified: “Our org focuses more on building out reusable micro services.” This organization had mature DevOps and clear service ownership.

For most Spring Boot projects, the reality is:

Reality Check
Premature Microservices Cost:
→ 2-3x development time for infrastructure
→ Distributed debugging complexity
→ Network failure modes
→ Data consistency challenges
→ Operational overhead per service
Monolith Benefits:
→ Focus on business logic first
→ Simple stack traces for debugging
→ ACID transactions without complexity
→ Lower infrastructure cost
→ Faster time to market

The evolution path is clear: A well-modularized monolith can extract services incrementally. A poorly-designed microservices system is hard to consolidate.

If you’re considering this decision, also understand:

  • Bounded Contexts: From Domain-Driven Design, defines clear module boundaries
  • Circuit Breakers: Pattern for handling remote service failures
  • Event Sourcing: Alternative to distributed transactions
  • API Gateway: Entry point for microservices, adds latency
  • Service Discovery: Required for dynamic microservices, adds complexity
  • Distributed Tracing: Essential for debugging across services

Learn these concepts before adopting microservices, not after.

Summary

I learned that starting with a modular monolith in Spring Boot is the right choice for most projects. Design clean boundaries within the monolith, and extract microservices only when specific scaling or organizational needs emerge.

The key principles:

  1. Assess your actual requirements - not industry trends
  2. Structure your monolith for growth - package by feature, define interfaces
  3. Watch for extraction signals - scaling, teams, deployment, compliance
  4. Extract when benefits are measurable - not when architecture feels “modern”

You can always split a monolith into services later. But you cannot easily undo the complexity of premature microservices.

As one experienced developer put it: “Start simple, evolve when needed.” This advice saved my next project from repeating the same mistake.

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