Enterprise Java

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.name value 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.

Download
You can download the full source code of this example here: dapr workflows pubsub

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button