• Skip to main content
  • Skip to secondary menu
  • Skip to primary sidebar
  • Skip to footer
  • Home
  • Contact Us

All about code

  • Java
    • Spring
      • Spring Boot
      • Spring Cloud
    • Java Miscellaneous Tips
    • Java Design Patterns
  • Data Structures and Algorithms
    • Algorithms
    • Data Structures
  • Thoughts and Stories
  • AI

How to Implement Message-Driven Services with Spring Cloud Stream

apple smartphone desk laptop 1282241

Introduction

In our previous articles, we have discussed the REST standard primarily when discussing micro-service communication styles. Message-driven communication is an alternative that guarantees asynchronous, highly decoupled interaction. This article will teach you how to implement message-driven services using Spring Cloud.

Message-Driven Services

We can distinguish two main different patterns based on messages:

  • Point-to-point
  • Publish-subscribe

Point-to-point involves exchanging messages between two established endpoints. In the public-subscribe model, on the other hand, messages are sent to a so-called topic. Every service that wants to consume those messages must register as a listener to a topic. This article will focus on the publish-subscribe model, i.e. the best way to reduce coupling between components.

The basic components of messaging-based systems are producers and consumers. A producer sends messages created by some source to a remote destination (a topic), and a consumer registers to the same destination to receive the messages.

The exchange of messages is done through specialized software components called message brokers. Examples are Rabbit MQ and Kafka. We can integrate micro-services based on Spring Cloud Stream and message brokers through the so-called binders. A binder provides a broker-specific implementation of the Spring Cloud Stream abstraction.

Once we configure a specific binder, we have established an integration between our Spring Cloud Stream application and a broker. Using Spring Cloud Stream and broker-specific configurations, we can define the basic connection properties and the bindings between our application and broker topics, that we can call destinations.

Message-Driven Style Broker Running and Configuration

To have a functioning sample we must have a running message broker. If we use RabbitMQ, we can run it by using docker, with a docker-compose file like this:

version: '3'
services:
  rabbitmq:
    image: rabbitmq:management
    ports:
      - 5672:5672
      - 15672:15672

To start it we have to position ourselves in the folder where the docker-compose file is and enter the command “./docker compose up” in the command line. Once we have RabbitMS running, we can configure it in our application.xml file:

spring:
   application:
      name: booksmanagement-service
   rabbitmq:
      host: localhost
      port: 5672

Message-Driven Style Implemented by Java Functional Interfaces

In the recent versions of Spring Cloud Stream, message-driven services can use Java functional interfaces to implicitly implement message producers and consumers. Three interfaces are used:

  • Consumer<T>, where T is the type of input
  • Supplier<T>, where T is the type of input
  • Function<T, R>, where T is the type of input and R is the type of the result

The Consumer interface, as its name implies, is implemented to provide a consumer in the publish-subscribe pattern, while a Supplier object implements a producer. The Function interface is a mixing of the two, it implements both a producer and a consumer in a single object. It connects to the broker to send a message to a topic and at the same time returns a value to an output topic.

To provide implementations of those three interfaces to obtain implicit producers and consumers we define Spring beans in the application, as in the following example:

@SpringBootApplication
public class AppMain {

	public static void main(String[] args) {
        new SpringApplicationBuilder(AppMain.class).run(args);
	}

    @Bean
    public Consumer<String> sink() {
        return payload -> {
            System.out.println(payload);
        };
    }

    @Bean
    public Supplier<String> source() {
        return () -> {
            String message = "Supplier message";
            System.out.println("Sending value: " + message);
            return message;

        };
    }

    @Bean
    public Function<String, String> uppercase() {
        return payload -> {
            System.out.println(payload);
            return payload.toUpperCase();
        };
    }
}

The name of the bean translates automatically to a binding with the format <function-name>-in-<index> for consumers and <function-name>-out-<index> for producers. The index part is 0 by default and has a particular significance only in the case of functions with multiple input/output arguments.

We can map the function names to remote destinations (topics or queues), like in the following piece of configuration:

spring:
   application:
      name: booksmanagement-service
   rabbitmq:
      host: localhost
      port: 5672 
   cloud:
      function:
         definition: uppercase;receive;send;source
      stream:
         bindings:
            uppercase-in-0:
               destination: test-out    
               error-handler-definition: errorHandler           
            uppercase-out-0:
               destination: test-in
            sink-in-0:
               destination: test-in
            source-out-0:
               destination: test-out  

Error Handling

We can attach an error handler to a function by defining a specific Consumer bean with ErrorMessage as input type:

    @Bean
    public Consumer<ErrorMessage> errorHandler() {
        return error -> {
            System.out.println(error.getPayload());
            System.out.println(error.getOriginalMessage());
        };
    }

The error handler can then be associated with the function like in the following example:

      stream:
         bindings:
            uppercase-in-0:
               destination: test-out    
               error-handler-definition: errorHandler          

Message-Driven Services Triggered by REST Calls

If we need to produce messages by some external REST call, we have to use a StreamBridge service. We can inject this service into a controller and use its send method, passing to it a binding and a payload:

@RestController
@RequestMapping("/booksmanagement")
public class BooksManagementController {

    @Autowired
    private StreamBridge streamBridge;

