Skip to content

A Beginner’s Guide to Dapr with Spring Boot, and integrating it in Azure

Photo by Glen Carrie on Unsplash

In our previous post, we explored what Dapr is, its core building blocks, and how it simplifies the development of distributed applications by abstracting away complex infrastructure concerns. We delved into its sidecar architecture, components, and best practices, showcasing why Dapr is a valuable tool for modern microservices. You should definitely check it out before reading further: Introduction to Dapr

In this blog post, we’ll take a hands-on approach to setting up a Spring Boot application integrated with Dapr. We’ll start by configuring Dapr in a local development environment, demonstrating its functionality using RabbitMQ for messaging and Redis for state management. Then, in the second part, we’ll take the application to the cloud, deploying both the Spring Boot app and Dapr on Azure. We’ll showcase how seamlessly Dapr enables switching backend components by replacing the Redis instance with Consul. By the end, you’ll have a fully functional, cloud-ready application leveraging Dapr’s powerful capabilities. Let’s get started!

Public Repositories

You can find all code mentioned in this post in the following repositories.

Spring Boot Demo Application

This repository contains a Spring Boot application showcasing how to set up a Spring Boot application using Dapr. It also provides guidance on running the application locally. Explore it here: Spring Boot Demo Application Repository

GitOps Repository

This repository includes the infrastructure code needed to deploy the Spring Boot application. It also covers setting up Dapr and integrating it with the necessary infrastructure components. Check it out here: GitOps Repository

Integrate Dapr in your Spring Boot Project

Let’s explore how to integrate Dapr into a Spring Boot application to leverage its building blocks. We’ll start by setting up a Spring Boot application and configuring Dapr for local development. This will provide a solid foundation before deploying the application to Azure.

Setting up your Spring Boot Project

Every great Spring Boot project begins at start.spring.io. Choose your dependencies (minimum requirements for this project are spring-boot-starter-web and spring-boot-starter-test), name your project, and import it into your preferred IDE. If you do not know it: This simple tool provides a boilerplate structure for your application, saving you time and effort.
To integrate Dapr with your Spring Boot project, add the following dependency to your pom.xml:

<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-springboot</artifactId>
<version>1.13.1</version>
</dependency>

Local Setup with Testcontainers

For local development, we’ll use Testcontainers, a library that simplifies the process of running test environments by managing lightweight, throwaway instances of containers. Testcontainers is, in my opinion, invaluable for developers, as it allows you to spin up really advanced mocks of databases, message brokers, and more, all within Docker containers, directly from your code.

With Testcontainers, you can avoid the manual setup of external services, ensuring your tests are reliable, isolated, and portable. Whether you’re working with Redis, Kafka, PostgreSQL, or even Dapr sidecars, Testcontainers makes integration testing and local development much easier. Learn more about Testcontainers here: testcontainers.com.

To start using Dapr locally with our Spring Boot application, we first need to add the needed dependencies for Testcontainers:

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
<dependency>
<groupId>io.diagrid.dapr</groupId>
<artifactId>testcontainers-dapr</artifactId>
<version>0.10.14</version>
</dependency>
<dependency>
<groupId>com.redis.testcontainers</groupId>
<artifactId>testcontainers-redis-junit</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.20.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>

 

Then we can configure the Dapr container with Testcontainers, which acts as the orchestrator between our application and the underlying infrastructure components. Below is the code snippet that achieves this, followed by a detailed explanation:

@Bean
public static Network daprNetwork = Network.newNetwork();

@Scope("singleton")
DaprContainer daprContainer() {
DaprContainer daprContainer = new DaprContainer("daprio/daprd:1.13.2")
.withAppName("local-dapr-app")
.withNetwork(daprNetwork)
.withComponent(new DaprContainer.Component("kvstore", "state.redis", getStateStoreProperties()))
.withComponent(new DaprContainer.Component("pubsub", "pubsub.rabbitmq", getPubSubProperties()))
.withAppPort(8080)
.withDaprLogLevel(DaprContainer.DaprLogLevel.debug)
.withAppChannelAddress("host.testcontainers.internal");
daprContainer.start();
org.testcontainers.Testcontainers.exposeHostPorts(8080);
System.setProperty("dapr.grpc.port", Integer.toString(daprContainer.getGRPCPort()));
System.setProperty("dapr.http.port", Integer.toString(daprContainer.getHTTPPort()));

return daprContainer;
}

 

