Event-Driven vs REST-Based Communication in Microservices

8 min read
Share:

Introduction

When building microservices, one question comes up quickly:

How should one microservice communicate with another?

Should it call a REST API and wait for a response, or publish an event and let other services react asynchronously?

There is no universal answer. Both approaches solve different problems.

As a developer, I prefer starting with the business requirement rather than the technology.

  • REST: “I need something from you.”
  • Event: “Something happened. React if you need to.”

This article covers where REST and event-driven communication fit, their trade-offs, practical Java examples, and how to choose between them.

REST-Based Communication

REST is one of the most common communication styles in Java microservices. One service exposes an HTTP API and another service calls it.

Order Service | | POST /payments  Payment Service | | PaymentResponse  Order Service

A simple Spring Boot endpoint:

@PostMapping
 public PaymentResponse makePayment(@RequestBody PaymentRequest request) { 
return paymentService.process(request); 
}

REST works well when the caller needs an immediate response.

Typical examples include:

  • Getting customer or product details
  • Checking account information
  • Validating data
  • Processing a payment where an immediate result is required

For example:

GET /customers/123
GET /orders/1001
GET /products/500

The Problem with Too Much Synchronous Communication

REST itself does not inherently create temporal coupling. The key factor is synchronous communication, where the caller waits for the downstream service to respond.

For example:

Order Service |  Payment Service |  Fraud Service |  Notification Service

If communication between these services is synchronous, a slow or unavailable downstream service can affect the upstream request.

We then need to handle:

  • Timeouts
  • Retries
  • Circuit breakers
  • Partial failures

This creates temporal coupling because the downstream service needs to be available and responsive at the time the request is made.

REST works well for synchronous request/response interactions, but long chains of synchronous dependencies can increase the impact of failures and latency across the system.

Event-Driven Communication

Instead of calling another service directly, a service publishes an event representing something that has happened.

For example:

Order Service | | OrderCreated --> Event Broker 

Inventory Service |  Notification Service | Analytics Service

The Order Service doesn’t need to know which services consume the event.

It simply publishes:

OrderCreated

Each consumer can process it independently.

Event vs Kafka

It is important to distinguish between an event and Kafka.

An event is a message representing a business fact, such as:

OrderCreated PaymentCompleted ShipmentDelivered

Kafka is a distributed event streaming platform that can be used to transport, store, and distribute these messages.

In other words:

Business Event |  Event Transport / Broker |  Consumers

Kafka is one technology that can be used to implement event-driven communication. Event-driven architecture is the broader architectural approach.

Java Example Using Kafka

With Spring Kafka, a producer can publish an event:

OrderCreatedEvent event = new OrderCreatedEvent(order.getId(), order.getCustomerId(), order.getTotalAmount());
kafkaTemplate.send("order-created", event);

A consumer can listen for it:

@KafkaListener(topics = "order-created")
public void handleOrderCreated(OrderCreatedEvent event) {
inventoryService.reserve(event.getOrderId()); 
}

This approach works particularly well when multiple services need to react to the same business event.

REST vs Events: The Core Difference

REST Event
“Give me the customer details.” “A customer was registered.”
“Process this payment.” “The payment was completed.”
“What is the order status?” “The order was shipped.”

A simple mental model is:

REST 
"Give me something."
"Do this for me."
"What's the current state?" 

Events 
"Something happened." 
"Here is a business fact." 
"React if you care."

Where Events Work Well

Events are a good fit when multiple independent services need to react to the same action.

For example:

OrderCreated | ----> Inventory ----> Notification ----> Analytics ----> Loyalty

Benefits include:

  • Lower temporal coupling
  • Independent scaling of consumers
  • Easy fan-out to multiple consumers
  • Asynchronous processing
  • Easier addition of new consumers

Event-driven systems introduce their own challenges:

  • Duplicate messages
  • Message ordering
  • Retries and dead-letter queues
  • Consumer failures and lag
  • Schema evolution
  • Idempotency
  • Distributed tracing
  • Monitoring and operational complexity

So I wouldn’t introduce Kafka simply because it sounds more scalable.

Use it when it solves a real business or architectural problem.

Handling Duplicate Events

A consumer may receive the same event more than once.

For example:

PaymentCompleted(orderId=1001) 
PaymentCompleted(orderId=1001)

The same event may be redelivered because of retries, consumer failures, acknowledgements, or other delivery scenarios.

If processing the event twice causes problems, the consumer should be idempotent.

