Skip to content

Java Spring – Microservices

Open all questions about Spring

In this article:

➤ How microservices can interact
➤ How to use Docker in Spring
➤ How to Use Docker Compose in Spring
➤ How to use Kubernetes in Spring
➤ How to use Elasticsearch in Spring
➤ How to use RabbitMQ in Spring
➤ How to use Kafka in Spring
➤ What is Eureka Service
➤ How to implement Spring Cloud Gateway
➤ How to implement redirection using Spring Cloud Gateway
➤ What is Service Discovery
➤ What is Dockerfile and Docker Compose
➤ What tools are there for monitoring microservices
➤ What is Trace ID and Span ID

In a microservice architecture, microservices can interact with each other in several ways, such as synchronous and asynchronous calls. Let's look at the main approaches to microservice interaction:

Synchronous calls (HTTP/REST):
This is the most common way for microservices to interact. One microservice makes an HTTP request to another microservice. In Spring Boot, this can be done using RestTemplate or WebClient.

Example of using RestTemplate:

import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate


@Service
class UserService {
  
    private val restTemplate = RestTemplate()
    
    fun getUserDetails(userId: Long): User {
        val url = "http://order-service/orders/user/$userId"
        val response = restTemplate.getForObject(url, User::class.java)
        return response ?: throw RuntimeException("User not found")
    }
    
}

Example of using WebClient:

import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient


@Service
class UserService {
  
    private val webClient = WebClient.create("http://order-service")
    
    fun getUserDetails(userId: Long): User {
        return webClient.get()
            .uri("/orders/user/$userId")
            .retrieve()
            .bodyToMono(User::class.java)
            .block() ?: throw RuntimeException("User not found")
    }
    
}

Asynchronous calls (messages):
Microservices can communicate asynchronously by exchanging messages through message brokers such as RabbitMQ, Apache Kafka, and others.

Example of using RabbitMQ:

import org.springframework.amqp.core.Queue
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration


@Configuration
class RabbitMQConfig {
  
    @Bean
    fun queue(): Queue {
        return Queue("example.queue", false)
    }
    
}
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.stereotype.Component


@Component
class MessageSender(private val rabbitTemplate: RabbitTemplate) {
  
    fun sendMessage(message: String) {
        rabbitTemplate.convertAndSend("example.queue", message)
    }
    
}
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Component


@Component
class MessageReceiver {
  
    @RabbitListener(queues = ["example.queue"])
    fun receiveMessage(message: String) {
        println("Received message: $message")
    }
    
}

Interaction via database:
Microservices can interact via a common database, but this is not recommended, as it violates the principle of data isolation in the microservice architecture. It is better to use the database only for storing data, and interact via API.

Using discovery and routing services:
Eureka (service discovery)

Example Eureka configuration:

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate


@Service
class UserService {
  
    @Autowired
    private lateinit var discoveryClient: DiscoveryClient
  
    private val restTemplate = RestTemplate()
    
    fun getUserDetails(userId: Long): User {
        val instances = discoveryClient.getInstances("order-service")
        val orderServiceUri = instances[0].uri
        val url = "$orderServiceUri/orders/user/$userId"
        val response = restTemplate.getForObject(url, User::class.java)
        return response ?: throw RuntimeException("User not found")
    }
    
}

Spring Cloud Gateway (routing):
used to route requests to the appropriate microservices.
Example Gateway configuration:

Using gRPC:
gRPC is a modern RPC (Remote Procedure Call) framework that uses Protocol Buffers and supports asynchronous calls.

Example of using gRPC (Protocol (user.proto)):

syntax = "proto3";
option java_package = "com.example.demo";
option java_multiple_files = true;
service UserService {
    rpc GetUserDetails (UserRequest) returns (UserResponse);
}
message UserRequest {
    int64 userId = 1;
}
message UserResponse {
    int64 id = 1;
    string username = 2;
    string password = 3;
    string role = 4;
}
import io.grpc.stub.StreamObserver
import net.devh.boot.grpc.server.service.GrpcService


@GrpcService
class UserServiceImpl : UserServiceGrpc.UserServiceImplBase() {
  
    override fun getUserDetails(request: UserRequest, responseObserver: StreamObserver<UserResponse>) {
        val user = UserResponse.newBuilder()
            .setId(request.userId)
            .setUsername("testuser")
            .setPassword("password")
            .setRole("ROLE_USER")
            .build()
        responseObserver.onNext(user)
        responseObserver.onCompleted()
    }
    
}
import net.devh.boot.grpc.client.inject.GrpcClient
import org.springframework.stereotype.Service


@Service
class UserServiceClient {
  
    @GrpcClient("user-service")
    private lateinit var userServiceStub: UserServiceGrpc.UserServiceBlockingStub
  
    fun getUserDetails(userId: Long): UserResponse {
        val request = UserRequest.newBuilder().setUserId(userId).build()
        return userServiceStub.getUserDetails(request)
    }
    
}

Dockerfile:
is used to create customized Docker images. It is the primary way of describing how to build a container image with the required dependencies and configurations.

