When Microservices Created More Problems Than They Solved

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.

Leave a Reply

Your email address will not be published. Required fields are marked *