Building Distributed Applications with Dapr
back-end

Building Distributed Applications with Dapr

Kaitou
Kaitou

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.

Sidecar Architecture

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:

  1. Order Service: Receives and processes orders
  2. Inventory Service: Manages product inventory
  3. Notification Service: Sends order confirmations

Architecture Flow

dapr-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

command-dapr-init

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"
  }'

create-orders

Check order status:

curl http://localhost:3500/v1.0/invoke/order-service/method/orders/[ORDER_ID]

order-status