Building Distributed Applications with Dapr
What is Dapr?
Dapr (Distributed Application Runtime) is an open-source, portable runtime that simplifies building resilient, stateless, and stateful microservices. Think of it as a toolkit that handles the complex aspects of distributed computing so you can focus on writing your business logic.
Reference: https://dapr.io/
Why Dapr?
You will take a lot of effort to solve above problems
- Service-to-service communication
- State management
- Pub/Sub messaging
- Secret management
- Observability
- Security
Dapr provides these capabilities as building blocks through simple HTTP and gRPC APIs, regardless of your programming language or framework.
Key Concepts
1. Building Blocks
Dapr provides several building blocks:
- Service Invocation: Call other services reliably
- State Management: Store and retrieve key-value pairs
- Pub/Sub: Publish and subscribe to messages
- Bindings: Interface with external systems
- Actors: Virtual actors for stateful objects
- Secrets: Secure secret management
- Configuration: Manage application configuration
2. Components
Components are Dapr's way of providing implementations for building blocks. For example:
- Redis for state store
- RabbitMQ for pub/sub
- Azure Key Vault for secrets
3. Sidecar Pattern
Dapr runs as a sidecar process alongside your application, communicating via HTTP or gRPC.
Dapr sidecar pattern - each application has its own Dapr runtime
Let start with the sample project: E-Commerce Order System
Let's build a simple e-commerce order processing system with Node.js and Dapr. Our system will have:
- Order Service: Receives and processes orders
- Inventory Service: Manages product inventory
- Notification Service: Sends order confirmations
Architecture Flow
Prerequisites
# Install Dapr CLI
curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash
# Initialize Dapr
dapr init
# Verify installation
dapr --version
Project Structure
ecommerce-dapr/
├── order-service/
│ ├── app.js
│ └── package.json
├── inventory-service/
│ ├── app.js
│ └── package.json
├── notification-service/
│ ├── app.js
│ └── package.json
└── components/
├── pubsub.yaml
└── statestore.yaml
1. Order Service
First, let's create the order service:
order-service/package.json
{
"name": "order-service",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^4.18.2",
"axios": "^1.6.0"
}
}
order-service/app.js
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const DAPR_HTTP_PORT = process.env.DAPR_HTTP_PORT || 3500;
const APP_PORT = process.env.APP_PORT || 3000;
// Create a new order
app.post('/orders', async (req, res) => {
try {
const { productId, quantity, customerEmail } = req.body;
const orderId = `order-${Date.now()}`;
// Check inventory using Dapr service invocation
const inventoryResponse = await axios.post(
`http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/inventory-service/method/check`,
{ productId, quantity }
);
if (!inventoryResponse.data.available) {
return res.status(400).json({ error: 'Insufficient inventory' });
}
// Store order using Dapr state management
const order = {
id: orderId,
productId,
quantity,
customerEmail,
status: 'confirmed',
createdAt: new Date()
};
await axios.post(
`http://localhost:${DAPR_HTTP_PORT}/v1.0/state/statestore`,
[{ key: orderId, value: order }]
);
// Publish order event using Dapr pub/sub
await axios.post(
`http://localhost:${DAPR_HTTP_PORT}/v1.0/publish/pubsub/orders`,
{
orderId,
customerEmail,
productId,
quantity
}
);
res.json({ orderId, status: 'Order created successfully' });
} catch (error) {
console.error('Error creating order:', error.message);
res.status(500).json({ error: 'Failed to create order' });
}
});
// Get order by ID
app.get('/orders/:id', async (req, res) => {
try {
const response = await axios.get(
`http://localhost:${DAPR_HTTP_PORT}/v1.0/state/statestore/${req.params.id}`
);
if (!response.data) {
return res.status(404).json({ error: 'Order not found' });
}
res.json(response.data);
} catch (error) {
console.error('Error fetching order:', error.message);
res.status(500).json({ error: 'Failed to fetch order' });
}
});
app.listen(APP_PORT, () => {
console.log(`Order service running on port ${APP_PORT}`);
});
2. Inventory Service
inventory-service/package.json
{
"name": "inventory-service",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^4.18.2",
"axios": "^1.6.0"
}
}
inventory-service/app.js
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const DAPR_HTTP_PORT = process.env.DAPR_HTTP_PORT || 3501;
const APP_PORT = process.env.APP_PORT || 3001;
// Mock inventory data
const inventory = {
'product-1': { name: 'Laptop', stock: 10 },
'product-2': { name: 'Mouse', stock: 50 },
'product-3': { name: 'Keyboard', stock: 25 }
};
// Check inventory availability
app.post('/check', (req, res) => {
const { productId, quantity } = req.body;
const product = inventory[productId];
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
const available = product.stock >= quantity;
if (available) {
// Reduce stock (in real app, this would be atomic)
product.stock -= quantity;
console.log(`Updated inventory for ${productId}: ${product.stock} remaining`);
}
res.json({
available,
productName: product.name,
remainingStock: product.stock
});
});
// Get current inventory
app.get('/inventory', (req, res) => {
res.json(inventory);
});
app.listen(APP_PORT, () => {
console.log(`Inventory service running on port ${APP_PORT}`);
});
3. Notification Service
notification-service/package.json
{
"name": "notification-service",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^4.18.2"
}
}
notification-service/app.js
const express = require('express');
const app = express();
app.use(express.json());
const APP_PORT = process.env.APP_PORT || 3002;
// Dapr pub/sub subscription endpoint
app.get('/dapr/subscribe', (req, res) => {
res.json([
{
pubsubname: 'pubsub',
topic: 'orders',
route: '/orders'
}
]);
});
// Handle order events
app.post('/orders', (req, res) => {
const { data } = req.body;
console.log('? Sending notification for order:', data);
// Simulate sending email notification
console.log(`
✅ Order Confirmation
Order ID: ${data.orderId}
Customer: ${data.customerEmail}
Product: ${data.productId}
Quantity: ${data.quantity}
Thank you for your order! ?
`);
res.status(200).send();
});
app.listen(APP_PORT, () => {
console.log(`Notification service running on port ${APP_PORT}`);
});
4. Dapr Components
components/statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
components/pubsub.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
Running the Application
(1) Install dependencies for each service:
cd order-service && yarn install
cd ../inventory-service && yarn install
cd ../notification-service && yarn install
(2) Run services with Dapr:
Terminal 1 - Order Service:
dapr run --app-id order-service --app-port 3000 --dapr-http-port 3500 --components-path ./components node order-service/app.js
Terminal 2 - Inventory Service:
dapr run --app-id inventory-service --app-port 3001 --dapr-http-port 3501 --components-path ./components node inventory-service/app.js
Terminal 3 - Notification Service:
dapr run --app-id notification-service --app-port 3002 --dapr-http-port 3502 --components-path ./components node notification-service/app.js
Testing the System
Create an order:
curl -X POST http://localhost:3500/v1.0/invoke/order-service/method/orders \
-H "Content-Type: application/json" \
-d '{
"productId": "product-1",
"quantity": 2,
"customerEmail": "customer@example.com"
}'
Check order status:
curl http://localhost:3500/v1.0/invoke/order-service/method/orders/[ORDER_ID]