When Microservices Should Stop Using REST

Page content

REST is probably the most common communication mechanism between microservices.

It is simple, familiar, easy to debug, and well supported by virtually every technology stack.

But sometimes, using REST between services starts creating more problems than it solves.

The question is not:

“Is REST good or bad for microservices?”

But it is:

“Does this interaction really need to be synchronous?”

If the answer is no, a message broker may provide a much better architectural boundary.

REST Creates a Runtime Dependency

Consider a simple synchronous interaction:

Service A
    │ HTTP request
Service B
    │ response
Service A

Service A cannot complete its operation until Service B responds.

That creates a runtime dependency.

If B is slow, A becomes slow.

If B is unavailable, A may become unavailable.

If B experiences increased load, A may also start consuming more resources while waiting.

The dependency is not just architectural. It exists at runtime.

For example:

Request
Service A
   ├── HTTP ─────────► Service B
   │                     │
   │                     └── slow database
   └── waiting...

Eventually, threads, connections, request slots, or other resources in Service A can become exhausted.

This is one reason why timeouts are so important in distributed systems.

But a timeout only limits how long we wait.

It cannot remove the underlying dependency.

Not Every Interaction Needs an Immediate Answer

Many business operations do not actually require synchronous communication.

Imagine an order service that needs to send a confirmation email.

Does the customer-facing request really need to wait for the email service?

Usually not.

A REST-based design might look like this:

Order Service
     │ POST /send-email
Email Service
SMTP Provider

Now the order operation depends on the availability and response time of multiple external components.

A message-based design can be much simpler:

Order Service
     │ OrderCreated
 Message Broker
Email Service

The order service can finish after successfully publishing the message.

The email service can process the message independently.

This introduces eventual consistency, but that is often a much better trade-off than introducing a synchronous runtime dependency.

Messaging Provides More Than Asynchronous Execution

A message broker is not simply a slower REST endpoint.

It changes the failure model.

With synchronous REST:

A ───────► B
          X unavailable

A immediately experiences the failure.

With messaging:

A ───────► Queue ───────► B
              └── message remains available

B can temporarily be unavailable without necessarily preventing A from completing its own work.

Depending on the messaging system and configuration, this can provide:

  • buffering
  • decoupling
  • load smoothing
  • consumer scaling
  • redelivery
  • retry handling
  • temporary downstream failure isolation

When a consumer fails or becomes temporarily unavailable, a message broker can typically retain the message and deliver it again when the consumer recovers. The producer does not necessarily need to know whether the consumer is currently available.

With REST, a failed request can be much more problematic. If the client receives a timeout or connection error, it may not know whether the server never processed the request or processed it successfully but the response was lost. Retrying the request can therefore result in duplicate processing, while not retrying may result in lost work.

This is one of the fundamental differences between synchronous REST communication and message-based communication: messaging can make failed delivery and redelivery explicit parts of the communication model, whereas REST requires the application to implement much of this reliability logic itself.

Making REST Delivery Reliable

REST does not provide persistent delivery semantics by itself. If a request fails, the client may not know whether the server never received the request, failed before processing it, or successfully processed it but the response was lost.

One way to make this more reliable is to persist the data that needs to be sent in the local database first. A background worker or scheduler can then periodically attempt to deliver the data to the other service:

Service A
   │ DB transaction
┌──────────────────┐
│ Business Data    │
│ Outbox / Pending │
│ Request          │
└──────────────────┘
         │ background worker
      REST call
     Service B

If Service B is temporarily unavailable, the pending data remains in the database and can be sent again later.

This pattern can provide reliable delivery even when REST is used for service-to-service communication. It is essentially an outbox-style approach combined with a delivery worker.

However, there is an important architectural observation here: once we add persistent messages, retry logic, delivery tracking, and redelivery semantics around REST, we are starting to build capabilities that message brokers already provide as part of their communication model.

That does not make the REST-based approach wrong. It can be a perfectly reasonable solution when introducing a message broker would be unnecessary or impractical.

But it is worth asking:

If we need to build a queue around REST, should this communication have been asynchronous messaging in the first place?

But Messaging Does Not Make Distributed Systems Easy

This is where messaging architectures are often overhyped.

Replacing REST with a message broker does not eliminate distributed systems problems.

It changes them.

You now have to think about:

  • duplicate messages
  • message ordering
  • delivery guarantees
  • poison messages
  • dead-letter queues
  • consumer failures
  • eventual consistency
  • idempotency
  • schema evolution
  • message observability

For example, if a payment message is delivered twice:

PaymentRequested
       ├────► Consumer
       │         │
       │         └── charge customer
       └────► Consumer
                 └── charge customer again

A message-based architecture therefore makes idempotency particularly important.

The goal is not to eliminate failure.

The goal is to make failure manageable.

The Distributed Transaction Problem

The situation becomes even more interesting when multiple services need to participate in one business transaction.

Imagine:

Order Service
     ├──► Payment Service
     └──► Inventory Service

The business requirement might sound simple:

Create the order, charge the customer, and reserve inventory.

But where is the transaction?

A local database transaction cannot cover all three services.

