The monolith had become difficult to work with.
Deployments were getting larger.
Teams were stepping on each other.
Some parts of the system needed to scale independently.
A small change in one area could require deploying the entire application.
The obvious architectural answer seemed clear:
Split the monolith into microservices.
So we did.
At first, everything looked better.
Services were smaller.
Teams had clearer ownership.
Deployments became more targeted.
Some workloads could scale independently.
The architecture diagram looked more modern.
Then production taught us something the architecture diagram didn’t show.
We had reduced the size of our applications.
But we had increased the complexity of the system.
Suddenly we were dealing with:
- Network failures
- Distributed transactions
- Service-to-service latency
- Deployment coordination
- Service discovery
- Distributed debugging
- Duplicated data
- Eventual consistency
- More operational overhead
The monolith had problems.
But some of those problems had been hidden by the fact that everything was running inside the same process.
We had traded local complexity for distributed complexity.
And some of the new complexity was much harder to reason about.
1. Why We Chose Microservices
The decision wasn’t irrational.
The monolith had grown significantly.
Different business capabilities had accumulated inside the same application.
Conceptually:
Monolith
│
┌───────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Payments Accounts Reporting
│ │ │
└───────────────┼────────────────┘
│
Database
The problems were becoming visible.
A change to payments could require a full application deployment.
A reporting workload could consume resources needed by customer-facing operations.
Different teams had different release schedules.
Testing the entire application became increasingly expensive.
We wanted more independence.
So we started decomposing.
2. The Architecture Looked Better on Paper
The first decomposition looked attractive:
API Gateway
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Payment Service Account Service Reporting Service
│ │ │
▼ ▼ ▼
Payment DB Account DB Reporting DB
Each service had:
- Its own codebase
- Its own deployment
- Its own ownership
- Its own scaling characteristics
- Its own data boundary
This was supposed to give us independence.
And in some areas, it did.
But we underestimated one thing:
A distributed system creates a new class of problems that a monolith doesn’t have.
Inside a monolith, a function call is usually just a function call.
Across services, it becomes:
Request
↓
Network
↓
Service
↓
Network
↓
Another Service
Every network boundary introduces uncertainty.
3. The First Problem: Network Calls Are Not Function Calls
Inside the monolith, we could do:
paymentService.processPayment()
The call either succeeded or threw an exception.
After decomposition:
Payment Service
│
▼
Account Service
Now the call could fail because:
- The service was unavailable
- The network was slow
- DNS failed
- The connection timed out
- The remote service overloaded
- A load balancer failed
- The request was duplicated
- The response was lost
The business operation hadn’t changed.
The failure model had.
This was one of the first lessons we learned:
A service boundary is also a failure boundary.
4. Latency Became Part of the Architecture
The monolith had relatively predictable internal calls.
After decomposition, one request could involve multiple network calls.
For example:
Customer Request
│
▼
Payment Service
│
├────► Account Service
│
├────► Fraud Service
│
└────► Ledger Service
Suppose each call normally takes:
Account Service 20 ms
Fraud Service 30 ms
Ledger Service 25 ms
The total might still look reasonable.
But under load, one dependency could become slower.
Now:
Account Service 20 ms
Fraud Service 30 ms
Ledger Service 300 ms
The payment request inherits that delay.
The application didn’t become slower because its own code changed.
It became slower because the request crossed more boundaries.
5. The Second Problem: Distributed Transactions
This was more serious.
Inside the monolith, a business transaction could sometimes be protected by one database transaction.
For example:
BEGIN TRANSACTION
Create Payment
Update Account
Create Ledger Entry
COMMIT
If something failed:
ROLLBACK
The entire operation could be atomic.
After decomposition:
Payment Service
│
▼
Account Service
│
▼
Ledger Service
Now each service potentially owns a different database.
The original transaction boundary no longer exists.
What happens if:
Payment ✓
Account ✓
Ledger ✗
We can’t simply roll back all three with a local database transaction.
We now have a distributed consistency problem.
6. We Discovered the Distributed Monolith
The most uncomfortable realization was that some of our new services weren’t actually independent.
They were just separate deployments with tight runtime dependencies.
For example:
Service A
│
├── requires Service B
│
└── requires Service C
Service B
│
└── requires Service D
Service C
│
└── requires Service D
Technically:
Four services.
Operationally:
One tightly coupled system.
If Service D was unavailable, several supposedly independent services were affected.
We had created what is often called a distributed monolith.
The code was distributed.
The dependency graph wasn’t.
7. The Third Problem: Deployment Coordination
One of the reasons we wanted microservices was independent deployment.
But independence only works when contracts are actually independent.
Suppose Service A expects:
customer_id
and Service B changes the API to:
customerId
We now have a compatibility problem.
If both services need to be deployed simultaneously, we have recreated the coordination problem we were trying to escape.
The architecture became:
Service A
│
│ old contract
▼
Service B
│
│ new contract
▼
Failure
The solution was backward-compatible evolution.
For example:
Old Client
│
▼
API
│
├── old field
└── new field
We learned to introduce changes gradually rather than assuming every service could evolve independently.
8. Service Discovery Became Infrastructure
In the monolith, there was no service discovery problem.
Everything was already inside the application.
With microservices:
Payment Service
│
▼
Where is Account Service?
We needed mechanisms for:
- Service registration
- Discovery
- Health checks
- Load balancing
- Routing
- Failure detection
None of these created customer value directly.
But all of them became necessary because we had chosen distributed deployment.
This is an important trade-off.
Microservices don’t eliminate complexity.
They often move complexity into the platform.
9. Debugging Became Much Harder
This was one of the biggest operational changes.
Inside the monolith, a request could often be traced through one process.
After decomposition:
Request
│
▼
Gateway
│
▼
Payment
│
▼
Account
│
▼
Fraud
│
▼
Ledger
When something became slow, the question changed from:
“Which function is slow?”
to:
“Which service, dependency, network hop, or data boundary is responsible?”
This is why observability became essential.
We needed:
- Correlation IDs
- Distributed tracing
- Structured logging
- Service-level metrics
- Dependency metrics
- Business-level metrics
This connected directly to what we learned in our observability work.
Distributed architecture requires distributed observability.
10. Data Duplication Became a Design Decision
Another challenge was data ownership.
In the monolith, multiple capabilities could access the same database.
After decomposition, we wanted services to own their data.
That created a question:
Payment Service
│
▼
Payment DB
Account Service
│
▼
Account DB
But what if Payment Service needs account information?
We had several choices.
Call Account Service.
Replicate the required data.
Publish events.
Create a read model.
Share the database.
Each choice introduces different trade-offs.
The important realization was:
Separating services doesn’t eliminate data dependencies. It makes those dependencies explicit.
11. The Shared Database Temptation
At one point, the easiest solution seemed obvious.
Just let the services use the same database.
That would make cross-service queries easier.
But it created another problem.
If multiple services directly modify the same tables:
Payment Service ──┐
├──► Shared Database
Account Service ──┤
│
Ledger Service ───┘
Who owns the data?
Who can change the schema?
Who is responsible for performance?
Who can add an index?
Who can change a constraint?
We had technically separated the application while keeping a major coupling point underneath it.
The database had become the shared boundary again.
12. We Revisited the Service Boundaries
Eventually, we stopped asking:
“How many services should we have?”
and started asking:
“What boundaries actually make sense?”
This was a much better question.
We evaluated services based on:
- Business capability
- Data ownership
- Change frequency
- Scaling characteristics
- Failure isolation
- Team ownership
- Transaction boundaries
- Deployment independence
Some boundaries made sense.
Others didn’t.
13. Some Services Were Too Small
We discovered that not every small component needed to become a service.
For example:
Service A
│
└── always calls Service B
│
└── always calls Service C
If the three components:
- Changed together
- Deployed together
- Scaled together
- Failed together
- Had tightly coupled data
then separating them didn’t provide meaningful independence.
It simply created more network boundaries.
We consolidated some of those components.
14. Some Services Were Too Large
The opposite problem also existed.
Some services had accumulated unrelated responsibilities.
For example:
Customer Service
├── Profiles
├── Notifications
├── Preferences
├── Reporting
└── Billing
These capabilities changed for different reasons.
They had different owners.
They had different scaling requirements.
They were candidates for more deliberate boundaries.
The goal wasn’t:
Smallest possible service.
The goal was:
Useful boundary.
15. We Changed How We Defined a Service
A service became a candidate for separation when several characteristics aligned.
For example:
Business Capability
+
Clear Ownership
+
Independent Change
+
Independent Scaling
+
Meaningful Failure Boundary
+
Clear Data Ownership
When those characteristics were absent, we became much more cautious about creating another service.
This dramatically improved the quality of our decomposition decisions.
16. We Introduced Explicit Failure Boundaries
Once services were genuinely independent, we had to design for failure.
For synchronous calls, we introduced appropriate controls such as:
- Timeouts
- Retries where safe
- Circuit breakers
- Bulkheads
- Rate limits
- Idempotency
But we also became more selective.
Not every failure should trigger a retry.
Not every dependency should block the entire request.
Not every capability needs synchronous execution.
The question became:
What should happen when this dependency is unavailable?
That question belongs in the architecture.
17. We Moved Some Work Asynchronously
Some operations didn’t need to happen during the customer request.
For example:
Payment
│
├── Ledger update
│
└── Notification
If notification didn’t need to determine whether the payment succeeded, it didn’t necessarily need to be part of the synchronous path.
We could use:
Payment Completed
│
▼
Event
│
├──► Notification
├──► Analytics
└──► Other Consumers
This reduced synchronous coupling.
But it introduced another trade-off:
eventual consistency.
We had already learned that asynchronous architecture can improve scalability while making consistency and failure handling more complicated.
The pattern repeated.
18. The Result Wasn’t “Fewer Services”
Interestingly, the final architecture wasn’t successful because we minimized the number of services.
We still had multiple services.
The difference was that the boundaries were more intentional.
Instead of:
Everything Must Be a Service
we moved toward:
Business Capabilities
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Service A Service B Modular Area
│ │ │
Clear Owner Clear Owner Internal Modules
Some capabilities needed independent deployment.
Some didn’t.
Some needed independent scaling.
Some didn’t.
Architecture became a decision rather than a fashion.
19. What Microservices Actually Gave Us
It would be wrong to conclude that microservices were a mistake.
They solved real problems.
We gained:
Independent scaling
A high-volume capability could scale without scaling everything else.
Independent deployment
Well-designed services could be released without redeploying the entire system.
Clearer ownership
Teams could own business capabilities end-to-end.
Failure isolation
A carefully designed service could fail without taking down unrelated capabilities.
Technology flexibility
Different components could evolve independently when there was a legitimate reason.
Those benefits were real.
The mistake was assuming that they came without a cost.
20. What Microservices Cost Us
We also had to pay for:
- Network communication
- Operational complexity
- Distributed tracing
- Service discovery
- Contract management
- Distributed consistency
- Deployment coordination
- Data synchronization
- More infrastructure
- More failure modes
The architecture had become more flexible.
It had also become more expensive to operate.
That is the fundamental trade-off.
21. The Architecture Decision We Should Have Made Earlier
Looking back, the mistake wasn’t choosing microservices.
The mistake was treating decomposition as an architectural destination.
We thought:
Monolith
↓
Microservices
↓
Better Architecture
The reality was:
Monolith
↓
Architectural Problems
↓
Evaluate Boundaries
↓
Selective Decomposition
↓
Choose Appropriate Architecture
Sometimes the right answer is a microservice.
Sometimes it is a module.
Sometimes it is an asynchronous workflow.
Sometimes it is a shared capability.
Sometimes it is leaving the existing design alone.
22. The Questions I Ask Before Creating a Service Now
Before splitting something into a new service, I ask:
Does this represent a meaningful business capability?
Then:
Does it have clear data ownership?
Then:
Does it need to scale independently?
Then:
Does it need to deploy independently?
Then:
Does separating it create a useful failure boundary?
And finally:
Will the new network boundary reduce complexity or simply move it somewhere else?
If the answers aren’t convincing, we don’t create the service.
23. The Bigger Architectural Lesson
Microservices aren’t an architecture upgrade.
They’re an architectural trade.
You exchange some types of complexity for others.
MONOLITH
│
┌─────────┴─────────┐
│ │
▼ ▼
Local Complexity Deployment Coupling
↓
MICROSERVICES
│
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
Network Data Operational
Failures Consistency Complexity
Neither architecture is universally better.
The right architecture depends on:
- Business boundaries
- Team structure
- Scale
- Change patterns
- Reliability requirements
- Data ownership
- Operational maturity
The architecture must fit the problem.
Not the other way around.
24. Final Thought
We started the journey believing the monolith was the problem.
After decomposing it, we discovered something more nuanced.
The monolith wasn’t inherently wrong.
Neither were microservices inherently right.
The real problem was boundaries that didn’t match the way the system actually behaved.
Some boundaries created independence.
Others created network calls.
Some reduced deployment coupling.
Others created coordination.
Some isolated failures.
Others simply moved the failure somewhere else.
That changed how we thought about architecture.
The question was no longer:
“Should we use microservices?”
It became:
“Where does a service boundary create more value than complexity?”
That’s a much better architectural question.
Microservices are a trade-off, not an upgrade.