Research collaborate build

Mar 29, 2023

From Chaos to Cohesion: How Dapr Simplified Our Microservices

A comparative analysis of Dapr, Express.js, and Spring Boot for building microservices
Aman Chauhan
Aman ChauhanSenior Software Engineer - II
Pushkar Kumar
Pushkar KumarSolution Architect - I
Vishal Singh
Vishal SinghSenior Software Engineer - I
lines

Introduction

The world of microservices has revolutionized how software is built and deployed. A plethora of frameworks and platforms have emerged to support this architectural pattern, each with its own unique set of features and capabilities.

As a team, we (Aman Chauhan, Vishal Singh and Pushkar Kumar) have had the opportunity to work with various microservices stacks such as Express.js and Spring Boot.

Dapr is our latest obsession, and till now, we have been mighty impressed by its simplicity and the speed it has added to our development process.

In this article, we delve into the world of Dapr and compare it with other popular stacks, Express.js and Spring Boot, using code snippets to illustrate key differences.

Dapr: A Platform-Agnostic Runtime for Microservices

Dapr (Distributed Application Runtime) is a portable, event-driven runtime for building microservices on the cloud and edge. It provides a set of building blocks that simplify the most common challenges developers face when building microservices, such as state management, service-to-service invocation, and pub/sub messaging.

One of Dapr's most compelling features is its platform-agnostic nature. It can be run on any infrastructure, including Kubernetes, virtual machines, or even your local development environment. This enables developers to focus on writing code without worrying about the underlying infrastructure.

from dapr.clients import DaprClient

with DaprClient() as d:
    req_data = {
        'orderId': 1
    }
    resp = d.invoke_service('cart', 'add', data=req_data)
    print(resp.text, flush=True)

Express.js: Fast and Minimalist Web Framework for Node.js

Express.js is a popular web framework for building microservices using Node.js. It is known for its simplicity, flexibility, and minimalism. Express.js provides a thin layer of fundamental web application features, making it a great choice for single-page applications, websites, and APIs.

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/hello', (req, res) => {
    res.send('Hello, World!');
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

Spring Boot: Java-Based Framework for Microservices

Spring Boot is a Java-based framework that simplifies the development, deployment, and management of standalone, production-grade Spring applications. It provides an opinionated approach to configuring Spring applications, reducing boilerplate code, and offering embedded server support.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello() {
        return "Hello, World!";
    }
}

Comparison: Dapr vs. Express.js vs. Spring Boot

While all three frameworks enable developers to build and deploy microservices, Dapr stands out due to its platform-agnostic nature, extensive building blocks, and focus on event-driven architecture. Express.js is a popular choice for Node.js developers who value simplicity and minimalism, while Spring Boot is a powerful and opinionated solution for Java-based applications.

Dapr simplifies the development of microservices by abstracting away the complexities associated with distributed systems, allowing developers to focus on writing business logic. Its ability to extend across platforms and infrastructure makes it a versatile solution. In contrast, Express.js and Spring Boot are more specific to their respective languages and ecosystems.

Now, let's dive deeper into comparing Dapr, Express.js, and Spring Boot regarding pub/sub-components, observability, tracing, and maintainability.

Pub/Sub Components

Dapr provides a unified API for pub/sub messaging, supporting multiple messaging systems like Redis, RabbitMQ, and Azure Service Bus. This abstraction simplifies the implementation of message-driven microservices and promotes loose coupling among services.

# Publishing a message
with DaprClient() as d:
    d.publish_event('orders', 'newOrder', data={'orderId': 1})

# Subscribing to a topic
from dapr.ext.grpc import App

app = App()

@app.subscribe(pubsub_name='orders', topic='newOrder')
def new_order(data: bytes):
    print(f'Received new order: {data}')

In Express.js and Spring Boot, you'll need to use additional libraries to implement pub/sub messaging. For example, you could use the amqplib library with Express.js and RabbitMQ, or Spring Cloud Stream with Spring Boot and RabbitMQ.

Observability

Dapr provides built-in observability features like logging, metrics, and distributed tracing. It integrates with popular monitoring tools like Prometheus, Grafana, and Jaeger, making it easier to monitor and troubleshoot microservices.

In contrast, Express.js and Spring Boot require additional middleware or libraries to achieve similar observability features. For instance, you might use morgan and winston for logging in Express.js, or Spring Boot Actuator for metrics and health checks in Spring Boot.

