Skip to content
Chapter 14 · Microservices

Spring Cloud Bus ​

Chapter 13 refreshed a service with POST /actuator/refresh, one call per instance. With a hundred instances, that means a hundred calls. It would be better to make one call and have every service hear about it.

That is Spring Cloud Bus: it carries events from one service to many, through a message broker. To see how, start with events inside a single application.

Events inside one application ​

Spring has a built-in publish/subscribe mechanism. Three pieces:

The event, a class extending ApplicationEvent:

java
public class MyCustomEvent extends ApplicationEvent {

    private final String message;

    public MyCustomEvent(Object source, String message) {
        super(source);                  // source: the object that published the event
        this.message = message;
    }

    public String getMessage() { return message; }
}

The publisher, using Spring's ApplicationEventPublisher:

java
@Service
public class MyEventPublisher {

    @Autowired
    private ApplicationEventPublisher publisher;

    public void publish(String msg) {
        publisher.publishEvent(new MyCustomEvent(this, msg));
    }
}

A listener, any bean method annotated @EventListener whose parameter is the event type:

java
@Component
public class MyEventListener {

    @EventListener
    public void handleEvent(MyCustomEvent event) {
        System.out.println("Received event message: " + event.getMessage());
    }
}

A controller calls publish("hello world"), and the listener prints Received event message: hello world. The publisher never called the listener; it does not even know it exists.

How it works: at startup Spring records, for each event type, the list of listener methods:

text
MyCustomEvent  -> MyEventListener.handleEvent, Listener2.onX, ...
SomeOtherEvent -> Listener3.onY, ...

publishEvent hands the event to SimpleApplicationEventMulticaster, which looks up the listeners for the event's type and invokes them one by one, synchronously. This is the observer pattern, inside one application.

Across services: Spring Cloud Bus ​

When the listeners live in other services, the event must travel over the network. Spring Cloud Bus connects every service to a message broker (RabbitMQ or Kafka) and does the transport:

  1. Service A publishes an event that extends RemoteApplicationEvent. Spring Cloud Bus intercepts it, serializes it to JSON and sends it to the broker.
  2. The broker delivers it to every service on the bus.
  3. In each service, Spring Cloud Bus deserializes it and hands it to the local SimpleApplicationEventMulticaster, which calls the @EventListener methods as before.

You do not create exchanges, queues or bindings in RabbitMQ, nor topics or partitions in Kafka; the bus creates what it needs. But it also gives you no control over them: no choice of retry behavior, persistence, acknowledgements or ordering. The broker's defaults decide.

So Spring Cloud Bus is meant for low-volume, non-critical messages: "refresh your configuration", "clear your cache". If one is lost, the next one fixes things. For business events that must not be lost, use the broker directly, for example through Spring Cloud Stream or KafkaTemplate, where you control topics, retries and acknowledgements.

Step 1: a broker ​

Any RabbitMQ will do. With a container runtime:

bash
docker run -d --name rabbitmq -p 5672:5672 rabbitmq:3

(or brew install rabbitmq and brew services start rabbitmq on a Mac). The default user and password are guest / guest.

Step 2: the publisher service ​

Add the bus with its RabbitMQ binding (for Kafka, use spring-cloud-starter-bus-kafka):

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
properties
spring.application.name=producer-service
server.port=8081

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

(Credentials belong in environment variables or a secrets manager; they are inline here only for testing.)

The event extends RemoteApplicationEvent (itself a subclass of ApplicationEvent), which is what the bus intercepts:

java
public class MyRemoteCustomEvent extends RemoteApplicationEvent {

    private String message;

    public MyRemoteCustomEvent() {}                 // needed for JSON deserialization

    public MyRemoteCustomEvent(Object source, String originService,
                               String destination, String message) {
        super(source, originService, destination);
        this.message = message;
    }

    public String getMessage() { return message; }
}
  • source: the publishing object, as before.
  • originService: which service published the event (its bus ID, below).
  • destination: which services should receive it: * (or null) for all, or a service name pattern such as order-service:**.

The bus ID and the origin rule ​

Every service on the bus has a bus ID, spring.cloud.bus.id. By default it is built from the application name, active profile, port and a random value:

text
producer-service:dev:8085:57ba12700c026f3a32a3d5d2b1550cad

The bus only sends events whose originService matches the publishing service's own bus ID. A service may not publish in another service's name; without this rule, a service that forwarded a received event would send it straight back, and events could loop forever.

So read the bus ID and pass it as the origin:

java
@Service
public class MyRemoteEventPublisher {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Value("${spring.cloud.bus.id}")
    private String myBusID;

    public void publish(String msg) {
        publisher.publishEvent(new MyRemoteCustomEvent(this, myBusID, "*", msg));
    }
}

Three origins, with two order-service instances listening:

originServiceSent over the bus?
the bus ID (producer-service:dev:8085:57ba...)yes: both order-service instances receive it
producer-serviceno: only the producer's own local listeners see it
producer-service:**yes: a pattern, matched against the bus ID

