Pub/Sub Setup Using Spring Boot and Dapr
Building event-driven applications is a common requirement in modern software systems. Pub/Sub messaging helps services communicate asynchronously, improves scalability, and reduces tight coupling between components. However, managing infrastructure, message brokers, retries, and failures can quickly become complex.
Dapr (Distributed Application Runtime) simplifies Pub/Sub messaging by providing a consistent API that works across different message brokers. In this article, we explore how to build a Pub/Sub messaging system using Spring Boot and Dapr.
1. What Is Pub/Sub Messaging?
Publish/Subscribe (Pub/Sub) messaging is a communication pattern where producers publish messages to a topic, and consumers subscribe to that topic to receive messages. Producers and consumers do not need to know about each other, which enables loose coupling and independent scaling.
With Dapr, Pub/Sub becomes infrastructure-agnostic. Our application code does not change whether we use Redis, RabbitMQ, Kafka, Azure Service Bus, or another broker. Dapr handles message delivery, retries, and subscriptions through simple configuration.
2. Why Use Dapr with Spring Boot?
Spring Boot is widely used for building production-ready Java applications, while Dapr provides essential building blocks for distributed systems, including Pub/Sub messaging. When used together, they allow us to focus on application logic instead of infrastructure details.
By including the Dapr Spring Boot starter, there is no need to add broker-specific client libraries or dependencies to the application. Messaging infrastructure is defined externally through Dapr components, making it easy to switch between brokers such as Redis, RabbitMQ, or Kafka without changing any application code.
Provider-specific features and settings can also be configured at the component level, keeping the application clean and portable across different environments.
3. Architecture Overview
The example application in this article follows a simple event-driven flow in which a REST API accepts a request, publishes an event to a Pub/Sub topic, and a subscriber consumes that event, which could trigger a workflow to orchestrate further processing steps.
Setting Up the Project
We begin by creating a Spring Boot project using Maven and adding the necessary dependencies.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>1.16.0</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>1.16.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.redis</groupId>
<artifactId>testcontainers-redis</artifactId>
<version>2.2.2</version>
<scope>test</scope>
</dependency>
This configuration enables Spring Web for REST APIs, Dapr integration for Pub/Sub, and testing support using Testcontainers. Redis is used during tests to simulate a real message broker.
Configuring a Redis StateStore Component
Dapr requires component definitions to connect to infrastructure. We will create a Redis-backed state store named messagestatestore.
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: messagestatestore
namespace: default
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: enableTLS
value: false
Configuring Dapr Pub/Sub via application.properties
dapr.pubsub.name=messagestatestore
This property defines the logical Pub/Sub component name that our application will use. Dapr will map this name to an actual Pub/Sub component definition.
4. Creating the Dapr Messaging Configuration
To keep our messaging setup clean and reusable, we define a dedicated configuration class that exposes a DaprMessagingTemplate bean. This allows us to centralize Pub/Sub configuration and inject the template wherever publishing is required, without repeatedly referencing configuration properties across the application.
@Configuration
@EnableConfigurationProperties({DaprPubSubProperties.class})
public class DaprPubSubConfig {
@Bean
public DaprMessagingTemplate<String> pubSubTemplate(DaprClient daprClient, DaprPubSubProperties pubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, pubSubProperties.getName(), false);
}
}
This configuration class enables binding of DaprPubSubProperties from application.properties, injects the configured Pub/Sub component name into the application context, creates a DaprMessagingTemplate bean for publishing messages, and centralizes all messaging-related configuration in a single, reusable location.
Now, any component can inject DaprMessagingTemplate and publish messages without worrying about the Pub/Sub name.
5. Implementing the Publisher
The Publisher exposes an HTTP endpoint that publishes messages to a Dapr topic.
@RestController
@RequestMapping("/publisher")
public class NotificationPublisher {
private final DaprMessagingTemplate<String> daprTemplate;
public NotificationPublisher(DaprMessagingTemplate<String> daprTemplate) {
this.daprTemplate = daprTemplate;
}
@PostMapping
public ResponseEntity<String> publish(@RequestBody String message) {
daprTemplate.send("notifications", message);
return ResponseEntity.ok("Notification published successfully");
}
}
Here, we inject DaprMessagingTemplate<String> through the constructor to enable message publishing, use the send(topic, message) method to publish events to a specific topic, rely on the configured Pub/Sub component name resolved automatically from application properties, and keep the Publisher entirely decoupled from Redis or any underlying broker configuration.
6. Implementing the Subscriber
Now we create a Subscriber that listens to the same topic.
@RestController
public class NotificationSubscriber {
private volatile String lastReceivedMessage;
@Topic(name = "notifications", pubsubName = "messagestatestore")
@PostMapping("/notifications")
public void receive(@RequestBody String message) {
this.lastReceivedMessage = message;
System.out.println("Received notification: " + message);
}
public String getLastReceivedMessage() {
return lastReceivedMessage;
}
}
This class uses the @Topic annotation to explicitly subscribe to the notifications topic through the messagestatestore Pub/Sub component, ensuring the correct binding between the topic and the configured messaging infrastructure.
By annotating the /notifications endpoint, the subscriber automatically registers its subscription with Dapr during application startup, and whenever a message is published to the notifications topic, Dapr routes the incoming event to this endpoint for processing.
7. Bootstrapping Dapr and Redis with Testcontainers
Now we remove manual infrastructure setup by bootstrapping everything using Testcontainers.
@TestConfiguration(proxyBeanMethods = false)
@EnableConfigurationProperties({DaprPubSubProperties.class})
public class DaprTestConfiguration {
@Value("${server.port}")
private int serverPort;
@Bean
public Network daprNetwork() {
return Network.newNetwork();
}
@Bean
public GenericContainer<?> redisContainer(Network daprNetwork) {
return new GenericContainer<>(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379)
.withNetworkAliases("redis")
.withNetwork(daprNetwork);
}
@Bean
@ServiceConnection
public DaprContainer daprContainer(
Network daprNetwork,
GenericContainer<?> redisContainer,
DaprPubSubProperties pubSub) {
Map<String, String> redisConfig = new HashMap<>();
redisConfig.put("redisHost", "redis:6379");
redisConfig.put("redisPassword", "");
return new DaprContainer("daprio/daprd:1.16.0")
.withAppName("dapr-pubsub")
.withNetwork(daprNetwork)
.withComponent(
new Component(
pubSub.getName(),
"pubsub.redis",
"v1",
redisConfig))
.withAppPort(serverPort)
.withAppChannelAddress("host.testcontainers.internal")
.withDaprLogLevel(DaprLogLevel.INFO)
.withLogConsumer(outputFrame
-> System.out.println(outputFrame.getUtf8String()))
.dependsOn(redisContainer);
}
}
This configuration does several important things:
- Creates an isolated Docker network so Redis and Dapr can communicate internally without exposing unnecessary ports externally.
- Bootstraps a Redis container using the official Redis image and assigns it the network alias
redis. - Programmatically registers a Dapr Pub/Sub component using
pubsub.redis - Uses the configured
dapr.pubsub.namevalue to create the component dynamically. - Connects the Dapr sidecar to the Spring Boot application using the defined
server.port. - Enables structured logging with
.withDaprLogLevel(DaprLogLevel.INFO)and streams container logs for debugging. - Ensures startup order with
.dependsOn(redisContainer)so Redis is ready before Dapr initializes.
By defining the Dapr component programmatically, we gain flexibility in tests. The Pub/Sub backend can be swapped by changing only the container configuration, while the application code remains untouched.
With this setup in place, running integration tests automatically provisions a Redis instance, a Dapr sidecar configured with Redis Pub/Sub, and a Spring Boot application connected to both services, eliminating the need for external dependencies or manual CLI commands and making the architecture portable, reproducible, and well-suited for continuous integration pipelines.
8. Running the Application
Once the infrastructure and messaging configuration are in place, we can start the application in a test-driven environment using the following entry point:
public class TestDaprWorkflowPubsubDemoApplication {
public static void main(String[] args) {
SpringApplication.from(DaprWorkflowPubsubDemoApplication::main)
.with(DaprTestConfiguration.class)
.run(args);
}
}
We can run the application with:
mvn spring-boot:test-run
This command starts the application, automatically starts the Redis container and the Dapr sidecar, and injects the configured Pub/Sub components into the application context. No additional setup or manual Dapr startup is required.
9. Conclusion
In this article, we built a fully containerized testing setup for Spring Boot using Dapr, Redis, and Testcontainers. We configured a Redis-backed Pub/Sub component and wired the Dapr sidecar into our test environment, demonstrating how to run and validate an event-driven application entirely within a self-contained setup.
10. Download the Source Code
This article explored how to use Dapr workflows with PubSub for orchestrating event-driven applications.
You can download the full source code of this example here: dapr workflows pubsub