# Using a base image
FROM openjdk:11-jre-slim
# Setting the working directory
WORKDIR /app
# Copy jar file to container
COPY target/myapp.jar /app/myapp.jar
# Specifying the command to launch the application
CMD ["java", "-jar", "myapp.jar"]

Docker Compose:
is used to manage multi-container applications. It is an orchestration tool that allows you to run and manage multiple containers as a single entity, defining their interactions and dependencies.

version: '3.8'
services:
  web:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - db
  db:
    image: postgres:13
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydatabase
    volumes:
      - db-data:/var/lib/postgresql/data
volumes:
  db-data:

Both tools are often used together:
First, a Dockerfile is created for each service, then a Docker Compose file is created to manage all of these services in one application.

Docker is a platform for developing, delivering, and running applications in containers. Containers allow you to package an application with its dependencies and ensure its isolation and portability.

Key concepts of Docker:

Containers:
Isolated environments in which applications run.

Images:
Templates used to create containers.

Dockerfile:
A script describing how to create a Docker image.

Docker Hub:
A cloud registry where you can store and share Docker images.

Basic Docker commands:

docker build:
Create a Docker image from a Dockerfile.

docker run:
Launching a container from a Docker image.

docker pull:
Loading a Docker image from a registry.

docker push:
Uploading a Docker image to the registry.

docker ps:
List of running containers.

docker stop/start:
Stop/start container.