The pattern form producer-service:** matches only the application name and skips profile, port and random parts. Alternatively, set a predictable bus ID yourself, for example spring.cloud.bus.id=${spring.application.name}-${server.port}, and use that.

A controller triggers it:

java
@RestController
public class RemoteEventController {

    @Autowired
    MyRemoteEventPublisher publisher;

    @GetMapping("/publish")
    public String publish(@RequestParam String message) {
        publisher.publish(message);
        return "Event published: " + message;
    }
}

Step 3: the listener service ​

The same dependency and RabbitMQ properties, with its own name (consumer-service, port 8082). The listener is an ordinary @EventListener:

java
@Component
public class MyRemoteEventListener {

    @EventListener
    public void handle(MyRemoteCustomEvent event) {
        System.out.println("Received remote event: " + event.getMessage());
    }
}

The listener needs the same event class to deserialize the JSON. Put it in a shared library both services depend on, or copy it with the same package and name.

The bus must also know which classes are remote events, so tell it where to look:

java
@SpringBootApplication
@RemoteApplicationEventScan(basePackages = "com.example.events")
public class ConsumerApplication { ... }

Start both, call GET localhost:8081/publish?message=hello world on the producer, and the consumer prints Received remote event: hello world.

The real use case: one refresh for every service ​

Chapter 13's setup, plus the bus:

Config server: add the bus and Actuator, configure RabbitMQ, and expose busrefresh:

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
properties
management.endpoints.web.exposure.include=health,busrefresh
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

Each service: add the same bus dependency and RabbitMQ properties. Its controller and @ConfigurationProperties class stay exactly as in chapter 13. No @EventListener to write.

Test with two order-service instances:

text
GET 8081/orders/fetch, GET 8082/orders/fetch   ->  Hello from order dev   (both)
commit: custom.message=Hello from order dev BUS
POST localhost:8888/actuator/busrefresh        ->  204
GET 8081/orders/fetch, GET 8082/orders/fetch   ->  Hello from order dev BUS   (both)

One call refreshed every instance. Neither order-service instance was called directly.

How it works ​

The busrefresh endpoint (RefreshBusEndpoint) publishes a RefreshRemoteApplicationEvent with the server's bus ID as origin and no destination (everyone). The bus sends it to the broker, which delivers it to every service on the bus, including the config server itself.

In each service, the framework's own RefreshListener, an @EventListener for that event, calls contextRefresher.refresh(): exactly what /actuator/refresh did in chapter 13. Each service logs Received remote refresh request., fetches its properties from the config server, and rebinds its configuration beans.

The config server publishes first; it does not refresh before telling the others. So a service may ask for its properties before the server has handled its own copy of the event. That is harmless: with a Git backend, the config server checks the repository on every request, so whichever arrives first gets the latest commit.

Next: With requests flowing through a gateway and across several services, finding where a slow or failing request went becomes hard. Chapter 15 introduces distributed tracing.

Check yourself ​

  1. How does publishEvent reach the right listeners inside one application?
  2. What does Spring Cloud Bus add to that mechanism?
  3. What must a bus event extend, and why?
  4. What do originService and destination mean?
  5. What is the default bus ID made of?
  6. Why must originService match the publisher's bus ID, and what happens if it does not?
  7. Why does the listener need @RemoteApplicationEventScan?
  8. What is Spring Cloud Bus suitable for, and what should you use instead for critical business events?
  9. What do the config server and the services need to support /actuator/busrefresh?
  10. Who handles the refresh event in each service?
  11. The config server publishes the refresh event before refreshing itself. Why is that not a problem?
Answers
  1. Spring records the listener methods for each event type at startup; SimpleApplicationEventMulticaster looks up the listeners for the published event's type and calls them synchronously.
  2. Transport between services: it serializes remote events to JSON, sends them through a message broker, and in each receiving service deserializes them and hands them to the local multicaster.
  3. RemoteApplicationEvent; only those are intercepted and sent over the bus.
  4. originService: the bus ID of the publishing service. destination: which services should receive it (* or null for all, or a pattern like order-service:**).
  5. Application name, active profile, port and a random value, e.g. producer-service:dev:8085:57ba....
  6. A service may only send events in its own name, which prevents forwarding loops. If it does not match, the event is not sent over the bus; only local listeners see it.
  7. To know which classes are remote events, so it can deserialize the incoming JSON into the right type.
  8. Low-volume, non-critical messages such as refreshing configuration or clearing caches. For critical events, Spring Cloud Stream or the broker's client (e.g. KafkaTemplate) with full control over retries, persistence and acknowledgements.
  9. Both: the bus starter (e.g. spring-cloud-starter-bus-amqp) and the broker's connection properties. The config server also needs Actuator with busrefresh exposed.
  10. The framework's RefreshListener, which calls contextRefresher.refresh().
  11. With a Git backend, the config server reads the repository on each request, so any service asking for properties gets the latest commit.