Background and inspiration

I had the opportunity to attend KubeCon 2025 in London, and it was inspiring to see the momentum in the Kubernetes ecosystem. As expected, AI was a major theme. But what really stood out was the widespread focus on OpenTelemetry. Observability and monitoring are clearly top priorities in modern infrastructure.

That said, I felt Developer Experience (DX) didn’t receive quite as much attention as it deserves. As Platform Engineers, it’s easy to get caught up in solving the challenges that affect us, like infrastructure automation, scaling, or operational tooling. But we are not the end users of the platform, we build it for developers.

It’s essential to remember that developers are the ones shipping the features that drive business value. Our role should be to support them by reducing friction, removing blockers, and enabling them to stay in a productive flow state. A good platform shouldn’t just be robust, it should be intuitive and enabling for its users.

Improving DX requires us to step out of our own bubble and see the platform from the developers’ perspective. That means understanding their pain points and proactively designing solutions that simplify their workflows.

One project that caught my eye at KubeCon was Dapr. It provides a set of building blocks that abstract common application concerns — like service-to-service communication, state management, and pub/sub messaging. I see potential in Dapr to streamline development workflows and improve the overall experience for teams building on top of our platform. I’m looking forward to exploring how we can leverage it to deliver more value to our developers.

In this blog post, I’ll specifically focus on the pub/sub messaging abstraction that Dapr offers.

Introduction to Dapr

So what is Dapr? Microsoft originally created Dapr—the Distributed Application Runtime (Dapr) in 2019 as an open‑source incubated project to simplify the construction of cloud‑native and microservices applications. In 2021, Microsoft donated Dapr to the Cloud Native Computing Foundation (CNCF), which now manages it, and the project attained graduated status in November 2024.

Quoted from their website, Dapr describes itself as:

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. Leveraging the benefits of a sidecar architecture, Dapr helps you tackle the challenges that come with building microservices and keeps your code platform agnostic.

In my opinion the most important aspect of Dapr, at least in the context of pub/sub messaging lies at the end of that statement: "... keeps your code platform agnostic". Dapr allows our application to integrate with a pub/sub messaging system in an agnostic way. Meaning your application doesn’t care about the messaging system in use. In fact, your application doesn’t even know the message broker that you are using, it’s all abstracted away by Dapr.

Why that is great? Well, imagine you have a decoupled application with let’s say ten different microservices. Now all of them have to integrate tightly with the message broker that you are planning to use, be it Redis, Kafka, AWS SNS/SQS or Azure Event Hubs. The more microservices you have, the more boilerplate code you have in your applications. Sure, you can create your own library that acts as a bridge to these components, but now you have even more code to maintain. Also think about companies that have multiple teams, each implementing different solutions. The amount of code that has to be written, tested, deployed and maintained to interact with these message brokers can grow quite fast - and is prone to contain bugs.

Wouldn’t it be great that all this logic can be decoupled from the application? Say you want to migrate from Redis to AWS SNS, because you want to make use of Public Cloud services instead of managing the infrastructure yourself. You now have a tricky migration to deal with, Dapr takes away these issues by decoupling the pub/sub integration from the application.

The technical stuff

Target architecture

To visualize the abstraction layer provided by Dapr when using the pub/sub components, I have created the following diagrams. In this blog post we will use the following two services as examples: The checkout service and the order-processor service. The checkout service will produce message, while the order-processor service will consume the messages.

The first diagram will visualize a traditional approach, where the application directly integrates with the messaging broker. In this example I’ll use Redis, which I will later on use in my Dapr PoC as well. While there is nothing wrong with this approach, it doesn’t scale that well and requires to implement timeouts, retries and circuit breakers manually, more on that later.

Traditional

Dapr introduces a sidecar pod that will be running next to your application pod. From that pod, the connections from/to the messaging broker will be made. Using Dapr, only Pods requiring access to the broker will actually start the sidecar pod.

Dapr

This post focuses on Dapr’s pub/sub functionality, although Dapr offers many more capabilities that are beyond the scope of this post. For me personally, I find the pub/sub functionality the most interesting.

Installing Dapr

First of all, we have to install Dapr in our Kubernetes cluster. Dapr offers a Helm Chart and the installation instructions can be found here. I’m using Argo CD in my local environment and am deploying the following ApplicationSet. By the time you are reading this, there might be a newer version of the Chart available.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
---
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: dapr
  namespace: argocd
