Getting to know Dapr

Building distributed applications or microservice applications brings a whole new range of problems. All those application components, or microservices, need to communicate with each other. How will we do that: using messaging, or would direct HTTP calls be a better choice? Often, we must make such decisions early in a project. Since it’s hard to change it later, we call it an “architectural decision”. But this is often an excuse so we can blame the architect if the choice turned out to be wrong.

Let’s take a step back, and revisit that last point. Does it make sense to call such choices “architectural decisions” and blame somebody else for it? What if we turned that around, and made that choice something that we could more easily change?

After all, building a distributed application is a tough job. There is a lot of complexity that we must take into account. Part of that complexity comes from the domain; this essential complexity is something we could only lower by dropping requirements. It’s there because the business problem is hard to solve. But another part - and I believe a growing part over the last decade - is accidental complexity. It’s not there because the software must solve a complex business problem. It is there because we chose a complex design to solve that business problem.

What if we could lower that accidental complexity? What if we could make those “hard to change” decisions a little bit easier to change?

That is precisely the promise of Dapr, the Distributed Application Runtime. Dapr provides you with an abstraction layer for many concerns that distributed applications face. Communication between components is just one of those concerns; others include state management, secret management, or binding input and output from/to external components. One post is not enough room to cover everything, so let’s focus on one topic for now: state management. How does Dapr help you there?

Getting to know Dapr

To understand that, we must first get to know Dapr a bit better. At its core, Dapr defines a set of “building blocks”. A building block describes a cross-cutting concern related to developing distributed applications. Examples include service-to-service invocation, state management and secret management.

The building block provides a generic interface to your application for performing that cross-cutting concern. It does not describe how that concern will be addressed for you, or how to implement that on your platform of choice. The only thing you need to concern yourself with is how you interact with that building block.

But that just moves the problem… who exactly takes care of that concern, you ask?

That is the responsibility of the Dapr sidecar. This sidecar is like an extra process that runs next to your application. Like a sidecar is attached to a motorbike, the Dapr sidecar is attached to your application. This can be an additional process on the (virtual) machine where your application is running. Or, in a Kubernetes environment, it can be an additional container as part of the pod that hosts your application. Your application code then communicates with this sidecar using well-defined interfaces over HTTP or gRPC.

Each application component has such a sidecar, and the sidecar knows how to perform the cross-cutting concern. It consists of many components, all baked into one executable. Those components are tied to a particular implementation based on a platform or a product.

Dapr acts as a sidecar to your application code
Dapr acts as a sidecar to your application code

Zooming in: state management

As an example, multiple components are implementing the State Management building block. They may use PostgreSQL, Redis, Mongo or any cloud-specific service such as AWS Dynamo, Azure Cosmos or Google Firestore. The full list is in the Dapr supported state stores.

The good part is: your application does not need to know or care about the underlying implementation! It needs to know the general contract for a state store, which you can find in the Dapr State API. This is a generic API that your application can invoke over HTTP; you don’t necessarily need a particular library or SDK to use it. Of course, Dapr offers one, and in a future post, I will dive into why you should consider it.

For now, it is enough to know that a simple HTTP GET or POST is enough to fetch or store a piece of state. The nice part is you can even try that without changing your Java code: you can use any HTTP client to explore the API:

# First, store some data...
curl -X POST http://localhost:3500/v1.0/state/name-of-my-state-store \
    -d '[{"key":"jEAkDoXfd-4rONDEx1aTtiA", "value":"here goes arbitrary data"}]'

# Now fetch it back
curl -X GET http://localhost:3500/v1.0/state/name-of-my-state-store/jEAkDoXfd-4rONDEx1aTtiA

"here goes arbitrary data"

See? Without knowing the underlying store, we can transparently store and retrieve data, without coupling the application to that store.

What just happened?

You might be curious what just happened, and what the prerequisite steps were to get here. I will skip installing Docker or Dapr; you will find that the Getting started guide of Docker covers that extensively.

After we have Dapr up and running, we must do two more things: first, we must configure the state store, and then we must start the sidecar.

Configuring the state store

You configure Dapr components, such as the state store, using YAML files. To configure a State store that is backed by Redis, you need a configuration file like this:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: name-of-my-state-store
  namespace: default
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: enableTLS
    value: false

This is the bare minimum; there are more configuration options. You can find them in the Redis state store docs.

Starting a Dapr sidecar

The next step is to start a Dapr sidecar. You must tell the sidecar a few things:

  • the ID of the application to which it is a sidecar. This is a logical name, Dapr does not enforce any naming conventions. Use --app-id dapr-state-demo for instance.
  • the HTTP port that the sidecar will open so the application can talk to its sidecar. Use --dapr-http-port 3500 to specify this.
  • where to find the component configuration. This is optional; if you omit it, it will default to ~/.dapr/components. For demos or experiments, use --components-path /path/to/folder/containing/yaml/file.

Bringing it together, it will be something like this:

dapr run --app-id dapr-state-demo --dapr-http-port 3500 --components-path ./components/

Run this, and the sidecar will throw some messages at you, which should end with

ℹ️  Dapr sidecar is up and running.
✅  You're up and running! Dapr logs will appear here.

Interacting with the state store

Now that the sidecar is up and running, you can use curl as I showed above. It will send an HTTP request to the sidecar, which knows - from the YAML file - that “name-of-my-state-store” is a state store backed by Redis. It will perform the necessary operations on Redis. If you want, you can inspect how the data ends up in Redis using redis-cli:

# Look which keys are available in Redis
docker exec -it dapr_redis redis-cli keys d*
1) "dapr-state-demo||jEAkDoXfd-4rONDEx1aTtiA"
# We see that Dapr created a key using the application name and the supplied key

# Inspect the data under that key
docker exec -it dapr_redis redis-cli hgetall "dapr-state-demo||jEAkDoXfd-4rONDEx1aTtiA"
1) "data"
2) "\"here goes arbitrary data\""
3) "version"
4) "1"

Limitations

Since Dapr is there to solve a particular problem, it’s not a Swiss army knife. For instance, you could switch the State store component to be a PostgreSQL one. But that does not give you an option to query on any additional database column, let alone join tables together or such. What you can do is defined by the public API for a State Store: store an entry, fetch one or more entries by their key(s), or delete an entry. If you want to store complex models in a relational database, then you still need to use JDBC or R2DBC, or any higher abstraction layer on top of that.

Generally put: since Dapr makes your application unaware of the underlying platform or service, Dapr doesn’t let you interact with that underlying platform or service. It only offers the generic API which is designed to address a particular concern.

Wrapping up

This was the first instalment of a series of blog posts I want to write about Dapr. The next topic will be a demonstration of how to use the State store component in a Jakarta EE application. After that, I want to dive into the Dapr SDK for Java: what does it give you and how do you use it? I also plan to write about writing actor-based systems using Dapr. So stay tuned for the next episode of this series!

In the meantime, do you have any questions? Maybe ideas or suggestions for an additional post? I’d love to hear from you! Feel free to reach out using the comments box below or any of the platforms in the sidebar.