DEV Community

Dev Cookies
Dev Cookies

Posted on

πŸ” Bidirectional Service-to-Service Communication using Kafka in Spring Boot with Spring Cloud Stream

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>
Enter fullscreen mode Exit fullscreen mode

🧾 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ§‘β€πŸ’» Domain Models

Order.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private String orderId;
    private String productId;
    private int quantity;
}
Enter fullscreen mode Exit fullscreen mode

InventoryUpdate.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class InventoryUpdate {
    private String orderId;
    private boolean inStock;
}
Enter fullscreen mode Exit fullscreen mode

🧱 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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
    }
}
Enter fullscreen mode Exit fullscreen mode

🧱 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
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ–ΌοΈ 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()|                      |                     |
+------------------+                      +---------------------+
Enter fullscreen mode Exit fullscreen mode

βœ… 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
Enter fullscreen mode Exit fullscreen mode

Run:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Testing the Flow

  1. Start Kafka
  2. Run Inventory Service
  3. Run Order Service
  4. 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}'
Enter fullscreen mode Exit fullscreen mode

βœ… 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)