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.