A simple approach such as checking whether an event was already processed can have a race condition:

if (processedEventRepository.exists(event.getEventId())) { 
return; 
} 
orderService.markPaymentCompleted(event.getOrderId());
processedEventRepository.save(new ProcessedEvent(event.getEventId()));

For example, two delivery attempts could both execute exists() before either one saves the record.

A more robust approach is to use an atomic deduplication mechanism, typically backed by a database unique constraint.

For example:

Processed Events event_id ---------------- evt-123 evt-456

The database can enforce:

UNIQUE(event_id)

The consumer can then attempt to insert the event ID atomically. If the event ID already exists, the duplicate can be safely ignored.

The exact implementation depends on the database and processing model, but the important principle is:

Design consumers assuming duplicate delivery is possible.

Consumers should not rely on exactly-once processing at the application level.

The Dual-Write Problem

A common problem occurs when updating a database and publishing an event:

orderRepository.save(order); 
kafkaTemplate.send("order-created", event);

What happens if the database succeeds but Kafka fails?

Database -> SUCCESS Kafka -> FAILURE

Now the order exists, but other services never received OrderCreated.

One common solution is the Transactional Outbox Pattern:

Application |  Database Transaction | ----> Order | ----> Outbox Event |  Outbox Publisher |  Kafka

The business record and event are stored together in the same database transaction, and a publisher later sends the event to Kafka.

Eventual Consistency

Event-driven systems often introduce eventual consistency.

For example:

OrderCreated |  PaymentCompleted |  InventoryReserved |  OrderConfirmed

Different services may temporarily have different views of the state.

This is acceptable for many use cases, such as analytics and notifications.

However, if the business requires an immediate result, such as:

Payment Approved / Payment Declined

synchronous communication may be a better choice.

The Hybrid Approach

In real-world microservices, I rarely recommend using only REST or only events.

A hybrid architecture is often the most practical:

Client |  Order Service | ---- REST ----> Payment Service | ---- Event ---> Kafka | ----> Inventory ----> Notification ----> Analytics

Use REST for interactions requiring an immediate response and events for asynchronous business reactions.

A Practical Decision Framework

When designing a service-to-service interaction, ask:

1. Does the caller need an immediate response?

Yes → REST is a strong candidate.

2. Is this a business event?

If it naturally sounds like “Something happened”, consider an event.

Examples:

  • OrderCreated
  • PaymentCompleted
  • ShipmentDelivered
  • CustomerRegistered

3. Are multiple services interested?

If several services need to react independently, events become more attractive.

4. Can the business tolerate eventual consistency?

If yes, asynchronous processing may be appropriate.

5. Can the consumer safely handle redelivery?

If you choose event-driven communication, consider idempotency, retries, acknowledgements, and failure recovery as part of the design.

6. Is the additional complexity justified?

For a simple request such as retrieving customer details, REST is often all you need.

Common Mistakes

Using REST Everywhere

Long synchronous chains can create strong dependencies:

A ---> B ---> C ---> D ---> E

If communication between these services is synchronous, one slow or unavailable service can affect the entire request chain.

Using Kafka Everywhere

Not every operation needs to become an event. A simple query can often remain a straightforward REST call.

Confusing Commands and Events

CreateOrder -> Command OrderCreated -> Event

A command is an instruction asking a system to perform an action.

An event is a fact stating that something has already happened.

For example:

CreateOrder "Please create this order."
OrderCreated "The order has been created."

Quick Comparison

Consideration REST Event-Driven
Communication Request/Response Publish/Subscribe
Immediate response Yes Usually no
Queries Excellent Usually not ideal
Multiple consumers More coupled Excellent
Event replay Not natural Often supported
Complexity Generally lower Generally higher
Eventual consistency Not required Common
Duplicate handling Usually simpler Important consideration
Temporal dependency Present with synchronous calls Reduced for asynchronous processing

Conclusion

REST and event-driven communication aren’t competing technologies. They are tools for different requirements.

Use REST when you need an answer.

Use events when something happened and other services need to react.

An event is a business fact, while technologies such as Kafka provide mechanisms to transport and retain those events.

For most enterprise microservices, the best architecture is often a combination of both:

REST + Events

Start with the business requirement, then consider consistency, failure handling, delivery semantics, scalability, and operational complexity before choosing the communication style.

The goal isn’t to use the most advanced technology. It’s to use the right communication model for the problem.

Leave a Reply

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