A Spring Boot:

@Transactional

cannot create a distributed transaction across independent services.

If the payment succeeds but inventory reservation fails, what happens?

Order      ✓
Payment    ✓
Inventory  ✗

We now need compensation or some other consistency strategy.

This is one of the fundamental problems of distributed systems.

The Two Generals’ Problem

There is a deeper reason why this problem is so difficult.

The classic Two Generals’ Problem illustrates the fundamental difficulty of achieving agreement over an unreliable communication channel.

This is closely related to the problems we encounter when trying to coordinate transactions across independent microservices.

The important point is that REST does not cause the Two Generals’ Problem.

REST simply gives us a convenient request/response mechanism over an inherently unreliable distributed network.

No HTTP status code can turn several independent databases into one atomic transaction.

Why Synchronous REST Can Make This Worse

Suppose we try to build a business workflow using synchronous REST calls:

A
├── REST ──► B
│             │
│             └── REST ──► C
└── wait

Now A depends on B, and B depends on C.

The dependency graph grows:

A → B → C → D

The more synchronous dependencies we add, the larger the failure surface becomes.

A single request may now require several services to be:

  • available
  • responsive
  • correctly configured
  • within their timeout limits
  • connected to their own dependencies

This is how apparently simple microservices architectures can develop surprisingly strong coupling.

A Better Boundary: Commands and Events

Messaging becomes particularly attractive when the interaction represents an event or a command.

For example:

Order Service
     │ OrderCreated
 Message Broker
     ├────► Inventory Service
     ├────► Email Service
     └────► Analytics Service

The order service does not need to know which consumers exist.

It publishes a fact:

An order was created.

Other services react independently.

This creates a much looser architecture.

It also makes adding another consumer significantly easier.

Adding analytics does not necessarily require modifying the order service:

                 ┌──► Inventory
Order ─► Broker ─┼──► Email
                 └──► Analytics

When REST Still Makes Perfect Sense

This does not mean that every microservice interaction should use messaging.

REST is often the right choice when the caller needs an immediate response.

For example:

Frontend
   │ GET /orders/123
Order Service
Response

The client is explicitly asking:

“Give me the current representation of this resource.”

A synchronous request/response model is natural here.

REST can also be appropriate for:

  • queries
  • read operations
  • interactive user workflows
  • operations where the result is immediately required
  • APIs exposed to external clients

The problem begins when REST is used simply because it is convenient for service-to-service communication.

A Useful Rule

A useful architectural question is:

Does the caller need the result now, or does the system only need the operation to happen eventually?

If the caller needs the result:

REST
A ─────────► B
◄───────────

If the operation can happen asynchronously:

Messaging
A ─────────► Queue ─────────► B

This distinction is often more useful than arguing about REST versus messaging as technologies.

Don’t Turn Every Workflow Into a Message

There is another common mistake: replacing every REST call with asynchronous messaging.

That can make a system unnecessarily complicated.

Consider a user requesting:

GET /customer/123

Turning that into:

RequestMessage
Queue
Customer Service
ResponseMessage

does not automatically make the architecture better.

If the user needs the answer immediately, synchronous communication is exactly what the use case requires.

The architecture should follow the business interaction, not the other way around.

The Real Problem Is Coupling

The most important distinction is therefore not:

REST vs. messaging

but:

synchronous coupling vs. asynchronous decoupling

REST is a perfectly reasonable technology for synchronous communication.

Messaging is a powerful mechanism for asynchronous communication.

The architectural mistake is using synchronous communication for operations that do not actually require synchronous coupling.

A Practical Decision Model

Before introducing a REST dependency between two services, ask:

  1. Does the caller need the response immediately?
  2. Can the operation tolerate eventual consistency?
  3. What happens if the target service is unavailable?
  4. What happens if the request times out after the target has already processed it?
  5. Can the operation be retried safely?
  6. Is the operation idempotent?
  7. Would buffering help absorb load?
  8. Should the operation survive temporary downstream failures?
  9. Are we accidentally building a distributed transaction?
  10. Would an event or command provide a cleaner boundary?

If the answers point toward asynchronous processing, a message broker is often the better architectural choice.

Final Thoughts

REST is not the enemy.

The problem is using synchronous communication everywhere simply because it is easy to implement.

Microservices are supposed to reduce coupling, but a system containing dozens of services connected through synchronous REST calls can end up looking like a distributed monolith:

        ┌──► B ──► D
A ──────┼──► C ──► E
        └──► F ──► G

Every synchronous dependency becomes part of the runtime availability of the system.

Messaging gives us another option:

                 ┌──► B
A ──────► Broker ┼──► C
                 ├──► D
                 └──► E

The goal is not to eliminate REST.

The goal is to use synchronous communication where synchronous coupling is actually required, and asynchronous messaging where the business process does not need an immediate response.

Once we make that distinction, many problems that otherwise require increasingly sophisticated timeout, retry, and resilience mechanisms become easier to reason about.

And that is perhaps the most important architectural lesson:

Don’t solve with resilience mechanisms a coupling problem that could have been avoided by choosing a different communication model.