spec:
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    - clusters: {}
  template:
    metadata:
      name: "dapr"
    spec:
      project: default
      source:
        repoURL: https://dapr.github.io/helm-charts/
        chart: dapr
        targetRevision: "1.15.6"
      destination:
        server: "{{.server}}"
        namespace: dapr
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - PrunePropagationPolicy=background
          - PruneLast=true
          - RespectIgnoreDifferences=true
          - ServerSideApply=true
          - ApplyOutOfSyncOnly=true
          - FailOnSharedResource=true
          - CreateNamespace=true

Great! Let Argo CD do it’s work and deploy all the resources. To actually make use of Dapr, we have to generate some events, that means we have to deploy resources to our cluster that does all of that. In the next chapter, we will create two sample applications - the checkout and order-processor microservices.

Dapr Component

A Dapr component is like a plug-in that tells Dapr how to interact with external systems. It basically instructs Dapr that it can use Redis as a pubsub broker. Configuration of a Component is really simple, a Custom Resource is provided by Dapr.

Note: this component must be created in both the checkout and order-processor namespaces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
---
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: orderpubsub
  namespace: <checkout,order-processor>
spec:
  type: pubsub.redis
  version: v1
  metadata:
    - name: redisHost
      value: redis-master.redis.svc:6379
    - name: redisPassword
      value: ""

I’m running a HA Redis cluster, provided by the Bitnami chart for Redis. Setting up the cluster is left as an exercise for the reader. Inside my Kubernetes cluster, Redis is exposed by a Service that is reachable on redis-master.redis.svc:6379. I’ve disabled authentication in my Redis setup to not make things more complicated than necessary.

Two sample applications

Dapr provides two ‘quickstart’ applications to get up to speed with Dapr. You can find the source for both sample applications at the following links:

I highly recommend looking at the source code to be able to fully understand how Dapr works, I’ll highlight the most important lines of codes below.


Producer - checkout

The checkout microservice is our producer and will publish messages to the broker. But instead of integrating directly with the broker, it instead sets up a Dapr client using the SDK.

First, we define constants to specify which Dapr component and topic the application should publish messages to. Note that the pubsubComponentName matches the name of the Component we’ve created in the previous chapter.

1
2
3
4
const (
    pubsubComponentName = "orderpubsub"
    pubsubTopic         = "orders"
)

After the client has been configured, we generate 10 fake “orders” and use the client.PublishEvent function, provided by the Dapr SDK, to send messages to the broker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Create a new client for Dapr using the SDK
client, err := dapr.NewClient()

// Publish events using Dapr pubsub
for i := 1; i <= 10; i++ {
    order := `{"orderId":` + strconv.Itoa(i) + `}`

    err := client.PublishEvent(context.Background(), pubsubComponentName, pubsubTopic, []byte(order))
    if err != nil {
        panic(err)
    }

    fmt.Println("Published data:", order)

    time.Sleep(time.Second)
}

Consumer - order-processor

Now let’s jump over to the consumer side, the order-processor service, where things look pretty similar.

The consumer will configure the subscription configuration as follows

1
2
3
4
5
var sub = &common.Subscription{
  PubsubName: "orderpubsub",
  Topic:      "orders",
  Route:      "/orders",
}

Next, we start a Dapr service on a specified port. This server will listen for events coming from Dapr clients, in our case, the checkout service.

1
2
3
4
5
6
// Create the new server on appPort and add a topic listener
s := daprd.NewService(":" + appPort)
err := s.AddTopicEventHandler(sub, eventHandler)
if err != nil {
    log.Fatalf("error adding topic subscription: %v", err)
}

We start an event handler that will trigger the eventHandler function every time a client sends messages to the orders topic.

The last step is printing out the message received.

1
2
3
4
func eventHandler(ctx context.Context, e *common.TopicEvent) (retry bool, err error) {
    fmt.Println("Subscriber received:", e.Data)
    return false, nil
}

Now we understand how the pub and sub components communicate with each other using Dapr. I’m deploying these two applications using a simple Helm Chart using Argo CD and have built and pushed the images to a Docker registry running in my cluster. To keep this blog post concise, I will not cover this part. If you’d like to know more about this setup, I do a similar thing in my other blog post: Exploring Mutating Webhooks in Kubernetes.

Seeing it in action

Checking the logs of the producer, the checkout service, I’m seeing that messages are successfully published to my message broker using Dapr.

1
2
3
4
5
6
dapr client initializing for: 127.0.0.1:50001
Published data: {"orderId":1}
Published data: {"orderId":2}
Published data: {"orderId":3}
Published data: {"orderId":4}
Published data: {"orderId":5}

On the receiving side, the logs show me that the messages are being processed.

