When we first split our monolith into microservices, it felt like a victory.
Smaller services. Independent deployments. Clean boundaries. We even had a diagram with boxes and arrows that made us feel like Netflix engineers.
Then production traffic hit.
The services scaled fine.
Kubernetes was happy.
Auto-scaling worked exactly as advertised.
And the database absolutely melted.
At first, we blamed everything except the obvious.
“Maybe we need more replicas.”
“Let’s increase the connection pool.”
“Postgres just doesn’t scale like NoSQL.”
But the truth was simpler and more uncomfortable:
Our microservices weren’t the problem.
Our shared database was.
Microservices Make Scaling Look Easy (Until It Isn’t)
Microservices sell a seductive idea: scale each part of your system independently. In theory, that’s exactly what you get.
In practice, most teams do this:
- Split the app into 10–20 services
- Point all of them at the same database
- Call it “microservices architecture”
Congratulations.
You’ve just built a distributed monolith with network latency.
Each service may scale horizontally, but every single one still funnels its traffic into the same bottleneck. When load increases, the database doesn’t see “microservices.” It sees chaos.
More connections
More concurrent queries
More locks
More contention
The database doesn’t scale horizontally just because your services do. It just cries louder.
The Hidden Cost of “Just One More Query”
The first cracks showed up as latency spikes.
No errors. No crashes. Just requests getting slower… and slower… and slower.
Here’s what was really happening:
- Service A added a new endpoint → +3 queries
- Service B added “just a join” → +2 queries
- Service C started polling every 5 seconds → oops
- Read replicas lagged behind writes
- Connection pools maxed out
- Locks piled up in places no one was monitoring
Individually, none of these changes looked dangerous.
Together, they turned the database into a shared trauma center.
This is the microservices trap: local decisions with global consequences.
Scaling Services Multiplies Database Pain
Here’s the part that surprised newer engineers on the team.
Scaling a service from 2 pods to 20 pods doesn’t just multiply throughput. It multiplies:
- Open connections
- Idle transactions
- Concurrent writes
- Cache misses
- Lock contention
The database doesn’t know these pods belong to the same service. It treats them as 18 new strangers aggressively asking for attention.
So while your dashboards show:
“Service latency looks fine!”
The database is over here thinking:
“WHY ARE THERE SO MANY OF YOU?”
Why “Add a Cache” Usually Isn’t Enough
At this point, someone always suggests caching.
And yes, caching helps.
But it doesn’t fix the underlying issue.
Most teams add:
- Redis for reads
- Maybe some HTTP caching
- A TTL they picked emotionally
Now the system is faster… until it isn’t.
Why?
Because:
- Writes still hit the same database
- Cache invalidation gets messy fast
- Cross-service data consistency becomes a guessing game
- You’ve added operational complexity without removing coupling
Caching is a painkiller.
The database problem is structural.
The Real Problem: Shared Ownership of Data
The moment it clicked for me was realizing this:
We didn’t have microservices.
We had microservices sharing the same state.
That breaks the core promise of the architecture.
When multiple services:
- Read the same tables
- Write to the same rows
- Depend on the same transactions
They are no longer independent. They’re tightly coupled through the database, just in a quieter, harder-to-debug way.
Your services can deploy independently.
Your data cannot.
What Actually Helped (And What Didn’t)
Here’s what didn’t solve it:
- Bigger database instance
- More replicas
- Higher connection limits
- Shouting “optimize queries” in standups
Here’s what did help:
1. Clear Data Ownership
Each service owns its data. Period.
If another service needs it:
- It calls an API
- Or consumes an event
- Or reads from a purpose-built read model
No “just this one join across services.” That’s how the crying starts again.
2. Fewer Cross-Service Transactions
Distributed transactions feel elegant until you try to operate them.
We replaced synchronous dependencies with:
- Events
- Async workflows
- Eventually consistent updates
Not everything needs to be instant. Most systems just need to be reliable.
3. Designing for Database Load First
We stopped asking:
“Can this service scale?”
And started asking:
“What does this do to the database at 10x traffic?”
That one question changed architecture reviews completely.
4. Accepting That Microservices Are a Tradeoff
Microservices don’t automatically give you scalability. They give you options — at the cost of discipline.
Without strict boundaries, they amplify database problems instead of solving them.
The Hard Lesson
Microservices didn’t fail us.
Our database design did.
We optimized for developer velocity early on and paid for it later with operational pain. That’s not a mistake — that’s a tradeoff. The mistake is pretending microservices magically remove scaling limits.
They don’t.
They move those limits somewhere less visible.
Usually into your database.
Final Thoughts
If your system slows down every time traffic increases, don’t just look at your services.
Look at:
- Who owns the data
- How many services touch the same tables
- How scaling pods multiplies database load
- Whether your architecture matches your traffic patterns
Because nine times out of ten, when “microservices don’t scale”…
They do.
Your database is just crying for help.
Top comments (10)
Governance is key in a distributed architecture. But for me the most important one before going for it is to have a good knowledge of the domain. Without it, you will likely end up having a distributed monolith even if each microservice has its own database and clear governance.
Good read, thanks for sharing!
Absolutely agree — strong domain understanding is what makes governance effective. Without clear domain boundaries, you’re just distributing complexity and calling it microservices.
This hits because it names the part everyone avoids saying out loud.
Most “microservices scalability” stories quietly assume the database will somehow absorb the blast radius. It won’t. All you’ve really done is turn vertical pressure into horizontal chaos and aim it at a single shared state.
The line about local decisions with global consequences is the real lesson here. A service adding “just one more query” feels harmless in isolation, but at scale it’s indistinguishable from coordinated abuse. The database doesn’t see services or intent — it sees contention.
What I appreciate most is the framing around data ownership. That’s the actual boundary, not deployment units. If two services write to the same tables, they’re coupled whether you admit it or not. The coupling just moved somewhere harder to observe.
Also important callout on caching: it masks symptoms, it doesn’t fix structure. If your write path is still shared, you’ve only delayed the pain.
This is one of those posts that should be required reading before anyone says “let’s break it into microservices.” Not as a warning against them — but as a reminder that scalability starts with data, not pods.
This really resonates because it clearly calls out the part most teams quietly work around instead of addressing head-on. I like how you frame the database as the true pressure point — it’s a strong reminder that architecture choices don’t disappear just because we distribute services. The point about local decisions turning into global consequences is especially sharp, and it mirrors what I’ve seen in real systems under load. Your take on data ownership being the real boundary feels like the right mental model, and it makes the coupling visible instead of hidden behind deployments. Posts like this don’t argue against microservices — they help people approach them with the right expectations and a much healthier starting point.
YESSSS decoupling data is a problem I see so many young companies run into. They just want to move faster and faster and after 2-3 years they start going "wait why is that grid taking 30 seconds to load...."
Exactly — early speed without data boundaries turns into invisible coupling, and the bill always comes due in latency and complexity.
While there is a redemption arc in the post, the developer that thinks it is a good idea to use a single database doesn't understand modularity or has just enough knowledge to be dangerous.
When the application is monolith it is possible to keep domains separate in a single database.
It is also possible to create cross domain calls.
And when one or more domains become a bottleneck, then it is time to start to move them out of the monolith.
This flow is the easiest part of scaling.
I really appreciate your breakdown—it makes the scaling journey much clearer! I agree that starting with a single database can work if domains are well-separated, and it’s smart to wait until a bottleneck appears before splitting things out. In my experience, modularity is more about how you structure code and interactions than the number of databases, so your point resonates. I’m curious how you handle cross-domain calls in practice without introducing too much coupling. Overall, this approach feels practical, and I’m excited to try it in a project and see how it scales.
Create a public domain API and only use that API. The thing you do to let microservices communicate, you can do in a monolith.
By treating your application like it already is split into microservices, you will understand the domain communications before the time comes you need to scale.
Great point — that mindset shows real architectural maturity. Designing clear domain APIs early builds understanding and discipline, so scaling later becomes an evolution, not a rewrite.