In modern microservices architectures, services often need to communicate both ways β sending and receiving messages asynchronously. While REST-based synchronous APIs are easy to start with, they don't scale well under high loads or when services are temporarily unavailable.
This is where Apache Kafka shines β providing durable, fault-tolerant, and decoupled communication through an event-driven model.
In this blog, weβll build a bidirectional communication flow between two Spring Boot microservices using Spring Cloud Stream and Kafka.
π§© Why Use Kafka and Spring Cloud Stream?
Feature | Benefit |
---|---|
π§΅ Asynchronous | Non-blocking communication |
π Resilient | Retry, Dead-letter topics |
π Decoupled | Producers donβt need to know who consumes the message |
π Bidirectional | Services can act as both consumer and producer |
π¦ Spring Cloud Stream | Abstraction layer over Kafka and RabbitMQ |
π οΈ Use Case
Weβll implement the following services:
1οΈβ£ Order Service
- Publishes an event when a new order is placed (
OrderPlaced
) - Listens for inventory updates from Inventory Service (
InventoryUpdated
)
2οΈβ£ Inventory Service
- Listens for new orders
- Sends inventory update events after checking stock
ποΈ Project Setup
We'll use:
- Spring Boot 3.x
- Java 17+
- Spring Cloud Stream with Kafka Binder
- Kafka (locally or via Docker)
βοΈ Maven Dependencies (Add to both services)
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
π§Ύ application.yml β Configuration
Order Service
spring:
application:
name: order-service
cloud:
stream:
bindings:
order-out:
destination: order-topic
content-type: application/json
inventory-in:
destination: inventory-topic
group: order-group
content-type: application/json
kafka:
binder:
brokers: localhost:9092
Inventory Service
spring:
application:
name: inventory-service
cloud:
stream:
bindings:
order-in:
destination: order-topic
group: inventory-group
content-type: application/json
inventory-out:
destination: inventory-topic
content-type: application/json
kafka:
binder:
brokers: localhost:9092
π§βπ» Domain Models
Order.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private String orderId;
private String productId;
private int quantity;
}
InventoryUpdate.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class InventoryUpdate {
private String orderId;
private boolean inStock;
}
π§± Order Service β Producer + Consumer
β
OrderService.java
β Send Orders
@Service
@RequiredArgsConstructor
public class OrderService {
private final StreamBridge streamBridge;
public void placeOrder(Order order) {
streamBridge.send("order-out", order);
System.out.println("π€ Order sent to Kafka: " + order);
}
}
β
OrderController.java
β REST API
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody Order order) {
orderService.placeOrder(order);
return ResponseEntity.ok("Order placed successfully!");
}
}
β
InventoryUpdateListener.java
β Listen to Inventory Update
@Component
public class InventoryUpdateListener {
@StreamListener("inventory-in")
public void handleInventoryUpdate(InventoryUpdate update) {
System.out.println("π₯ Inventory update received for Order ID: " + update.getOrderId());
// Update order status accordingly
}
}
π§± Inventory Service β Consumer + Producer
β
OrderListener.java
@Component
public class OrderListener {
@StreamListener("order-in")
public void handleOrder(Order order) {
System.out.println("π₯ Order received in Inventory: " + order);
boolean inStock = checkStock(order.getProductId(), order.getQuantity());
InventoryUpdate update = new InventoryUpdate(order.getOrderId(), inStock);
streamBridge.send("inventory-out", update);
System.out.println("π€ Inventory status sent for Order ID: " + order.getOrderId());
}
@Autowired
private StreamBridge streamBridge;
private boolean checkStock(String productId, int quantity) {
// Mock logic
return quantity <= 10; // Example stock logic
}
}
πΌοΈ Communication Diagram
+------------------+ +---------------------+
| Order Service | | Inventory Service |
|------------------| |---------------------|
| REST API (POST) | | |
| placeOrder() | | |
| βββ order-out βββΊ Kafka (order-topic) βββΊ order-in |
| | | handleOrder() |
| inventory-in βββ Kafka (inventory-topic) βββ inventory-out |
| handleInventory()| | |
+------------------+ +---------------------+
β Kafka Setup (Local)
You can run Kafka locally using Docker:
π¦ docker-compose.yml
version: '2'
services:
zookeeper:
image: wurstmeister/zookeeper
ports:
- "2181:2181"
kafka:
image: wurstmeister/kafka
ports:
- "9092:9092"
environment:
KAFKA_ADVERTISED_HOST_NAME: localhost
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
Run:
docker-compose up -d
Create topics manually (optional):
# Create order-topic
bin/kafka-topics.sh --create --topic order-topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
# Create inventory-topic
bin/kafka-topics.sh --create --topic inventory-topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
π§ͺ Testing the Flow
- Start Kafka
- Run Inventory Service
- Run Order Service
- Send a POST request to the Order API:
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"orderId":"O123", "productId":"P789", "quantity":5}'
β Output:
- Order sent to Kafka
- Inventory service receives and checks stock
- Inventory update sent back
- Order service receives inventory status
π§ Key Concepts Recap
Term | Meaning |
---|---|
order-out |
Spring binding name to produce messages to order-topic
|
order-in |
Spring binding name to consume messages from order-topic
|
inventory-out |
Sends messages to inventory-topic
|
inventory-in |
Receives messages from inventory-topic
|
π‘ Best Practices
- Use consumer groups to support multiple instances
- Add error handling and dead-letter topics
- Use Avro + Schema Registry for schema evolution
- Avoid tight coupling by treating messages as contracts
π Conclusion
Bidirectional communication with Kafka using Spring Cloud Stream is simple, scalable, and reliable. It decouples services, improves resilience, and allows services to operate independently using asynchronous events.
This pattern is widely used in:
- E-commerce order processing
- Payment & notification systems
- IoT and real-time analytics
π Ready to scale your microservices with event-driven design? This setup is your first step toward building resilient systems.
Top comments (0)