1
2
3
4
5
Subscriber received: map[orderId:1]
Subscriber received: map[orderId:2]
Subscriber received: map[orderId:3]
Subscriber received: map[orderId:4]
Subscriber received: map[orderId:5]

By jumping into the redis-master-0 Pod and issuing the redis-cli monitor command, the communication is visible. Apparently Dapr adds some metadata to it as well!

1
2
1751745971.590038 [0 10.0.0.242:40312] "xadd" "orders" "*" "data" "{\"data\":\"{\\\"orderId\\\":5}\",\"datacontenttype\":\"text/plain\",\"id\":\"9e2434c6-7485-4fbb-a76d-d718f8eef6c5\",\"pubsubname\":\"orderpubsub-cb\",\"source\":\"checkout\",\"specversion\":\"1.0\",\"time\":\"2025-07-05T20:06:11Z\",\"topic\":\"orders\",\"traceid\":\"00-00000000000000000000000000000000-0000000000000000-00\",\"traceparent\":\"00-00000000000000000000000000000000-0000000000000000-00\",\"tracestate\":\"\",\"type\":\"com.dapr.event.sent\"}"
1751745972.593005 [0 10.0.0.242:40312] "xadd" "orders" "*" "data" "{\"data\":\"{\\\"orderId\\\":6}\",\"datacontenttype\":\"text/plain\",\"id\":\"bbcbfb12-00ae-4874-bb41-d54a3b4e931c\",\"pubsubname\":\"orderpubsub-cb\",\"source\":\"checkout\",\"specversion\":\"1.0\",\"time\":\"2025-07-05T20:06:12Z\",\"topic\":\"orders\",\"traceid\":\"00-00000000000000000000000000000000-0000000000000000-00\",\"traceparent\":\"00-00000000000000000000000000000000-0000000000000000-00\",\"tracestate\":\"\",\"type\":\"com.dapr.event.sent\"}"

Cool! Without using any Redis library, my two applications are able to communicate with each other seamlessly.

Switching to another message broker

The big benefit of abstracting away the integration to the message broker in use, is that it allows you to switch seamlessly to another backend. In this example, I will deploy a simple Kafka cluster and adjust the Component we defined before and point to our Kafka cluster instead.

I’m using the Kafka chart provided by Bitnami to deploy a HA Kafka Cluster. I’m setting listeners.client.protocol to PLAINTEXT so I don’t have to deal with any kind of authentication. Again, use at your own risk ;)

We let Argo CD deploy the Chart and in the meantime we can adjust our Component and switch over to Kafka. For a list of available pubsub types , please refer to the official Dapr documentation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
---
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: orderpubsub
  namespace: <checkout,order-processor>
spec:
  metadata:
    - name: brokers
      value: kafka.kafka.svc:9092
    - name: authType
      value: none
  type: pubsub.kafka
  version: v1

After the adjusted Component has been deployed, we can do a rolling restart of our Deployments and the message broker has been switched from Redis to Kafka. All without touching the application code, a powerful abstraction!

A feature that is currently in preview are Declarative subscriptions. With this new feature, subscriptions can be made declarative and “hot reloading” is supported as well. Interesting to keep an eye on.

Resiliency

Dapr provides the capability for defining and applying fault tolerance resiliency policies via a Resiliency Custom Resource. Using these policies, you are able to define timeouts, retries and circuit breakers. The configuration allows you to define different timeouts/retries or circuit breakers for different type of messages. In our example, we’ll keep it simple and just configure a DefaultTimeoutPolicy - a policy that applies to all operations of a Component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
---
apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
  name: myresiliency
scopes:
spec:
  policies:
    timeouts:
      DefaultTimeoutPolicy: 5s

The Resiliency spec allows a lot of specific configuration options. I’m not going more in-depth than the above, but it’s really interesting to tinker with the settings and see what the possibilities are.

Conclusion

I’m convinced that Dapr has real potential to improve Developer Experience. As a graduated CNCF project with continued backing from Microsoft, it’s a well-established and actively maintained tool. The documentation is clear, and the setup process was surprisingly smooth.

That said, Dapr is also yet another component in the platform stack. One more abstraction, and one more thing that can fail. As Platform Engineers, we have to weigh the operational overhead and complexity of introducing new tools, even when they promise to simplify development.

I’m still on the fence about whether the tradeoff is worth it in every case. I’d love to exchange thoughts with developers who are using or evaluating Dapr, since their perspective on its real-world impact could be very different from mine.

All in all, this was a fascinating deep dive and I’m glad I came across Dapr during KubeCon.