The real cost of splitting too early
We've all heard the story: Netflix started as a monolith, then moved to microservices and changed the world. What we don't hear as often is how many teams split their systems prematurely and spent the next two years fighting distributed systems problems that didn't exist before. Spring Boot microservices: when to split for production isn't a question of architecture purity—it's a question of whether the benefits of separation exceed the operational and organizational costs. The truth is simpler than the hype suggests: most teams should stay monolithic longer than they think.
We've worked on systems that split at 20 engineers and systems that stayed unified at 200. The difference wasn't technology—it was whether the pain of staying together was greater than the pain of splitting apart.
Start with a modular monolith — your first move
Before we talk about microservices, let's be honest: a well-designed monolith solves most of the problems you think you're solving by splitting. A modular monolith is a single deployable jar file with clear internal boundaries between domains. It gives you 80% of the benefits of microservices—independent development on separate teams, logical isolation, testability—without the operational tax.
Here's what a modular monolith looks like in a Spring Boot application:
// Package structure for a modular monolith
com.example.ecommerce
├── orders/
│ ├── OrderController.java
│ ├── OrderService.java
│ ├── OrderRepository.java
│ └── OrderEvent.java (published to internal event bus)
├── inventory/
│ ├── InventoryController.java
│ ├── InventoryService.java
│ └── InventoryRepository.java
├── payments/
│ ├── PaymentController.java
│ ├── PaymentService.java
│ └── PaymentRepository.java
└── shared/
├── events/EventBus.java
├── config/DatabaseConfig.java
└── exceptions/DomainException.javaEach module has its own controller, service, and repository layer. Modules communicate through domain events published to a local event bus—Spring's ApplicationEventPublisher works perfectly for this. There's no HTTP or gRPC overhead. Deployment is a single artifact. Testing is straightforward because you can integration-test multiple modules together.
The ordering module publishes an OrderCreatedEvent. The inventory module subscribes to it and updates stock. The payments module subscribes and charges the card. All in the same process, with local transactions where possible and eventual consistency where needed. This is clean, fast, and debuggable.
Why wait? Because splitting into separate services means you lose this simplicity immediately. Every call becomes a network call. Every transaction becomes distributed. Debugging requires tracing across services. Deployment becomes choreography. Stay modular monolithic as long as the single deployable works for your team size and release cadence.
Split for real reasons — not architecture religion
There are exactly three reasons we see justify splitting a monolith into microservices in production. Not five. Not ten. Three.
Independent scaling
You've measured production and discovered that your payment processing service needs 10x more capacity than your user settings service. One team runs hot all the time; the other is idle. Kubernetes autoscaling won't help if you're scaling a monolith—you scale the whole thing together. If this is your bottleneck, split the payment service to its own deployment so it can scale independently with its own database and resource limits.
Team ownership and independent development
You have 50 engineers and six teams. They step on each other's toes constantly. Teams can't deploy during their own working hours because deployment affects everyone. The monolith has become a coordination problem. If you split services along team boundaries—payments owned by one team, orders by another, subscriptions by a third—teams can deploy independently, own their infrastructure, and move at their own speed.
Different release cadence or technology choices
Your core ordering system is stable and deployed once a month. Your recommendation engine is experimental and deployed fifty times a week. Keeping them together slows down the recommendation team and adds risk to the core system. Splitting lets each team choose their deployment frequency. Similarly, if one service genuinely needs a different technology stack (a machine learning pipeline in Python, for example), that's a legitimate reason to separate it—but only after you've proven it can't be solved with a Spring Boot embedded service or async worker.
Notice what's not on this list: "microservices are cool," "it's the industry standard," or "we want to be cloud-native." Those are not reasons to split.
Own your data — this is non-negotiable
If you do split your monolith into microservices, there's one rule that's harder than any other to enforce in production: each microservice owns its database. No shared tables.
This is where we see the most damage in real systems. Teams start with the best intentions—"we'll just have orders and payments share a customer table for convenience"—and suddenly you've recreated the monolith's coupling inside a distributed system. Now you can't scale payments independently because it's locked to orders' schema. You can't deploy orders without coordinating with payments. You've added all the complexity of microservices without any of the benefits.
Instead, each service owns its data:
| Service | Owns this data | Consumes from other services via |
| Orders | orders, order_items, order_status_history | HTTP calls to get current customer data |
| Payments | payments, payment_methods, transaction_logs | Events from orders, HTTP calls to orders |
| Inventory | products, stock_levels, reservations | Events from orders |
| Customer | customers, customer_profiles, addresses | None—owns the source of truth for customer data |
If the payments service needs customer information, it calls the customer service's REST API. If payments needs to react to an order being placed, it subscribes to the order service's OrderCreatedEvent published to Kafka or RabbitMQ. This separation is uncomfortable at first—queries that used to be a single join are now two service calls—but it's what makes independent deployments and scaling possible.
The inverse is also true: if you find yourself unable to separate databases because too much data needs to be shared, that's a signal you're not ready to split yet. Go back to the modular monolith. There's no shame in that.
Budget for operations — the real tax of microservices
Here's what nobody tells junior engineers: the operational cost of microservices isn't paid in code. It's paid in debugging, deployments, and on-call rotations.
A monolith with one database and one deployment target is operationally cheap. A request comes in, hits your code, hits the database, returns a response. Something goes wrong, you look at one log file. One team owns the production environment.
Five microservices with five databases and five deployment pipelines costs you:
- Distributed tracing: You need Jaeger or Datadog to track a request across services. That's infrastructure to run and operators to maintain.
- Contract testing: When the orders service changes its API response, does the payments service still work? Integration tests in a monolith become contract tests between services—more tests, more CI time, more brittleness.
- Service mesh or observability: With five services talking to each other, you need visibility into all those calls. Istio or Linkerd (infrastructure), or extensive custom instrumentation.
- Deployment complexity: One monolith deploy is one CI/CD pipeline. Five services are five pipelines, five rollback procedures, five canary deployments. If they're interdependent, you need to orchestrate them.
- On-call tax: With one monolith, one engineer can own production. With five services, you need specialists who understand each service, or generalists who understand five times more. Incident response gets slower, not faster.
- Local development: Running five services locally means Docker Compose with five containers, longer startup times, harder debugging. Your development velocity decreases.
We've seen teams discover this in production at 2 AM when a cascading failure across three services brought the whole system down, and they spent eight hours debugging distributed timeouts instead of one-hour debugging a single monolith.
Budget for this tax. If your team can't operate five services—meaning you have the monitoring, the runbooks, the on-call rotation, the deployment automation—don't split into five services.
Common mistakes
Splitting before you measure. A common pattern: "We're going to split payments into its own service because payments is critical." Then you discover payments is 2% of your infrastructure cost and never scales independently. Measure first. Profile in production. Let the data tell you where the real bottlenecks are.
Creating a "shared data service." Teams often create a service that owns all the shared data—customers, products, configurations. Everything else depends on it. This becomes your bottleneck. You've created a distributed monolith where the worst possible service is now remote.
Treating microservices as a scaling solution without redesigning. Splitting a poorly-designed monolith into microservices doesn't make it better. If your code is slow because of N+1 queries, that doesn't change. You just move the problem into network calls. Redesign the monolith first, then split if you still need to.
Skipping the modular monolith phase. Teams sometimes go straight from a ball of mud to microservices thinking the split will force structure. It won't. You'll just have a distributed ball of mud. Build a well-structured monolith first. Then, if you need to split, you'll know what clean separation looks like.
Key takeaways
- Start with a modular monolith. A well-organized single deployable gives you 80% of microservices benefits with 20% of the operational cost.
- Split only when you have a real reason: independent scaling, team ownership, or different release cadence. Not for architecture elegance.
- Enforce strict data ownership. Each service owns its database. Shared tables are a critical smell.
- Count the operational tax: distributed tracing, contract testing, deployment orchestration, and on-call burden all multiply with each service. Make sure your team can actually operate what you're building.
- Measure before splitting. Use profiling and production metrics to prove you need the complexity.






Comments
Loading comments…