Improve developer joy and productivity with Quarkus and Dapr
TL;DR
If you are already using Quarkus, you are likely familiar with the incredible developer experience it offers through features such as DevServices, DevUI, and live reload. You probably appreciate how these features enhance your experience.
Now, imagine taking that developer joy to the next level when building complex distributed cloud native applications by integrating it with the productivity and standardization provided by Dapr. While Quarkus optimizes the experience for the development process of a single application, Dapr focuses more on providing best practices and well-known patterns to help developers to build distributed applications.
In this blog post, we will explore what Dapr is and how to use it in combination with the Quarkus framework.
What is Dapr?
Dapr stands for Distributed Application Runtime:
Dapr is a portable, event-driven runtime that makes it easy for any developer to build resilient, stateless, and stateful applications that run on the cloud and edge and embraces the diversity of languages and developer frameworks.
In my opinion, what makes Dapr truly remarkable is the abstraction layer and standardization it provides in the shape of building blocks when integrated into your architecture.
Dapr Building Blocks
So, what are Dapr building blocks? Essentially, building blocks are APIs accessed over the network through HTTP or gRPC calls. In this post, I will discuss in details only two building blocks, including: Publish and Subscribe, State Management.
About the diagram
The diagram above illustrates what we will implement in the following sections, using an in-memory State Store and Pub/Sub for simplicity. However, Dapr is not limited to these options and supports a wide range of state stores and messaging systems.
Remember when I mentioned abstraction layer?
When you use the State Management building block, you are interacting with the Dapr runtime to store or retrieve data from a data store. This state store can be AWS DynamoDB, Azure CosmosDB, Redis, Cassandra, Firebase, and more.
The same principle applies to Publish and Subscribe. You interact with the Dapr API, and Dapr takes care of communication with the message broker on your behalf. You can take a look at all the PubSub supported implementations here.
Dapr also provides other useful building blocks:
- Service Invocation: Perform resilient (retries and circuit breakers), secure (mtls), service-to-service method calls.
- Workflow: Orchestrate logic across various microservices
- State management: Create long running stateful services by persisting and retrieving data
- Bindings: Integrate reliably with or be triggered from external systems
- Actors: Encapsulate code and data in reusable actor objects as a common microservices design pattern
- Secrets management: Securely access secrets from your application
- Configuration: Manage and be notified of application configuration changes
- Distributed lock: Distributed locks provide mutually exclusive access to shared resources from an application. No need to add new libraries to your application or new components in the infrastructure.
- Cryptography: Perform cryptographic operations without exposing keys to your application
- Jobs: Manage the scheduling and orchestration of jobs
Ufa!
How do you configure all these abstractions and integration points? Let’s look at Dapr Components.
Dapr Components
Components serve as configurations for building blocks and applications. With components, you can define specific behaviors and characteristics when utilizing a building block.
If you're an experienced developer, you might be asking: Do I need to configure retries, dead letter queues, and resilience features if I don't have the Kafka API library to set up?
Getting Pub/Sub building block (using Kafka) as example, you can define routes to your topic:
apiVersion: dapr.io/v2alpha1
kind: Subscription
metadata:
name: myevent-subscription
spec:
type: pubsub.kafka
pubsubname: pubsub
topic: inventory
routes:
rules:
- match: event.type == "widget"
path: /widgets
- match: event.type == "gadget"
path: /gadgets
default: /products
scopes:
- app1
- app2
Getting State management as example, you can enable transaction outbox pattern, using the component specification:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: mysql-outbox
spec:
type: state.mysql
version: v1
metadata:
- name: connectionString
value: "<CONNECTION STRING>"
- name: outboxPublishPubsub # required
value: "mypubsub"
- name: outboxPublishTopic # required
value: "newOrder"
- name: outboxPubsub # optional
value: "myOutboxPubsub"
- name: outboxDiscardWhenMissingState # optional, defaults to false
value: false
There are a bunch of configurations and Component types, to see a more detailed view, see the official documentation for each building block.
What is Quarkus?
Quarkus is a modern, open-source Java framework designed for building cloud-native applications. It optimizes Java specifically for Kubernetes and the cloud, providing a powerful solution for developing microservices and serverless applications.
There are some benefits, I will list what makes sense for me actually:
-
Native Compilation: Quarkus prepare and allows you to generate a native image for you Operational System, without the need to have a JVM running.
-
Live Reload: When I am working with another frameworks and languages, I need to
run
>change
>stop
andrun
... in a loop. With Live reload I am happy, the feedback loop is very fast. -
Developer Joy: With DevService and DevUI our life as developer is amazing, we do not nee more to access the Docker image documentation for a database, to copy and pase a
docker-compose.yml
file from a project to another... You just need to add an extension with DevService and your infrastructure is ready to use. -
Subatomic and Supersonic: Thanks to Quarkus's mission, uses significantly less resources (CPU/Memory) than traditional approaches.
If you want to see more benefits about Quarkus, see the official documentation.
Creating you Quarkus application with Dapr
Let's create our Quarkus application with Quarkus CLI.
The previous command creates a Quarkus application with ìo.quarkiverse.dapr:quarkus-dapr
extension.
Running the application
Before configuring the DevService for running Dapr, let's run our Quarkus application:
The previous command runs the Quarkus application in dev mode.
By default, the quarkus.dapr.devservices.enabled
is set to false. This property indicates wether the DevService for Dapr extension is enabled or not. Let's enable!
Configuring the application
Using your browser access the DevUI Configuration. You will filter by quarkus.dapr.devservice.enabled
and check the checkbox.
Looking the changes
If you look the application.properties
file, you will se the changes made by DevUI configuration page.
After this simple change, if you access your terminal and type docker ps
you will se that you have a container image running.
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7e32f9223bd9 docker.io/testcontainers/sshd:1.2.0 echo ${USERNAME}:... 2 minutes ago Up 2 minutes 0.0.0.0:38461->22/tcp, 22/tcp blissful_solomon
2dd0127159a8 docker.io/daprio/daprd:latest ./daprd -app-id l... 2 minutes ago Up 2 minutes 0.0.0.0:33613->3500/tcp, 0.0.0.0:42571->50001/tcp, 3500/tcp, 50001/tcp focused_fermi
The daprio/daprd
container is the Dapr sidecar, configured by the Dapr DevService.
Behind the scenes
Behind the scnes the Quarkus Dapr extension uses TestContainer API to create a container with the following command.
withCommand(
"./daprd",
"-app-id", appName,
"--dapr-listen-addresses=0.0.0.0",
"--app-protocol", "http",
"-placement-host-address", placementService + ":50006",
"--app-channel-address", appChannelAddress,
"--app-port", Integer.toString(appPort),
"--log-level", daprLogLevel.toString(),
"-components-path", "/components");
See the daprd
reference here.
Using Pub/Sub
Now, we have all necessary things to use Dapr with Quarkus - We have a Dapr Sidecar container running and our Quarkus application running/listening changes.
We will create a ProductResource
responsible for publishing message through SyncDaprClient
.
package dev.matheuscruz.product;
import io.quarkiverse.dapr.core.SyncDaprClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Random;
@Path("/products")
public class ProductResource {
static final String DEV_SERVICE_PUBSUBE_NAME = "pubsub"; // (2)
@Inject
SyncDaprClient syncDaprClient; // (1)
@POST
public Response create() {
List<String> products = List.of("mouse", "gpu", "cpu");
int random = new Random().nextInt(products.size());
syncDaprClient.publishEvent(DEV_SERVICE_PUBSUBE_NAME, "products.new", // (3)
products.get(random)); // (4)
return Response.accepted().build();
}
}
- The Dapr client API responsible for perform calls to Dapr Sidecar.
- The Pub/Sub component name, by default Quarkus Dapr extension creates a Pub/Sub component with
pubsub
name. - The topic name.
- The message that will be sent.
By default, Quarkus Dapr extension uses a in-memory Pub/Sub and State Store components, but you can declare it through src/main/resources/components
folder.
Consuming events
To make our demostration more simple, let's consume the event sent in the same application.
package dev.matheuscruz.product;
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@Path("/handlers")
@ApplicationScoped
public class ProductCreatedHandler {
@POST
@Topic(name = "products.new", pubsubName = "pubsub") // click (1) to see more
@Path("/products") // (2)
public void handle(CloudEvent<String> event) {
System.out.println("Received: " + event.getData());
}
}
-
The
@Topic
annotation is responsible for mapping an endpoint to a topic, the@Topic#name
is the name of the topic and the@Topic#pubsubName
is the name of the Pub/Sub component. -
This is just to get your topic mapped, you can set any string on
@Path#value
, it can bebanana
if you want.
Testing the Pub/Sub
If we make a request to /products
:
We can see the following log in our application:
Using State Management
Let's change our ProductCreatedHandler
to use State Management building block.
package dev.matheuscruz.product;
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import io.dapr.client.domain.State;
import io.dapr.utils.TypeRef;
import io.quarkiverse.dapr.core.SyncDaprClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@Path("/handlers")
@ApplicationScoped
public class ProductCreatedHandler {
@Inject
SyncDaprClient syncDaprClient;
@POST
@Topic(name = "products.new", pubsubName = "pubsub")
@Path("/products")
public void handle(CloudEvent<String> event) {
System.out.println("Received: " + event.getData());
try {
State<Product> state = syncDaprClient.getState("kvstore",// (1)
event.getData(), TypeRef.get(Product.class));
System.out.println("We already have a product with name: " + state.getValue());
} catch (Exception e) {
System.out.println("We do not have a product :(");
// Let's create
syncDaprClient.saveState("kvstore", event.getData(), new Product(
event.getData()
)); // (2)
}
}
public record Product(String name) {}
}
-
Trying to get the product, the
kvstore
is the state store component name created by Quakus Dapr extension, this is a in-memory state store. -
Using Dapr client to save a product.
Testing the state store
If we make some requests to /products
endpoint:
We can see the following log in our application:
Received: book
We do not have a product :(
Received: mouse
We do not book a product :(
Received: cpu
We already have a product with name: Product[name=book]
Received: book
We already have a product with name: Product[name=book]
Post Summary
In this post, we explored how to combine Quarkus and Dapr to enhance the developer experience when building distributed and cloud-native applications. We started by reviewing what Dapr is—a portable, event-driven runtime that provides building blocks like State Management and Pub/Sub, offering abstraction and standardization to simplify integrations with databases, message brokers, and other services.
We also discussed Quarkus, a modern Java framework optimized for the cloud and Kubernetes, highlighting its benefits such as native compilation, live reload, and DevServices. We demonstrated how to create a Quarkus application integrated with Dapr, configure DevServices to enable a Dapr sidecar, and use the Pub/Sub and State Management building blocks through practical examples.
Below, you can find some links to documentation and sample code to help deepen the concepts presented. This post makes it clear how combining Quarkus and Dapr can boost productivity and standardize the development of distributed applications.
References
- https://docs.quarkiverse.io/quarkus-dapr/dev/index.html
- https://quarkus.io/
- https://docs.dapr.io/reference/arguments-annotations-overview/
- https://docs.dapr.io/concepts/
Source Code
If you want to see a real code that uses Dapr, see the following repositories:
Thank you
That's all; thank you for reading! See you in the next post. Goodbye! 👋