    @GetMapping(value = "/sendBookInfo")
    public void sendBookInfo() {
        BookInfo bookInfo = new BookInfo();
        bookInfo.setId(1L);
        Book book = new Book();
        book.setTitle("The Naked Sun");
        book.setGenre("Sci-fi");
        Author author = new Author();
        author.setName("Isaac Asimov");
        author.setBiographyInfo("Biography Info");

        System.out.println("Sending " + bookInfo);
        streamBridge.send("sendBookInfo-out-0", bookInfo);
    }

In the application.xml we then define a function with the same name as the controller method and associate it with the same binding we passed as input to the send method:

   cloud:
      function:
         definition: sendBookInfo
      stream:
         bindings:
            sendBookInfo-out-0:
               destination: bookInfo-out

Grouping

In micro-service applications, we normally run multiple instances for each service. If the micro-service acts as a consumer for a specific topic, we normally do not want all the instances consuming the same message. We can avoid this by using the grouping functionality. Defining a group for a binding definition allows only one instance of the group at a time to consume a specific message.

For instance, we can implement a sample by sending messages to a topic using a StreamBridge service:

    @GetMapping(value = "/sendBookInfoGrouped")
    public void sendBookInfoGrouped() {
        BookInfo bookInfo = new BookInfo();
        bookInfo.setId(1L);
        Book book = new Book();
        book.setTitle("The Naked Sun");
        book.setGenre("Sci-fi");
        Author author = new Author();
        author.setName("Isaac Asimov");
        author.setBiographyInfo("Biography Info");

        System.out.println("Sending " + bookInfo);
        streamBridge.send("sendBookInfoGrouped-out-0", bookInfo);
    }

We define the binding in the configuration file of the producer side:

   cloud:
      function:
         definition: sendBookInfoGrouped
      stream:
         bindings:
            sendBookInfoGrouped-out-0:
               destination: bookInfoGrouped-out

And in the consumer part, we define a group for the binding definition:

   cloud:
      function:
         definition: receiveBookInfoGroup
      stream:
         bindings:
            receiveBookInfoGroup-in-0:
               destination: bookInfoGrouped-out
               group: bookInfoGroupBooks

Partitioning

A typical requirement in message-driven architectures is to partition the input data so single service instances elaborate single partitions. To partition the data we have to use some attribute of the payload. In the example below we add the id attribute of a BookInfo object as a property of the header, named bookInfoId.

    @GetMapping(value = "/sendBookInfoPartitioned")
    public void sendBookInfoPartitioned() {
        BookInfo bookInfo = new BookInfo();
        bookInfo.setId(1L);
        Book book = new Book();
        book.setTitle("The Naked Sun");
        book.setGenre("Sci-fi");
        Author author = new Author();
        author.setName("Isaac Asimov");
        author.setBiographyInfo("Biography Info");

        System.out.println("Sending " + bookInfo);
        streamBridge.send("sendBookInfoPartitioned-out-0", MessageBuilder.withPayload(bookInfo)
            .setHeader("bookInfoId", bookInfo.getId())
            .build());
    }

In the binding configuration, we need two additional properties:

  • partitionKeyExpression: headers[‘bookInfoId’]
  • partitionCount: 4

As in the following application.xml fragment:

   cloud:
      function:
         definition: sendBookInfoPartitioned
      stream:
         bindings:
            sendBookInfoPartitioned-out-0:
               destination: bookInfoPartitioned-out
               producer:
                  partitionKeyExpression: headers['bookInfoId']
                  partitionCount: 4 

The partitionKeyExpression property defines in general an expression that serves the purpose of partitioning the data. It could be any expression, and in this case, it is the bookInfoId attribute retrieved from the header. The partitionCount property defines how many partitions are used. The consumer part will use this number to decide which instance will consume a particular partition.

By default, Spring Cloud Stream will calculate the hash of the partitionKeyExpression property and the module of it and partitionCount: key.hash()%partitionCount. In our example, this will be a number from 0 to 3, and on the consumer side we can set which partition each instance will elaborate by a binding configuration like this:

   cloud:
      function:
         definition: receiveBookInfoPartitioned
      stream:
         bindings:
            receiveBookInfoPartitioned-in-0:
               destination: bookInfoPartitioned-out
               group: bookInfoGroupBooks
               consumer:
                  partitioned: true
                  instance-index: 0
                  instance-count: 4

We define the consumer as partitioned setting the partitioned parameter to true, and assign a number from 0 to 3 to the instance-index parameter. The instance-count parameter defines the number of instances working on the partitioned data.

Conclusion

The message-driven style of communication has particular importance in micro-service systems. It allows for asynchronous interaction and loose coupling of the services. You can find the source code of the example of this article on GitHub.

Primary Sidebar

Social

  • Facebook
  • LinkedIn
  • Twitter

Archives

  • May 2026
  • April 2026
  • March 2026
  • August 2025
  • July 2025
  • June 2025
  • May 2025
  • April 2025
  • March 2025
  • December 2024
  • October 2024
  • August 2024
  • July 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • September 2023
  • May 2023
  • March 2023
  • January 2023
  • November 2022
  • September 2022
  • August 2022

Recent Posts

  • Better Avoid Spring Boot Lazy-initialization @PostConstruct Trap
  • How To Handle Large Datasets in Spring Boot
  • How to Log HTTP Incoming Requests in Spring Boot
  • How to Reliably Implement Post-Commit Actions in Spring
  • Brief and Easy Explanation of HATEOAS

TAGS

Algorithms Apache POI Backtracking Date Structures Dynamic Programming engineering Graphs Greedy Horror Stories HSSF Java Java New Features Java Principles Java tips real life Sorting Spring Boot Spring Cloud Strings Trees

Footer

Privacy Policy Cookie Policy Terms and Conditions