There’s quite a lot happening here, so let’s go through it step by step.

Firstly, we define the version of Dapr we want to use with daprio/daprd:1.13.2. We then assign an application name (local-dapr-app) and specify the network. This network is essential because all containers — Dapr, the Spring Boot application, and supporting infrastructure — need to communicate with each other over it.
Next, we define two components using .withComponent(). If you’re unfamiliar with the concept of components in Dapr, I recommend checking out my previous blog post, where I explain them in more detail. Briefly, a component encapsulates the logic for a specific functionality and implements a standardized interface, making it interchangeable. For example, a state store component could use Redis or Consul — both adhere to the same interface, but their internal workings differ. These components collectively form the building blocks of Dapr’s architecture.
In our setup, we define two components:

  • Redis (state.redis), representing a state store, is identified by the name kvstore.
  • RabbitMQ (pubsub.rabbitmq), for pub/sub messaging, is referenced by the name pubsub.

The component type is specified by the value in parentheses. For example, “state.redis” and “pubsub.rabbitmq” indicate the underlying infrastructure component being used. Each component is identified by a unique name, as there can be multiple components of the same type. This name (e.g., “kvstore” for state management or “pubsub” for messaging) serves as an identifier, allowing Dapr to determine which specific component to interact with.
The third parameter in .withComponent() provides the configuration for each component. This includes details like the container’s address, credentials, and other properties. The complete file with all its configuration can be found in the mentioned repository.
Lastly, we configure ports and logging. The application port is set to 8080, Logging is set to debug for better visibility during development. Adapt these settings to your needs.

With the Dapr container correctly configured, the next step is to set up the container for Redis and RabbitMQ. These services will serve as the state store and pub/sub-messaging broker. Below is the code for initializing their corresponding Testcontainers:

@Bean
@Scope("singleton")
RedisContainer redisContainer() {
RedisContainer redisContainer = new RedisContainer(DockerImageName.parse("redis/redis-stack"))
.withNetworkAliases("redis")
.withNetwork(daprNetwork);
redisContainer.start();

org.testcontainers.Testcontainers.exposeHostPorts(8080);
return redisContainer;
}

@Bean
@Scope("singleton")
RabbitMQContainer rabbitMQContainer() {
var rabbit = new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine"))
.withNetworkAliases("rabbit")
.withNetwork(daprNetwork);
rabbit.start();

org.testcontainers.Testcontainers.exposeHostPorts(8080);
return rabbit;
}

 

Here, two Testcontainers are initialized — one for Redis and the other for RabbitMQ. The configuration is relatively straightforward:

  • Redis
    The Redis container uses the redis/redis-stack image. It is connected to the same network (daprNetwork) as the Dapr container. This ensures that Redis is discoverable and accessible by the Dapr sidecar for state management operations.
  • RabbitMQ
    Similarly, the RabbitMQ container utilizes the rabbitmq:3.7.25-management-alpine image. It is also attached to the same network. RabbitMQ serves as the messaging backend for the pub/sub building block.

Both containers are exposed to the host environment, allowing Dapr and the Spring Boot application to communicate with them without additional setup.

This minimal configuration ensures that Redis and RabbitMQ are operational and accessible as components in the Dapr runtime. With these foundational pieces in place, we are ready to integrate them into our Spring Boot application.

The full file with all the needed configurations can be found here.

Using a State Store with Dapr

In this section, we’ll demonstrate how to use Dapr to save and retrieve states in a Spring Boot application via REST endpoints. Dapr abstracts away the underlying state management logic, allowing you to focus solely on functionality without worrying about platform-specific implementation details.

We start by creating a REST controller:

@RestController
class MyDaprController {

private final Logger logger = org.slf4j.LoggerFactory.getLogger(MyDaprController.class);

private DaprClient daprClient;

@Value("${daprdemo.store.name}")
private String storeName;

@PostConstruct
public void init() {
daprClient = new DaprClientBuilder().build();
}
}


The DaprClient is initialized in the @PostConstruct method, which allows us to interact with Dapr’s APIs. The store name is fetched from an environment variable (STORE_NAME) defined in the application.yml file:

daprdemo.store.name=${STORE_NAME:kvstore}

Here, the default store name is kvstore, corresponding to the component configured earlier during the local setup.

To save a state, we define a POST endpoint:

@PostMapping("/state")
public void saveState(@RequestBody DaprState state) {
logger.info("Saving state: {}", state);
daprClient.saveState(storeName, state.key(), state.value()).block();
logger.info("State saved");
}

record DaprState(String key, String value) { }


This method takes a key-value pair and uses saveState to persist it. Dapr handles the underlying logic for securely storing the state according to best practices, whether it is Redis, Consul, or any other configured backend. It is just important that the “storeName” matches the one configured on the component.

We can retrieve the saved state using a GET endpoint:

@GetMapping("/state/{key}")
public String getState(@PathVariable String key) {
logger.info("Getting state for key: {}", key);
State<String> state = daprClient.getState(storeName, key, String.class).block();
logger.info("State retrieved: {}", state);
return state.getValue() == null ? "No state found" : state.getKey() + " - " + state.getValue();
}


This endpoint uses getState to fetch the value associated with a specific key. Dapr ensures seamless integration with the state store without requiring any platform-specific code.

Trying it out

Now that we’ve created REST endpoints for saving and retrieving state, let’s put them to the test. Start the application and verify that all the containers are up and running. You can check this using docker ps or by opening Docker Desktop. It should resemble the following:


If everything looks good, your application should be running on port 8080 (or the port you configured, then you need to adapt the following requests). Next, we’ll test the functionality step by step.

First, ensure the application is running and the endpoints are exposed. Try fetching a state that hasn’t been persisted yet. You should receive a response like “No state found”:

curl http://localhost:8080/state/andamp

Now, let’s save a state. Use the following command to persist a key-value pair:

curl -X POST http://localhost:8080/state -H "Content-Type: application/json" -d '{"key": "andamp", "value": "🤝 dapr"}'

Finally, try fetching the state again using the initial GET request:

curl http://localhost:8080/state/andamp

Nice, seems like we persisted our state.

This demonstration highlights how simple it is to manage state with Dapr. Developers can implement persistence and retrieval functionality without any knowledge of the underlying storage technology. Dapr handles everything for you.
Additionally, this local setup demonstrates how easy it is to run a complete environment with minimal configuration, making it an easy solution for local development.

Pub/Sub Messaging

Let’s explore another essential mechanism in the microservice ecosystem: pub/sub messaging. This pattern allows services to communicate asynchronously by sending messages with a topic, which other services can subscribe to and consume.

To publish messages, we’ll add a new REST endpoint that takes a message and a topic name as input:

@PostMapping("/messages")
public void publish(@RequestBody DaprMessage message) {
logger.info("Publishing message: {}", message);
daprClient.publishEvent("pubsub", message.topic(), message.message()).block();
logger.info("Message published");
}

record DaprMessage(String topic, String message) {}


Here, we use Dapr’s publishEvent method, which takes the name of the component (pubsub), the topic, and the message content. Dapr handles the rest, ensuring the message is routed correctly. The component name pubsub is the same as the one we configured in the local setup.
For those unfamiliar with pubsub messaging, the topic acts as a named channel where services can subscribe to receive messages. For more details, refer to Dapr’s documentation or most other pub/sub message brokers.

To receive messages, we need to implement another REST endpoint. Dapr forwards incoming messages to this endpoint:

@PostMapping("/subscriptions")
@Topic(name = "andamp", pubsubName = "pubsub")
public void subscribe(@RequestBody CloudEvent<String> event) {
logger.info("Received message: {}", event.getData());
}


At first glance, this looks like a typical REST endpoint. However, the @Topic annotation is Dapr-specific. It indicates:

  • name: The topic the service subscribes to (andamp in this case).
  • pubsubName: The pubsub component name (pubsub) configured earlier.

When Dapr receives a message published to the andamp topic, it automatically forwards it to this endpoint.

Dapr’s pub/sub functionality simplifies event-driven communication. Developers don’t need to deal with the underlying pub/sub-provider (e.g., RabbitMQ or Kafka) directly. Instead, they rely on standardized methods and annotations, making the code cleaner and the implementation portable.

Trying it out

Now that we’ve set up the necessary components for Dapr’s pub/sub messaging, it’s time to see it in action. We’ll use a simple REST request to trigger a message, and the service that subscribes to the topic will log the received message.

To trigger the publishing of a message to the topic andamp, you can use the following curl command:

curl -X POST --location "http://localhost:8080/pubsub" \
-H "Content-Type: application/json" \
-d '{
"topic": "andamp",
"message": "Hello from Dapr"
}'


After sending the request, check your application logs for the following message:

Received message: Hello from Dapr


This confirms that the message was successfully sent to the andamp topic and received by your service.

Congratulations! You’ve successfully set up Dapr’s pub/sub messaging locally. You’ve now seen how easy it is to send and receive messages between services using Dapr, without worrying about the underlying messaging infrastructure.

Deploying your Spring Boot Application to Azure using GitOps

Setting up Azure and AKS

Before we deploy your Spring Boot application with Dapr to Azure, you need to have a working Azure Kubernetes Service (AKS) cluster. If you’re unfamiliar with how to set up AKS, I highly recommend checking out my previous blog post on how to quickly set up a modern Kubernetes cluster in Azure.

In that post, I will guide you through the process of setting up AKS in Azure, creating an environment for your containerized applications, and deploying them easily. Once you have AKS set up, you’re ready to move on to deploying your Spring Boot application and Dapr.

Deploying Dapr

The process to deploy Dapr in AKS is quite straightforward since Dapr is available as an extension in AKS. This integration automatically sets up all necessary components for your microservices.
For detailed instructions on how to deploy Dapr within AKS, including installing the extension, refer to the official Azure documentation here: Deploying Dapr in AKS. This guide provides all the information you need to get started and manage your Dapr-enabled services on AKS.

Deploying Third-Party Infrastructure Components

To test our Spring Boot application in the cloud with real infrastructure components, we will use Consul and Redis as the infrastructure for state storage (But feel free to use any other supported provider). In this case, the main focus will be on testing the state store functionality, where Dapr will interact with these external services to store and retrieve state data. By deploying these services in a cloud environment, we can simulate a production-like setup for testing purposes, ensuring that our application interacts with real, cloud-native systems, as opposed to local Testcontainers.

Consul

Consul provides a distributed key-value store for managing states in Dapr. When deployed, it can act as the persistent state store for your Dapr-powered microservices. We will configure Consul as a state store component within Dapr.

Here’s the configuration to set up the Consul state store component:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: consul-statestore
namespace: ows-engl-namespace
spec:
type: state.consul
version: v1
metadata:
- name: datacenter
value: <datacenter-name>
- name: httpAddr
value: <consul-http-address>


This code can also be found in this commit.

With Consul deployed, Dapr will use it to persist state information. Again, Dapr will identify the store to use by the name, here consul-statestore

To deploy Consul, refer to the Consul Installation Guide.

Redis

Redis, a fast, in-memory key-value store, is also a popular choice for state management in cloud-native applications. In Dapr, Redis can be used as a state store for high-performance state persistence.
To configure Redis as a Dapr state store, use the following configuration:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: redis-statestore
namespace: ows-engl-namespace
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: <redis-host>
- name: redisPassword
value: <redis-password>
- name: redisType
value: <redis-type>


This code can also be found in this commit.

You can deploy Redis using Azure Redis Cache, which is a fully managed Redis service on Azure. Alternatively, you can deploy your own Redis instance in AKS. For detailed instructions, check the Azure Redis Cache Documentation.

Deploying our Spring Boot Application

Now that the infrastructure is set up, we can proceed with deploying our Spring Boot application to Azure Kubernetes Service (AKS). To do this, we need to build the application into a Docker image. One efficient and easy way to accomplish this is by using Buildpacks, which automatically optimizes the image for us.

To build the application image using Buildpacks, we can run the following command in the terminal:

mvn spring-boot:build-image


This will generate a Docker image optimized for running in Kubernetes or other containerized environments. Just don’t forget to remove the @Component annotation in the Testcontainer class so we do not deploy our Testcontainers.

Alternatively, if you prefer to skip the build process or want to use a pre-built image, you can use the following publicly available image:

jonasenglandamp/daprdemo:0.0.1-SNAPSHOT


To deploy our Spring Boot application to AKS, we need to create a Kubernetes Deployment YAML file. This file defines the deployment, replicas, and containers for the application.

Here’s the Deployment YAML configuration:

apiVersion: apps/v1
kind: Deployment
metadata:
name: ows-engl-dapr-demo
namespace: ows-engl-namespace
labels:
app: my-demo-app
spec:
replicas: 1
selector:
matchLabels:
app: my-demo-app
template:
metadata:
labels:
app: my-demo-app
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "jonas-dapr-app"
dapr.io/app-port: "8080"
spec:
containers:
- name: my-demo-app
image: jonasenglandamp/daprdemo:0.0.1-SNAPSHOT # Your application's Docker image
ports:
- containerPort: 8080
env:
- name: STORE_NAME
value: consul-statestore
- name: alpine
image: alpine/curl
command:
- "sh"
- "-c"
- |
apk add --no-cache redis && \
while true; do sleep 600000; done


The annotation dapr.io/enabled: “true” is essential for enabling Dapr in the application and spinning up the sidecars. The annotation dapr.io/app-id: “jonas-dapr-app” sets a unique application identifier for Dapr, allowing it to manage communication and interactions with other services in the ecosystem. Additionally, specifying dapr.io/app-port: “8080” ensures that the application listens on port 8080, which is necessary for Dapr’s service discovery and communication features.

The STORE_NAME environment variable is used to configure the state store component. In this example, it points to consul-statestore, but it can be adjusted to redis-statestore depending on whether you’re using Redis or Consul as the backend for storing states. This provides flexibility in choosing the underlying technology without needing to modify the application logic itself. You can find all the code in this commit.

Trying it out

Now that everything is running, we can try out the same requests as before and verify that the values are indeed saved in the infrastructure components.

Consul

Since we’ve deployed our Spring Boot application configured with Consul, we can start directly with it. However, before we proceed, we need to identify the IP address of our service. You can get this by running commands like kubectl -n $NAMESPACE get svc or checking the Azure portal for the external IP. If you’ve deployed an Alpine sidecar with your Spring Boot application, like me (check the deployment yaml from before), you can make the calls directly from there. Otherwise, you may need to expose the service using a LoadBalancer or similar mechanism.

Once you have the service IP, you can interact with the application. After making the requests, you can switch to Consul to check whether the values are saved:

To check values in Consul:

  1. Access the Consul UI (typically at http://<consul-ip>:8500).
  2. Navigate to the “Key-Value” section.
  3. Search for the keys that you’ve set via your REST endpoint. You should see the values you saved through your application.

For more detailed steps on checking key-value pairs in Consul, refer to the official Consul documentation or access the UI via the provided URL.

Redis

Switching to Redis is as simple as changing the environment variable in our application. You can modify the STORE_NAME environment variable to point to redis-statestore, and the application will now write to Redis instead of Consul — all without any code changes.

To switch to Redis:

  1. Change the STORE_NAME environment variable to redis-statestore.
  2. Redeploy the application with the updated configuration.

To access Redis:

  • You can connect to the Redis instance using redis-cli (or another Redis client).
  • Use commands like GET <key> to check if the saved state is present in Redis.
  • For a more thorough guide on accessing Redis, check the Redis documentation.

This demonstrates the simplicity of using Dapr to switch between different state store providers like Consul and Redis without modifying the core logic of your application.

Key Takeaways

Dapr provides a powerful yet simple framework for building microservices and distributed applications. It abstracts away much of the complexity associated with managing inter-service communication, state management, pub/sub messaging, and more. Below are some of the key takeaways that highlight why Dapr might be a good choice for developers looking to build scalable, maintainable, and portable microservice-based applications.

  • Easy to Use Across Frameworks: Dapr is designed for simplicity and can be integrated not just with Spring Boot but with many other frameworks. This flexibility helps streamline development regardless of the technology stack you are using. For more information on frameworks supported by Dapr, check out this link.
  • Best Practices Out of the Box: Dapr encourages the use of best practices, such as the sidecar pattern for microservice communication and a consistent way of managing state, pub/sub messaging, and more. It simplifies the complexity of building distributed applications with minimal configuration.
  • Infrastructure Changes Are Simple: Switching between different infrastructure components (like state stores or messaging systems) is straightforward. With Dapr, changing your backend doesn’t require rewriting the application logic. For example, switching from Consul to Redis as a state store only requires modifying the configuration without touching your code.
  • Easy Local Development: With Dapr, it’s simple to spin up local environments for development and testing. You can easily run your components using Docker or Testcontainers, mimicking a cloud-like environment on your local machine. This makes local development a breeze, without worrying about external dependencies.

If you want to dive deeper into other use cases and see how Dapr can fit your projects, feel free to contact me. We also organize workshops where you can experience Dapr in action!