Since Dapr's observability features are built-in, there's no additional code needed. Dapr automatically collects metrics and logs, which can be visualized using monitoring tools like Prometheus and Grafana.

Create a dapr.yaml configuration file:

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: custom-logging
spec:
  tracing:
    samplingRate: "1"
  logLevel: debug

This configuration file sets the log level to debug and enables tracing with a sampling rate of 100%. You can choose different log levels based on your requirements, such as info, warning, or error.

To apply this configuration, pass the --config flag to the dapr command when starting your application:

dapr run --app-id myapp --config ./dapr.yaml python app.py

Once you have configured Dapr, logs and metrics will be automatically collected and can be visualized using monitoring tools like Prometheus and Grafana. For distributed tracing, you can configure the tracing exporter to use a tool like Jaeger or Zipkin, as demonstrated in the previous response.


Express.js

const express = require('express');
const morgan = require('morgan');
const winston = require('winston');

const app = express();
app.use(morgan('combined'));

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' })
    ]
});

app.get('/hello', (req, res) => {
    logger.info('Hello endpoint was called');
    res.send('Hello, World!');
});

Spring Boot

Add the following dependency to your pom.xml:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Enable metrics and health check endpoints in your application.properties:

management.endpoints.web.exposure.include=health,info,metrics

Tracing

Dapr simplifies distributed tracing by integrating with OpenTelemetry, enabling automatic trace context propagation across services. This provides a consistent way to visualize service dependencies and identify performance bottlenecks.

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: tracing-config
spec:
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans"

For Express.js, you could use the OpenTelemetry JavaScript SDK to manually implement distributed tracing. In Spring Boot, you could use Spring Cloud Sleuth and Zipkin to achieve similar functionality.

const api = new opentelemetry.NodeSDK({
  resource: new Resource({
    [ResourceAttributes.SERVICE_NAME]: 'express-service',
  }),
  spanProcessor: new SimpleSpanProcessor(new ZipkinExporter({
    serviceName: 'express-service',
    url: 'http://localhost:9411/api/v2/spans',
  })),
});

api.start();

// Add OpenTelemetry middleware for Express.js
app.use(opentelemetry.express.middleware());

Spring Boot

Add the following dependencies to your pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

Configure Zipkin in your application.properties:

spring.zipkin.baseUrl=http://localhost:9411
spring.sleuth.sampler.probability=1

Comparison Chart

A feature comparison of Dapr, Express.js, and Spring Boot

This comparison chart highlights the key differences between Dapr, Express.js, and Spring Boot regarding language, pub/sub components, observability, distributed tracing, maintainability, infrastructure, and event-driven architecture. As seen in the chart, Dapr stands out with its platform-agnostic nature, built-in features, and standardized APIs that promote maintainability and clean architecture. While Express.js and Spring Boot are strong options for their respective languages, they rely more heavily on additional libraries and middleware for certain features.

Why Dapr in a Nutshell

As a team, working with diverse microservices stacks, we've experienced firsthand the benefits and challenges of using various frameworks and platforms. Dapr has emerged as a game-changer in the microservices landscape, with its platform-agnostic approach and built-in support for critical features like pub/sub messaging, observability, and distributed tracing. Its clean separation of concerns and focus on event-driven architecture make it a compelling choice for modern, scalable applications.

Express.js and Spring Boot continue to be popular choices for Node.js and Java developers, respectively. But the advantages of Dapr's abstractions and standardized APIs cannot be ignored.

By choosing Dapr, development teams can achieve greater flexibility and maintainability as their applications grow and easily switch between different infrastructure components and services with minimal impact on the application code.

Our insights and experiences with Dapr have shown us that it not only simplifies the development of microservices but also streamlines deployment, scaling, and monitoring. This allows developers to focus on what matters most: delivering high-quality, performant, and reliable software. For organizations looking to embrace microservices architecture, Dapr represents a powerful, versatile, and future-proof choice that can streamline development and foster innovation.

Conclusion

In conclusion, Dapr has undoubtedly simplified our microservices, taking us from chaos to cohesion. Its unique approach to building microservices, along with its extensive set of features, sets it apart from other popular stacks like Express.js and Spring Boot. As the world of software development continues to evolve, embracing tools like Dapr can help organizations stay ahead of the curve and deliver outstanding products and services to their customers.

Hire our Development experts.