Why Microservices do not solve every problem

For several years, microservices have often been presented as an almost miraculous solution to the problems of applications that become too large.

The idea seems attractive: splitting a large application into small independent services, allowing each team to work separately, deploying each part without affecting the rest, and choosing the most appropriate technology for each need.

On paper, this seems like a natural evolution compared to monolithic applications. But in practice, microservices often move problems rather than completely remove them. They introduce new challenges, mainly related to distributing an application across multiple systems.

We will look at some of the problems related to microservices.

Network complexity

In a monolithic application, a call between two components is usually just a function call. Everything happens within the same process. But with microservices, each communication now goes through the network. And the network is not reliable.

A call can fail because of:

  • server unavailable;
  • DNS issue;
  • high latency;
  • timeout;
  • temporary network problem.

A classic function call:

result = calculate_price(product)

becomes:

Send a network request
        ↓
Wait for a response
        ↓
Handle errors
        ↓
Handle timeouts
        ↓
Handle retries

Communication between services therefore becomes an additional source of complexity and can potentially impact performance.

Multiple deployments

One of the main advantages often mentioned for microservices is the ability to deploy each service independently. This is indeed an advantage, but it also adds complexity.

In a monolithic application:

Version 1.5
    ↓
Deployment
    ↓
Complete application updated

With microservices:

Service A: version 2.1
Service B: version 3.4
Service C: version 1.8
Service D: version 5.0

You need to manage version compatibility, API changes, progressive migrations, and partial deployments. Changing an API used by several services often requires a transition period.

For example:

Old API
    ↓
New API available
    ↓
Client migration
    ↓
Removal of the old version

This process is more complex than a simple change in a single application. Everything must be carefully planned.

Monitoring becomes essential

In a monolithic application, when a user encounters an error, it is often possible to easily follow the execution path.

With microservices:

User
    ↓
API Gateway
    ↓
User service
    ↓
Order service
    ↓
Payment service
    ↓
Database

Where is the problem?

  • in the API Gateway?
  • in the user service?
  • in the network?
  • in the payment service?
  • in the database?

Classic logs are not always enough anymore. It becomes necessary to use centralized logs, cross-service correlation IDs, distributed tracing, and metrics for each service. Without these tools, diagnosing a problem can become a real nightmare.

Distributed transactions

In a monolithic application, a transaction is often simple:

1. Create the order
2. Reduce the stock
3. Process the payment

With a single database:

BEGIN;

INSERT INTO orders (...);

UPDATE products
SET stock = stock - 1;

COMMIT;

In case of a problem:

ROLLBACK;

Everything is cancelled.

With microservices, each service often has its own database:

Order service
        ↓
Orders database


Stock service
        ↓
Stock database


Payment service
        ↓
Payments database

There is no longer a simple global transaction. What happens if the order is created, the stock is reduced, but the payment fails?

More complex mechanisms are required:

  • Saga pattern;
  • compensating transactions;
  • events;
  • eventual consistency.

Business logic becomes more difficult to design. It is not always simple for developers to handle.

Communication between services

Services need to communicate with each other. There are two main approaches.

Synchronous communication

Example:

Order service
        ↓
HTTP call
        ↓
Payment service
        ↓
Response

Advantage:

  • easy to understand.

Disadvantages:

  • strong dependency;
  • timeout problems;
  • domino effects.

Asynchronous communication

Example:

Order service
        ↓
Message Queue
        ↓
Payment service

Advantages:

  • better resilience;
  • less coupled services.

But this also introduces other problems:

  • duplicated messages;
  • event ordering;
  • lost messages;
  • retry handling.

Communication between services therefore becomes a topic on its own. You also have to accept that data is not always immediately consistent between different services.

Microservices require a certain level of maturity

Microservices generally require good DevOps practices, deployment automation, advanced monitoring, and above all, good design practices.

A team already struggling with a monolithic application will often face even more problems with multiple services. You need enough experience and perspective to introduce this architecture properly. A team starting from scratch is not necessarily in the best position to adopt microservices immediately.

Adding microservices does not remove complexity. It simply moves it somewhere else.

Why a monolith can sometimes be preferable

A monolith is not necessarily a bad architecture. I have worked for more than 15 years on a monolith, and if it is well designed, it can have many advantages as simple deployment, easier debugging, simple transactions, less infrastructure, faster development ...

For many applications, a monolith is more than enough. It is often better to start with a simple architecture and extract some services only when necessary. A poor microservice design can be worse than a good monolith.

Some good practices

Before choosing a microservice architecture:

  • identify the real problems to solve;
  • avoid splitting the application too early;
  • start simple;
  • clearly define each service's responsibilities;
  • plan monitoring from the beginning;
  • properly handle network errors;
  • think about distributed transactions;
  • automate deployments.

Conclusion

Microservices are a powerful tool, but they are not a universal solution and they are absolutely not simple to implement.

The best architecture is not necessarily the most complex one. It is the one that actually meets the project's needs!