Spring Cloud Bus: Broadcasting Config Changes & Events Across Microservices
Learn how to use Spring Cloud Bus to broadcast config changes and events across microservices.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Java 17+
- ✓Spring Boot 3.x
- ✓RabbitMQ or Kafka running locally (or in Docker)
- ✓Basic understanding of Spring Cloud Config
- Spring Cloud Bus links microservices via a lightweight message broker (RabbitMQ or Kafka).
- It broadcasts
RefreshRemoteApplicationEventto all instances when config changes. - Enables dynamic config refresh without restarting each service individually.
- Works with Spring Cloud Config Server to push changes instantly.
- Avoids polling overhead and manual refresh calls.
Imagine you're the manager of a large restaurant chain. Instead of calling each branch individually to update the menu, you send one message to a group chat. Spring Cloud Bus is that group chat for your microservices — one message updates everyone at once.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
If you've ever managed a fleet of microservices, you know the pain of updating configuration. You change a property in your Config Server, then SSH into each box and hit /actuator/refresh — or worse, restart the whole service. That's 2005 thinking. Spring Cloud Bus solves this by turning your services into a mesh that listens to a message broker. When you trigger a refresh, it broadcasts an event to every connected instance. No polling, no manual curl commands. Just one endpoint call, and your entire system picks up the new values.
I've seen teams with 50+ microservices waste entire deployment windows just to change a database connection timeout. With Bus, that same change takes seconds. But here's the kicker: it's not just for config. You can broadcast any custom event — cache invalidation, feature flags, schema migrations — across your entire ecosystem. The pattern is simple: publish to a broker topic, consume in every service that cares.
In this guide, I'll walk you through setting up Spring Cloud Bus with RabbitMQ (the most common choice), show you how to broadcast config changes and custom events, and share the gotchas that only appear under production load. By the end, you'll know when to use Bus and when to reach for something else.
What is Spring Cloud Bus?
Spring Cloud Bus links multiple microservice instances over a lightweight message broker. It's essentially a distributed event bus. The most common use case is broadcasting configuration changes from Spring Cloud Config Server to all connected services. But you can also send arbitrary events — think cache invalidation, feature flag toggles, or even schema migration triggers.
Under the hood, Bus uses either RabbitMQ or Kafka. RabbitMQ is the default and more common in Spring Boot apps. Each service instance declares a unique queue bound to a topic exchange. When you hit /actuator/bus-refresh, the Config Server publishes a RefreshRemoteApplicationEvent to the broker. Every instance picks it up and refreshes its @RefreshScope beans.
Here's the key: Bus is not for high-frequency event streaming. It's for control-plane operations — changes that happen infrequently but must reach every instance. Don't use it for real-time data pipelines. Use Kafka Streams or similar for that.
Let's see a basic setup. Add the dependency to your pom.xml:
`` <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency> ``
Then configure RabbitMQ connection in application.yml:
``yaml spring: rabbitmq: host: localhost port: 5672 username: guest password: guest cloud: bus: enabled: true id: ${spring.application.name}:${spring.application.instance_id:${random.value}} ``
The bus.id must be unique per instance. The default pattern uses application:port but you can customize.
Now, expose the bus refresh endpoint:
``yaml management: endpoints: web: exposure: include: bus-refresh ``
That's it. Start your services, then POST to /actuator/bus-refresh on any instance. All instances will refresh.
${random.value} in your bus id.Broadcasting Config Changes with Bus
The killer feature of Spring Cloud Bus is dynamic config refresh. You change a property in your Config Server repository, then trigger a refresh via Bus. All services pick up the change without restart.
Here's the flow: 1. Config Server hosts properties (e.g., in Git). 2. You update a property and push to Git. 3. You POST to /actuator/bus-refresh on any service (or Config Server). 4. Config Server publishes RefreshRemoteApplicationEvent. 5. Each service instance receives the event and refreshes its @RefreshScope beans.
But there's a catch: not all beans should be @RefreshScope. Only beans that hold configuration properties need it. Overusing @RefreshScope can cause memory leaks because the old bean context is not garbage collected until all references are released.
Example: A @RefreshScope bean that reads from @Value:
```java @RefreshScope @Component public class DatabaseConfig {
@Value("${database.max-connections:10}") private int maxConnections;
public int getMaxConnections() { return maxConnections; } } ```
When refresh happens, this bean is recreated with the new value. But if you have a long-lived reference to it (e.g., stored in a static field), the old bean never gets garbage collected. That's a memory leak.
Another gotcha: @ConfigurationProperties classes are automatically refreshable if they are used in @RefreshScope beans. But if you inject them directly without @RefreshScope, they won't refresh.
Best practice: Use @ConfigurationProperties with a @RefreshScope wrapper, or use the Environment directly.
``java @RefreshScope @ConfigurationProperties(prefix = "database") @Component public class DatabaseProperties { private int maxConnections; // getters and setters } ``
Now, when Bus triggers refresh, DatabaseProperties gets new values.
@RefreshScope bean injected into a singleton scheduler. After a few refreshes, memory usage spiked. We found dozens of stale beans in heap dumps. Solution: use @Scope(value = "refresh", proxyMode = ScopedProxyMode.TARGET_CLASS) and inject via ObjectFactory.@RefreshScope judiciously. Only beans holding configuration properties need it. Singleton references to scoped beans cause memory leaks.Broadcasting Custom Events with Bus
Config refresh is just one use case. You can broadcast any custom event across all services. For example, invalidate a cache in all instances, toggle a feature flag, or trigger a background job.
To create a custom event, extend RemoteApplicationEvent:
```java public class CacheInvalidationEvent extends RemoteApplicationEvent { private String cacheName; private String key;
// constructor, getters, setters public CacheInvalidationEvent() { // for serialization }
public CacheInvalidationEvent(Object source, String originService, String destinationService, String cacheName, String key) { super(source, originService, destinationService); this.cacheName = cacheName; this.key = key; } } ```
Then publish it using ApplicationEventPublisher:
```java @Service public class CacheService {
@Autowired private ApplicationEventPublisher publisher;
public void invalidateAll(String cacheName) { CacheInvalidationEvent event = new CacheInvalidationEvent( this, "my-service", "**", cacheName, null ); publisher.publishEvent(event); } } ```
The destination * means all services. You can target specific services with a pattern like my-service:.
On the receiving end, listen with @EventListener:
```java @Component public class CacheInvalidationListener {
@EventListener public void handleInvalidation(CacheInvalidationEvent event) { String cacheName = event.getCacheName(); String key = event.getKey(); // evict from local cache System.out.println("Invalidating cache: " + cacheName + " key: " + key); } } ```
One thing the docs gloss over: serialization. RemoteApplicationEvent is serialized and deserialized via JSON (Jackson). Your custom event must have a no-arg constructor and getters/setters. Also, the class must be on the classpath of all services. Otherwise, deserialization fails silently.
Another gotcha: the originService and destinationService fields are used for routing. If you set destinationService to **, it goes to all. But if you set it to a specific service ID, only that service receives it. The service ID is the spring.cloud.bus.id value.
@JsonTypeInfo annotation to the event for polymorphic deserialization.RemoteApplicationEvent for custom events. Ensure serialization compatibility across services. Use destination patterns to target specific services.What the Official Docs Won't Tell You
I've been using Spring Cloud Bus since the Dalston release, and the official docs hide some sharp edges. Here are the ones that bit me hardest.
1. Bus is NOT reliable for critical events. Bus uses RabbitMQ's default exchange with transient queues. If a service disconnects and reconnects, it may miss events published during the downtime. There's no guaranteed delivery. For critical config changes, implement a fallback: poll Config Server periodically as a safety net.
2. The /bus-refresh endpoint is not idempotent by default. If you call it twice in quick succession, every instance refreshes twice. That can cause duplicate side effects. Use a distributed lock or deduplication on the event ID.
3. Memory leaks with @RefreshScope are real. I've seen production OutOfMemoryErrors because a @RefreshScope bean was injected into a singleton. The singleton holds a reference to the old bean, preventing GC. Use @Scope(value = "refresh", proxyMode = ScopedProxyMode.TARGET_CLASS) and inject via ObjectFactory if you need to inject into singletons.
4. Bus events are serialized as JSON using Jackson. But the default ObjectMapper may not handle your custom types. If you use Lombok's @Data, Jackson works fine. But if you have complex nested objects, you might need to configure Jackson serialization.
5. The destinationService field is case-sensitive. I spent hours debugging why events weren't reaching a service named payment-service when I sent to Payment-Service. Bus matches exactly.
6. Bus doesn't work well with multiple Config Servers. If you have multiple Config Server instances behind a load balancer, Bus events may not propagate correctly because each Config Server has its own broker connection. Use a single Config Server or a shared broker.
7. The spring.cloud.bus.id must be unique per instance. If you deploy multiple replicas with the same ID, only one will receive events. Always include ${random.value} or a unique instance ID.
8. Bus adds latency to every startup. Each instance declares a queue and binds to the exchange. If you have 100 instances, that's 100 queue declarations. RabbitMQ can handle it, but it adds startup time. Consider using a lazy queue or a separate vhost for Bus.
Bus vs. Manual Refresh: When to Use Which
You might be tempted to use Bus everywhere. Don't. Here's my rule of thumb: use Bus when you need to propagate a change to every instance within seconds, and you can tolerate the occasional missed event. Use manual refresh (calling /actuator/refresh on each instance) when you need guaranteed delivery or when the change is critical.
When to use Bus: - Changing log levels across all services. - Toggling feature flags (non-critical). - Invalidating caches. - Updating non-critical configuration like rate limits (with fallback).
When to avoid Bus: - Changing database connection strings (use manual refresh with blue-green deployment). - Updating authentication secrets (use restart). - Critical security patches (restart is safer).
Another pattern: use Bus for broadcast, but have each service also poll Config Server every few minutes. That way, if Bus misses an event, the poll catches it.
Here's a comparison table:
Testing Spring Cloud Bus in a Distributed Environment
Testing Bus locally is easy — you can run RabbitMQ in Docker and a few service instances. But testing in a CI pipeline with multiple services is tricky. Here's how we do it.
Unit Testing: You can test the event publishing and listening logic without a broker. Use @SpringBootTest with spring.cloud.bus.enabled=false and mock the ApplicationEventPublisher.
Integration Testing: Use Testcontainers to spin up RabbitMQ in your tests. Then start multiple service instances and verify that events propagate.
Example test:
```java @SpringBootTest(properties = { "spring.cloud.bus.enabled=true", "spring.rabbitmq.host=localhost", "spring.rabbitmq.port=5672" }) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class BusIntegrationTest {
@Autowired private ApplicationEventPublisher publisher;
@Test @Order(1) void testCustomEventBroadcast() { // publish event publisher.publishEvent(new CacheInvalidationEvent(this, "test-service", "**", "users", "123")); // give time for propagation Thread.sleep(1000); // assert that listener in another service received it (via shared state) } } ```
But testing across multiple JVMs requires a more elaborate setup. We use a separate test suite that starts multiple Spring Boot applications with different ports and uses a shared RabbitMQ instance.
Gotcha: In tests, Bus events are processed asynchronously. You need to wait for them. Use Awaitility to poll for expected state.
Another tip: disable Bus in unit tests to avoid needing RabbitMQ. Use @SpringBootTest(properties = "spring.cloud.bus.enabled=false").
The Midnight Config Cascade That Took Down Payments
max-connections property in Config Server. Spring Cloud Bus broadcast the change to 12 instances at once. Each instance recreated its connection pool, causing a thundering herd on the database. The old pool was closed before the new one was ready, leading to connection failures.@RefreshScope bean to drain old connections before creating new ones. Also implemented a staggered refresh using a custom event with a random delay.- Always test config changes with a canary instance first.
- Use a distributed rate limiter on external resources during refresh.
- Implement connection pool warmup after refresh.
- Consider using a 'draining' phase in your beans to avoid abrupt cuts.
- Monitor the refresh event flow — Bus can amplify mistakes instantly.
spring.cloud.bus.enabled=true. Look for 'Bus auto-configuration' in logs. Ensure the service has spring-cloud-starter-bus-amqp on classpath.RemoteApplicationEvent subclass with a delay field.curl -X POST http://localhost:8080/actuator/bus-refreshCheck logs for 'Received remote refresh request'spring.cloud.bus.enabled=true and restart.| File | Command / Code | Purpose |
|---|---|---|
| BusConfig.java | @Component | What is Spring Cloud Bus? |
| RefreshController.java | @RestController | Broadcasting Config Changes with Bus |
| CustomEventPublisher.java | @Service | Broadcasting Custom Events with Bus |
| SafeRefreshBean.java | @Component | What the Official Docs Won't Tell You |
| PollingConfig.java | @Component | Bus vs. Manual Refresh |
| BusTest.java | @SpringBootTest(properties = "spring.cloud.bus.enabled=false") | Testing Spring Cloud Bus in a Distributed Environment |
Key takeaways
Interview Questions on This Topic
How does Spring Cloud Bus propagate configuration changes?
/actuator/bus-refresh, the Config Server publishes a RefreshRemoteApplicationEvent. Each service instance listens on a unique queue bound to a topic exchange. Upon receiving the event, it refreshes all @RefreshScope beans, picking up new configuration values from the Config Server.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Spring Cloud. Mark it forged?
6 min read · try the examples if you haven't