# Step 1: Building the application
FROM openjdk:21-jdk-slim AS build
WORKDIR /app
# Copy the Gradle build files and source code
COPY build.gradle settings.gradle gradlew gradlew.bat ./
COPY gradle gradle
COPY src src
# Set permissions to execute the gradlew file
RUN chmod +x ./gradlew
# Download dependencies and build the project
RUN ./gradlew bootJar
# Step 2: Create a minimal image to run the application
FROM openjdk:21-jdk-slim
WORKDIR /app
# Copy the assembled jar file from the build stage
COPY --from=build /app/build/libs/*.jar /app/app.jar
# Specify the command to launch the Spring Boot application
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Assembling the project:
Build the project using Maven or Gradle to get an executable jar file in the target folder.

./gradlew build

Building a Docker image:
Use the docker build command to create a Docker image from a Dockerfile.

docker build -t your-dockerhub-username/demo .

Launching a container:
Run the container using the docker run command.

docker run -p 8080:8080 your-dockerhub-username/demo

Your application is now available at http://localhost:8080.

Uploading the image to Docker Hub:
Log in to Docker Hub and upload your image.

docker login
docker push your-dockerhub-username/demo

Useful Docker commands:

docker images:
List of all images.

docker ps -a:
List of all containers.

docker stop:
Container stop.

docker start:
Starting a stopped container.

docker rm:
Removing the container.

docker rmi:
Deleting an image.

Docker Compose is a tool for defining and managing multi-container Docker applications. With Docker Compose, you can describe services, networks, and volumes in a single YAML file, then easily deploy them with a single command.

Example of using Docker Compose:
Let's say we have a Spring Boot application that uses a PostgreSQL database. We want to deploy these two services together using Docker Compose.

spring.datasource.url=jdbc:postgresql://db:5432/mydatabase
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.hibernate.ddl-auto=update

Creating a Dockerfile:

# Use the official image as a parent image
FROM openjdk:11-jre-slim
# Set the working directory in the container
WORKDIR /app
# Copy the jar file to the container
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
# Run the jar file
ENTRYPOINT ["java", "-jar", "app.jar"]

Create a docker-compose.yml file in the project root directory:

version: '3.8' # Specifies the version of the Docker Compose file format
services: # Defines a list of services to be deployed
  app: # Service definition for Spring Boot application
    image: your-dockerhub-username/demo # Docker image name
    build: # Define the Docker image build process
      context: . # Build context, current directory
      dockerfile: Dockerfile # The name of the Dockerfile that will be used for the build
    ports:
      - "8080:8080" # Forwarding port 8080 of the host to port 8080 of the container
    environment: # Environment variables for application configuration
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydatabase # Database connection URL
      SPRING_DATASOURCE_USERNAME: postgres # Username for the database
      SPRING_DATASOURCE_PASSWORD: postgres # Database password
    depends_on: # Defines dependencies on other services
      - db # Depends on the db service
  db: # Service definition for PostgreSQL database
    image: postgres:13 # PostgreSQL version 13 Docker image name
    environment: # Environment variables for PostgreSQL configuration
      POSTGRES_DB: mydatabase # Name of the database to be created
      POSTGRES_USER: postgres # Database user name
      POSTGRES_PASSWORD: postgres # Password for the database user
    ports:
      - "5432:5432" # Forwarding port 5432 of the host to port 5432 of the container
    volumes:
      - postgres_data:/var/lib/postgresql/data # Create a volume to store PostgreSQL data
volumes: # Defines a list of volumes that will be used by services
  postgres_data: # Volume to store PostgreSQL data so that it persists across container restarts

Explanation for each section:

version:
Specifies the version of the Docker Compose file format. In this case, version 3.8 is used.

services:
Defines all services to be deployed.

app:
Service for Spring Boot application.

image:
The name of the Docker image that will be used to run the container.

build:
Defines the Docker image build parameters.

context:
The directory where the Dockerfile is located.

dockerfile:
The name of the Dockerfile.

ports:
Forwarding port 8080 of the host to port 8080 of the container.

environment:
Environment variables for configuring the database connection.

depends_on:
Specifies that this service depends on the db service.

db:
Service for PostgreSQL database.

image:
The name of the PostgreSQL version 13 Docker image.

environment:
Environment variables for database configuration.

ports:
Forwarding port 5432 of the host to port 5432 of the container.

volumes:
Create a volume to store PostgreSQL data.

volumes:
Specifies the volume to be used to store PostgreSQL data.

How does this work:

Assembly and launch:
When you run the docker-compose up –build command, Docker Compose first builds a Docker image for the app service using the specified Dockerfile. Then both services (app and db) are started, with the app service depending on the db service, so db is started first.

Environment variables:
Environment variables are used to configure database connection settings in a Spring Boot application.

Port forwarding:
Ports are forwarded from containers to the host machine so that the application and database can be accessed.

Data volume:
A postgres_data volume is created to store PostgreSQL database data so that the data is persistent across container restarts.

This docker-compose.yml file makes it easy to deploy a multi-container application using Docker Compose, providing consistency and ease of container management.

Assembly and launch:
Run docker-compose to build and start all services.

docker-compose up --build

This command:
Build a Docker image for your Spring Boot application.
Launch containers for the PostgreSQL application and database.
Set up communication between containers.

Kubernetes is an open-source container orchestration system that enables automated deployment, scaling, and management of containerized applications. It provides a platform for running, managing, and scaling containers.

Here's a step-by-step guide on how to use Kubernetes to deploy a Spring Boot application.

Prerequisites:

Docker:
Make sure Docker is installed and configured.

Kubernetes:
Install and configure Kubernetes (e.g. using Minikube for local development).

kubectl:
Install the kubectl command-line utility to interact with the Kubernetes cluster.

Creating a Docker image:

# Use the official image as a parent image
FROM openjdk:11-jre-slim
# Set the working directory in the container
WORKDIR /app
# Copy the jar file to the container
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
# Run the jar file
ENTRYPOINT ["java", "-jar", "app.jar"]

Build the Docker image and upload it to Docker Hub (or any other Docker image registry).

# Building a Docker image
docker build -t your-dockerhub-username/demo .
# Login to Docker Hub
docker login
# Uploading a Docker image to Docker Hub
docker push your-dockerhub-username/demo

Creating Kubernetes Manifests:
Create Kubernetes manifests for your Deployment, Service, and Ingress configuration.

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-deployment
  labels:
    app: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - name: demo
        image: your-dockerhub-username/demo:latest
        ports:
        - containerPort: 8080

service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: demo-service
spec:
  selector:
    app: demo
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer

ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: demo.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demo-service
            port:
              number: 80

Using Kubernetes Manifests:
Use the kubectl command to apply manifests to your Kubernetes cluster.

kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml

DNS setup:
For local development with Minikube, add the following entry to the /etc/hosts file:

<MINIKUBE_IP> demo.local

Get Minikube IP using the command:

minikube ip

Checking the deployment:
Check the status of your deployment, service, and ingress using the commands:

kubectl get deployments
kubectl get services
kubectl get ingress

Open your browser and navigate to http://demo.local to see your deployed Spring Boot application.

Apache Kafka is a distributed streaming platform used to build real-time data processing systems. Kafka was originally developed at LinkedIn and opened as an open source project in 2011. It is used to publish, store, and process real-time streams of records.

The main components of Kafka are:

Producer:
Sends records (messages) to Kafka topics.

Consumer:
Reads records (messages) from Kafka topics.

Broker:
A Kafka server that accepts data from producers, stores it, and distributes it to consumers. A Kafka cluster consists of one or more brokers.

Topic:
A logical category or channel where producers send data and consumers read it from. A topic is partitioned to provide parallelism and scalability.

Partition:
A subdivision of a topic. Each partition is an ordered and immutable log where producers add messages.

Zookeeper:
The coordination system used by Kafka to manage cluster metadata and track the status of brokers and topics.

How Kafka works:

Posting messages:
Productors publish messages to specific topics. The messages are stored in the topic partitions as logs.

Storing messages:
Messages are stored in topic partitions on disk and can be configured to be stored for a specified time or until a specified data volume is reached.

Reading messages:
Consumers subscribe to topics and read messages from partitions. Kafka allows consumers to control where they start reading messages, which provides great flexibility and the ability to re-read data.

Scalability:
Partitions allow you to distribute the load across multiple brokers, providing horizontal scaling.

Folltolerance:
Replicating partitions across multiple brokers provides high availability and fault tolerance.

Setting up Kafka:
Install and configure Apache Kafka and Zookeeper using Docker Compose.
Create a docker-compose.yml file

version: '3.8'
services:
  zookeeper:
    image: bitnami/zookeeper:latest
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes
  kafka:
    image: bitnami/kafka:latest
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      - KAFKA_BROKER_ID=1
      - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
      - ALLOW_PLAINTEXT_LISTENER=yes

Go to the file directory in the command line and run:

docker-compose up -d

Check the status

docker-compose ps

Creating an event producer:
Create a Spring Boot application with Kafka dependencies.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.kafka:spring-kafka")
}
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=my-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

or via configuration class

import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.common.serialization.StringSerializer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.core.DefaultKafkaProducerFactory
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.core.ProducerFactory
import org.springframework.kafka.support.serializer.JsonSerializer


@Configuration
class KafkaProducerConfig {
  
    @Bean
    fun producerFactory(): ProducerFactory<String, String> {
        val configProps = HashMap<String, Any>()
        configProps[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG] = "localhost:9092"
        configProps[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java
        configProps[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = JsonSerializer::class.java
        return DefaultKafkaProducerFactory(configProps)
    }
    
    @Bean
    fun kafkaTemplate(): KafkaTemplate<String, String> {
        return KafkaTemplate(producerFactory())
    }
    
}
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.annotation.EnableKafka
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
import org.springframework.kafka.core.ConsumerFactory
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
import org.springframework.kafka.support.serializer.JsonDeserializer


@EnableKafka
@Configuration
class KafkaConsumerConfig {
  
    @Bean
    fun consumerFactory(): ConsumerFactory<String, String> {
        val configProps = HashMap<String, Any>()
        configProps[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = "localhost:9092"
        configProps[ConsumerConfig.GROUP_ID_CONFIG] = "group_id"
        configProps[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = ErrorHandlingDeserializer::class.java
        configProps[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = ErrorHandlingDeserializer::class.java
        configProps[ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS] = JsonDeserializer::class.java.name
        return DefaultKafkaConsumerFactory(configProps, StringDeserializer(), JsonDeserializer())
    }
    
    @Bean
    fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> {
        val factory = ConcurrentKafkaListenerContainerFactory<String, String>()
        factory.consumerFactory = consumerFactory()
        return factory
    }
    
}

Producer and Consumer:

import org.springframework.kafka.core.KafkaTemplate
import org.springframework.stereotype.Service


@Service
class EventProducer(private val kafkaTemplate: KafkaTemplate<String, String>) {
  
    fun sendMessage(topic: String, message: String) {
        kafkaTemplate.send(topic, message)
    }
    
}
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Service


@Service
class EventConsumer {
  
    @KafkaListener(topics = ["topic_name"], groupId = "group_id")
    fun consume(message: String) {
        println("Consumed message: $message")
    }
    
}

RabbitMQ is a message broker software that allows applications to exchange messages and perform tasks asynchronously. It supports multiple messaging protocols and is widely used to build distributed and scalable systems.

Basic concepts of RabbitMQ:

AMQP Protocol:
RabbitMQ supports the AMQP (Advanced Message Queuing Protocol), which defines the rules for exchanging messages between clients and brokers.

Queues:
A queue is a buffer for storing messages. Messages are sent to the queue where they wait to be processed by the recipient.

Exchangers:
The exchanger receives messages from the producer and routes them to one or more queues depending on the established rules.

Bindings:
A binding connects a queue to an exchanger and defines the rules for routing messages from the exchanger to the queue.

Messages:
Messages are data that are passed between applications through queues. Each message consists of a header and a body (payload).

Producers:
Manufacturers send messages to exchangers.

Consumers:
Consumers receive messages from queues and process them.

Example of RabbitMQ in operation:
The Producer sends a message to the Exchange.
The exchanger routes the message to the appropriate queue (Queue) based on the established rules.
The Consumer receives a message from the queue and processes it.

To use RabbitMQ in Kotlin using Spring Boot, you can use the Spring AMQP library (Spring for RabbitMQ)

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-amqp'
    implementation 'org.jetbrains.kotlin:kotlin-reflect'
    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
}
import org.springframework.amqp.core.Queue
import org.springframework.amqp.rabbit.connection.ConnectionFactory
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration


@Configuration
class RabbitConfig {
  
    @Bean
    fun queue(): Queue {
        return Queue("myQueue", false)
    }
    
    @Bean
    fun rabbitTemplate(connectionFactory: ConnectionFactory): RabbitTemplate {
        return RabbitTemplate(connectionFactory)
    }
    
}
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.stereotype.Service

  
@Service
class MessageSender(private val rabbitTemplate: RabbitTemplate) {
  
    fun sendMessage(message: String) {
        rabbitTemplate.convertAndSend("myQueue", message)
    }
    
}
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Service


@Service
class MessageListener {
  
    @RabbitListener(queues = ["myQueue"])
    fun receiveMessage(message: String) {
        println("Received message: $message")
    }
    
}
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController


@RestController
class MessageController(private val messageSender: MessageSender) {
  
    // http://localhost:8080/send?message=Hello
    @GetMapping("/send")
    fun send(@RequestParam message: String) {
        messageSender.sendMessage(message)
        return "Message sent: $message"
    }
    
}

Kafka and RabbitMQ are two popular messaging systems used to transfer messages between components of distributed systems. However, they have different architectural approaches, target use cases, and features.

Apache Kafka:

Architecture:

Log-based pub/sub:
Kafka stores messages as logs, allowing subscribers to read messages from a specific offset.

Message broker:
Kafka consists of clusters of brokers that store data in a distributed manner.

Storing messages:
Messages are stored on disk, and each message has a unique offset. This allows messages to be re-read and the log to be rewinded.
Support for long-term data storage.

Performance:
High performance and throughput suitable for processing large amounts of data in real time.

Scaling support:
Easily scales horizontally by adding new brokers and partitions.

Target use cases:
Stream processing.
Implementation of event systems.
Real-time data analytics.

Ecosystem:
Includes components such as Kafka Streams for stream processing and Kafka Connect for integration with various data sources.

RabbitMQ:

Architecture:

Message queues:
RabbitMQ uses the concept of message queues and routing.

Message broker:
Messages are transmitted through exchanges and queues.

Storing messages:
Messages can be stored in RAM or on disk.
Supports reliable message delivery through acknowledgements and retry.

Performance:
Good performance for a wide range of scenarios, but may be less efficient when processing large amounts of data compared to Kafka.

Scaling support:
Supports clustering and federation for horizontal scaling, but can be more complex to set up and manage than Kafka.

Target use cases:
Classic task queue for asynchronous processing.
Implementation of a messaging system with complex routing.
Integration and data exchange between heterogeneous systems.

Ecosystem:
Support for various protocols (AMQP, MQTT, STOMP).
A large number of plugins to extend functionality.

Kafka and RabbitMQ solve different messaging problems and have their own advantages and limitations. Kafka is suitable for processing large volumes of data and stream processing, while RabbitMQ is better at handling asynchronous processing tasks and complex message routing. The choice between them depends on the specific requirements and use cases in your project.

Eureka Service is part of Netflix’s microservices development toolkit called Netflix OSS. It is used to register and discover services in distributed systems. Eureka Service is the core of the service discovery service, which allows different microservices to find and interact with each other.

The main components of Eureka:

Eureka Server:
A central server that acts as a registrar for all microservices. Microservices register with Eureka Server and report their status (healthy, down, etc.).

Eureka Client:
Eureka clients that register themselves with Eureka Server and can use it to discover other services.

eureka-service-project
├── eureka-server
│   ├── build.gradle
│   ├── src
│   │   ├── main
│ │ │ ├── java (or kotlin)
│   │   │   │   └── com
│   │   │   │       └── example
│   │   │   │           └── eurekaserver
│   │   │   │               └── EurekaServerApplication.kt
│   │   │   ├── resources
│   │   │   │   └── application.properties
│   │   └── test
│ │ ├── java (or kotlin)
│   │       │   └── com
│   │       │       └── example
│   │       │           └── eurekaserver
│   │       │               └── EurekaServerApplicationTests.kt
├── eureka-client
│   ├── build.gradle
│   ├── src
│   │   ├── main
│ │ │ ├── java (or kotlin)
│   │   │   │   └── com
│   │   │   │       └── example
│   │   │   │           └── eurekaclient
│   │   │   │               ├── EurekaClientApplication.kt
│   │   │   │               └── ServiceInstanceRestController.kt
│   │   │   ├── resources
│   │   │   │   └── application.properties
│   │   └── test
│ │ ├── java (or kotlin)
│   │       │   └── com
│   │       │       └── example
│   │       │           └── eurekaclient
│   │       │               └── EurekaClientApplicationTests.kt
└── settings.gradle

How Eureka Service works:

Registration of services:
Each microservice that is a Eureka client registers itself with Eureka Server when it starts, providing its information such as address, port, and service ID.

Status update:
Clients regularly send "pings" (heartbeats) to the Eureka Server to confirm that they are still active.

Service discovery:
Microservices can use Eureka Client to get a list of all available services registered in Eureka Server and interact with them.

Basic annotations and their usage in Eureka:

@EnableEurekaServer:
Includes Eureka Server.

@EnableEurekaClient:
Includes Eureka Client for registration and service discovery.

@EnableDiscoveryClient:
Includes a generic service discovery mechanism (can be used with various discovery systems, including Eureka).

@LoadBalanced:
Marks a RestTemplate or WebClient to use client load balancing.

Example of using Eureka Service:
In settings.gradle you need to add includeBuild all modules

Example for Eureka Server:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
server.port=8080
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.server.enable-self-preservation=false

server.port=8761:
Specifies the port on which Eureka Server will run. By default, Eureka Server runs on port 8080.

eureka.client.register-with-eureka=false:
Specifies that the Eureka Server should not register itself as a client. This is a Eureka client configuration parameter that specifies that this Eureka Server will not register with another Eureka Server.

eureka.client.fetch-registry=false:
Specifies that Eureka Server should not fetch the registry of other services. This is also a Eureka client configuration parameter that disables attempts to obtain a list of all registered services.

eureka.server.enable-self-preservation=false:
Disables self-preservation mode on Eureka Server. This mode is designed to protect against the loss of service registrations when the server does not receive pings from clients for a long time. Disabling this mode allows services to be immediately removed from the registry if they do not send pings, which can be useful for testing, but is not recommended for production environments.

@EnableEurekaServer:
This annotation is used to enable the Eureka Server. It is specified on the main class of the application to mark it as a Eureka Server.

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer


@SpringBootApplication
@EnableEurekaServer
class EurekaServerApplication


fun main(args: Array<String>) {
    runApplication<EurekaServerApplication>(*args)
}

Eureka Client example:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
server.port=0
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

server.port=0:
Specifies the port on which Eureka Client will be launched. In this case, the client application will run on the port specified automatically.

eureka.client.service-url.defaultZone=http://localhost:8761/eureka/:
Specifies the URL for registering the client with the Eureka Server. The defaultZone parameter specifies the address of the Eureka Server that the client will interact with for registration and service discovery. In this case, the client will register with the Eureka Server running at http://localhost:8761/eureka/.

Additional parameters that can be used in application.properties:

For Eureka Server:

eureka.instance.hostname:
Sets the hostname for Eureka Server.

eureka.instance.prefer-ip-address:
Specifies whether to use the IP address instead of the hostname for registration.

For Eureka Client:

eureka.instance.hostname:
Sets the hostname for Eureka Client.

eureka.instance.prefer-ip-address:
Specifies whether to use an IP address instead of a host name for client registration.

eureka.client.initial-instance-info-replication-interval-seconds:
Specifies the interval in seconds between retries of sending instance metadata to Eureka Server.

eureka.client.registry-fetch-interval-seconds:
Specifies the interval in seconds between client attempts to obtain an updated service register from Eureka Server.

eureka.client.instance-info-replication-interval-seconds:
Specifies the interval in seconds between sending information about a service instance to Eureka Server.

@EnableEurekaClient:
This annotation is used to enable a Eureka client. It is specified on the main application class to mark it as a Eureka client. The client automatically registers with the Eureka Server and can use it to discover other services.

@EnableDiscoveryClient:
This annotation is a more general annotation for enabling service discovery. It allows an application to use different service discovery mechanisms, including Eureka. It is used similarly to the @EnableEurekaClient annotation.

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.netflix.eureka.EnableEurekaClient


@SpringBootApplication
@EnableEurekaClient
class EurekaClientApplication


fun main(args: Array<String>) {
    runApplication<EurekaClientApplication>(*args)
}
import org.springframework.cloud.client.discovery.DiscoveryClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController


@RestController
class ServiceInstanceRestController(private val discoveryClient: DiscoveryClient) {

  
    @GetMapping("/service-instances")
    fun serviceInstances(): List<String> {
        return discoveryClient.services
    }
    

}

@LoadBalanced:
This annotation is used to mark a RestTemplate or WebClient to use Ribbon-based client load balancing (if used). This allows a RestTemplate or WebClient to make HTTP requests to named services registered with Eureka, with load balancing across service instances.

import org.springframework.boot.autoconfigure.SpringBootApplication 
import org.springframework.boot.runApplication
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.context.annotation.Bean
import org.springframework.web.client.RestTemplate


@SpringBootApplication
class LoadBalancedApplication {

  
    @Bean
    @LoadBalanced
    fun restTemplate(): RestTemplate {
        return RestTemplate()
    }

}


fun main(args: Array<String>) {
    runApplication<LoadBalancedApplication>(*args)
}

Spring Cloud Gateway is a modern API gateway built on Spring Framework 5, Spring Boot 2, and the Spring WebFlux project. It provides powerful capabilities for routing and managing API traffic in a microservices architecture. Spring Cloud Gateway is designed to provide easy integration with other Spring Cloud components and to work in an asynchronous and reactive environment.

Key features of Spring Cloud Gateway:

Request Routing:
Spring Cloud Gateway can route requests to different microservices based on various criteria such as path, headers, request parameters, and more.

Filters:
Allows you to apply filters to requests and responses. Filters can modify requests, add or change headers, perform authentication and authorization, logging, and more.

Load Balancing:
Built-in load balancing support allows you to distribute requests across multiple microservice instances.

Integration with Eureka:
Spring Cloud Gateway integrates easily with Eureka and other service discovery systems to dynamically route requests to registered services.

Support for asynchronous processing:
Built on the Spring WebFlux reactive stack, allowing it to handle large numbers of parallel requests with high performance.

Safety:
Support for various authentication and authorization mechanisms, including OAuth2.

Example of using Spring Cloud Gateway:
Create a new Spring Boot project with dependencies for Spring Cloud Gateway and Spring Boot Actuator.

eureka-service-project
├── eureka-server
│   ├── build.gradle
│   ├── src
│   │   ├── main
│   │   │   └── ...
│   │   └── ...
├── eureka-client
│   ├── build.gradle
│   ├── src
│   │   ├── main
│   │   │   └── ...
│   │   └── ...
├── api-gateway
│   ├── build.gradle
│   ├── src
│   │   ├── main
│   │   │   └── ...
│   │   └── ...
└── settings.gradle

Configuring settings.gradle

rootProject.name = 'eureka-service-project'
include 'eureka-server', 'eureka-client', 'api-gateway'

api-gateway/build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:2021.0.1"
    }
}

api-gateway/src/main/resources/application.properties

server.port=8080
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

api-gateway/src/main/kotlin/com/example/apigateway/ApiGatewayApplication.kt

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient

@SpringBootApplication
@EnableDiscoveryClient
class ApiGatewayApplication

fun main(args: Array<String>) {
    runApplication<ApiGatewayApplication>(*args)
}

Explanations of the settings:

server.port=8080:
API Gateway will run on port 8080.

spring.cloud.gateway.discovery.locator.enabled=true:
Enables automatic route discovery for microservices registered in Eureka.

spring.cloud.gateway.discovery.locator.lower-case-service-id=true:
Converts service identifiers to lowercase to simplify routing.

eureka.client.service-url.defaultZone=http://localhost:8761/eureka/:
Eureka Server URL for registration and service discovery.

Request Routing:
Spring Cloud Gateway automatically creates routes for all microservices registered with Eureka. For example, if you have a microservice named eureka-client, requests to API Gateway at /eureka-client/** will be routed to the corresponding microservice.
Routing can be configured in a configuration file or programmatically.

src/main/resources/application.properties

server.port=8080
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Defining routes for eureka-client
spring.cloud.gateway.routes[0].id=eureka-client
spring.cloud.gateway.routes[0].uri=lb://eureka-client
spring.cloud.gateway.routes[0].predicates[0]=Path=/eureka-client/**
spring.cloud.gateway.routes[0].filters[0]=RewritePath=/eureka-client/(?<remaining>.*), /${remaining}

Configuration notes:

server.port=8080:
Specifies that Spring Cloud Gateway will run on port 8080.

spring.cloud.gateway.discovery.locator.enabled=true:
Enables automatic route discovery for services registered in Eureka.

spring.cloud.gateway.discovery.locator.lower-case-service-id=true:
Converts service identifiers to lowercase to simplify routing.

eureka.client.service-url.defaultZone=http://localhost:8761/eureka/:
Specifies the Eureka Server URL for registration and service discovery.

routes:
Setting up routes. In this case, requests starting with /eureka-client/** will be redirected to the eureka-client service.

Let's assume we have two microservices registered in Eureka: order-service and inventory-service.
We want to configure routing in Spring Cloud Gateway such that requests starting with /orders/ are redirected to order-service, and requests starting with /inventory/ are redirected to inventory-service.

server.port=8080
# Enable automatic route discovery for services registered in Eureka
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# Specify the Eureka Server URL for registration and service discovery
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Defining routes for order-service
spring.cloud.gateway.routes[0].id=order-service
spring.cloud.gateway.routes[0].uri=lb://order-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/orders/**
spring.cloud.gateway.routes[0].filters[0]=RewritePath=/orders/(?<remaining>.*), /${remaining}
# Defining routes for inventory-service
spring.cloud.gateway.routes[1].id=inventory-service
spring.cloud.gateway.routes[1].uri=lb://inventory-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/inventory/**
spring.cloud.gateway.routes[1].filters[0]=RewritePath=/inventory/(?<remaining>.*), /${remaining}

Explanation of parameters:

server.port:
Specifies that Spring Cloud Gateway will run on port 8080.

spring.cloud.gateway.discovery.locator.enabled:
Enables automatic route discovery for services registered in Eureka.

spring.cloud.gateway.discovery.locator.lower-case-service-id:
Converts service identifiers to lowercase to simplify routing.

eureka.client.service-url.defaultZone:
Specifies the Eureka Server URL for registration and service discovery.

spring.cloud.gateway.routes:
routes: Section for defining routes in Spring Cloud Gateway.

Route for order-service:

id:
Unique identifier of the route. In this case, it is order-service.

type:
URI for redirecting requests. The lb:// prefix indicates the use of the built-in load balancer, and order-service is the name of the service registered in Eureka.

predicates:
The conditions that must be met for a request to be forwarded along this route.

Path=/orders/:
Path condition: All requests starting with /orders/** will match this route.

filters:
List of filters that will be applied to requests and responses.

RewritePath=/orders/(?.*), /${remaining}:
A filter that rewrites the request path. In this case, the /orders/ part of the path will be removed from the request before it is sent to order-service. The rest of the path will be preserved and passed on.

Route for inventory-service:

id:
Unique identifier of the route. In this case, it is inventory-service.

type:
URI to forward requests to. The lb:// prefix indicates the use of the built-in load balancer, and inventory-service is the name of the service registered in Eureka.

predicates:
The conditions that must be met for a request to be forwarded along this route.

Path=/inventory/:
Path condition: All requests starting with /inventory/** will match this route.

filters:
List of filters that will be applied to requests and responses.

RewritePath=/inventory/(?.*), /${remaining}:
A filter that rewrites the path of the request. In this case, the /inventory/ part of the path will be removed from the request before it is sent to inventory-service. The rest of the path will be preserved and passed on.

Service Discovery is a mechanism that allows microservices to find and interact with each other in a distributed system without having to explicitly specify network addresses. In modern microservice architectures, where the number of services can be very large, manually managing their addresses becomes complex and inconvenient. Service Discovery automates the process of finding services and simplifies scaling and management of the system.

The main components of Service Discovery are:

Service Registry:
A service registrar is a central repository where all available services are registered with their metadata such as IP addresses, ports, and states. Examples of service registrars include Consul, Eureka, Zookeeper, and Etcd.

Service Clients:
Service clients are components that interact with the service registrar to register, update, and find services.

Health Checks:
Health check mechanisms are used to monitor the availability and health of services. Services regularly send information about their health to the service registrar to keep the data up to date.

Types of Service Discovery:

Client-Side Discovery:
In this approach, client applications themselves query the registrar service to obtain addresses of available services. Client discovery is usually implemented using libraries integrated into client applications.
Example:
Netflix Eureka
Consul

Server-Side Discovery:
In this approach, client applications send requests to a server proxy or gateway (such as API Gateway), which interacts with a service registrar and forwards requests to the desired services.
Example:
AWS Elastic Load Balancer (ELB)
Kubernetes Service

Prometheus:
Monitoring and alerting system with a powerful query language.

Grafana:
A platform for visualizing metrics and creating dashboards.

Elasticsearch, Logstash, Kibana (ELK Stack):
Solution for centralized logging and data analysis.

Jaeger, Zipkin:
Distributed tracing tools.

Elasticsearch is an open-source, distributed search and analytics system designed to store, search, and analyze large amounts of data in real time. Elasticsearch is often used for full-text search, logging, and analytics.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch")
}
spring.elasticsearch.uris=http://localhost:9200
spring.elasticsearch.username=elastic
spring.elasticsearch.password=your_password
import org.springframework.data.annotation.Id
import org.springframework.data.elasticsearch.annotations.Document
import org.springframework.data.elasticsearch.annotations.Field


@Document(indexName = "products")
data class Product(
    @Id
    val id: String? = null,
    val name: String,
    val description: String,
    val price: Double,
    @Field(type = Date) 
    val date: String;
)
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository
import org.springframework.stereotype.Repository


@Repository
interface ProductRepository : ElasticsearchRepository<Product, String> {
  
    fun findByName(name: String): List<Product>
    
}
import org.springframework.stereotype.Service


@Service
class ProductService(private val productRepository: ProductRepository) {
  
    fun findAllProducts(): List<Product> {
        return productRepository.findAll().toList()
    }
    
    fun saveProduct(product: Product): Product {
        return productRepository.save(product)
    }
    
    fun findProductByName(name: String): List<Product> {
        return productRepository.findByName(name)
    }
    
}
import org.springframework.web.bind.annotation.*

  
@RestController
@RequestMapping("/products")
class ProductController(private val productService: ProductService) {
  
    @GetMapping
    fun getAllProducts(): List<Product> {
        return productService.findAllProducts()
    }
    
    @PostMapping
    fun createProduct(@RequestBody product: Product): Product {
        return productService.saveProduct(product)
    }
    
    @GetMapping("/search")
    fun searchProductsByName(@RequestParam name: String): List<Product> {
        return productService.findProductByName(name)
    }
    
}

Trace ID and Span ID are key components of distributed tracing, which are used to monitor and debug distributed systems. They help track and link requests across multiple services, allowing you to understand and diagnose system behavior.

Trace ID:
This is a unique identifier that is assigned to each request that passes through the system. This identifier remains unchanged throughout the life cycle of the request and is used to link all operations related to that request.

Target:
Trace ID allows you to combine all spans (parts of a request) into one common trace, so you can see the full path of a request through all microservices and components of the system.

Span ID:
is a unique identifier for a specific operation or part of a request. Each span represents a separate step or segment of a request, such as calling a method or executing a database query.

Target:
Span ID allows you to identify and track individual operations within a single request, providing detailed information about each step.

Example of using Trace ID and Span ID:
Let's look at how Trace ID and Span ID can be used in a Spring Boot application using the Spring Cloud Sleuth library, which integrates with Zipkin for distributed tracing.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.cloud:spring-cloud-starter-sleuth")
    implementation("org.springframework.cloud:spring-cloud-starter-zipkin")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}
dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:2020.0.4")
    }
}
spring.zipkin.base-url=http://localhost:9411
spring.sleuth.sampler.probability=1.0
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.client.RestTemplate


@RestController
class TraceController(
  @Autowired val restTemplate: RestTemplate
) {

    @GetMapping("/trace")
    fun trace(): String {
        val response = restTemplate.getForObject(
          "http://localhost:8081/external",
          String::class.java
        )
        return "Trace ID and Span ID example: $response"
    }

    @GetMapping("/external")
    fun external(): String {
        return "External service response"
    }

}

Run Zipkin using Docker:

docker run -d -p 9411:9411 openzipkin/zipkin

Run your Spring Boot application and it will automatically generate Trace ID and Span ID for all requests.

View trace:
Go to http://localhost:9411 to open the Zipkin interface.
Send a GET request to http://localhost:8080/trace.
Go back to the Zipkin interface and find the trace associated with your request. You will see a Trace ID and Span ID showing the path of the request through your services.

Copyright: Roman Kryvolapov