A Simple Microservices using Quarkus, Kong, and Kafka

by Didin J. on Oct 23, 2024 A Simple Microservices using Quarkus, Kong, and Kafka

A comprehensive step-by-step tutorial on building a Microservices using Java, Quarkus, Kafka, PostgreSQL, MongoDB, Docker, and Kong API Gateway


This is a beginner guide for those who must learn how to build simple Microservices. The services (for this tutorial REST API) will be built using Quarkus a fast boot and low resident set size memory of the Java Microservice framework. The backend will use an RDBMS PostgreSQL (each service will use a different schema) and a NoSQL database MongoDB. The message exchange between services will use Apache Kafka, an open-source distributed event-streaming platform. The client such as Mobile apps, PWA, or web apps will access these Microservices through the Kong API gateway. Kong is also good for communicating between services because it has low latency. How this Microservices works will look like this. 

A Simple Microservices using Quarkus, Kong, and Kafka - diagram

We will create 3 services Student, Lecture, and Study service. The Student and Lecture service will be using PostgreSQL with a different schema. The study service will be using MongoDB. Three of them will be running on a different port. Each of them will have the Kafka consumer and producer. For example, if the Student service produces a Kafka message then the message with the same topic will be consumed by other services. All services will registered to the Kong API gateway, at the end, the client side (Mobile or Web Apps) will consume the API using one port. In the production, the client side will use one domain or URL.

In this tutorial, we will use Docker instead of Kubernetes or GraalVM to make it easy to set up and run. This tutorial is divided into several steps:

Let's get started with the main step!


Step #1: Setup The Required Environment

This tutorial will require the following tools, frameworks, and libraries:

JDK 17+
Quarkus-CLI
Maven
Docker
IDE or Text Editor (we are using VSCode)

Install JDK 17

To install JDK 17 on Mac OS, we can use SDK man. Let's type this command in the terminal to determine the required Java 17 version. 

sdk list java

For this moment we will use Oracle Java 17. Type this command to install it.

sdk install java 17.0.12-oracle

Check the installed Java version by typing this command.

java --version

Check the installed Java version by typing this command.

java 17.0.12 2024-07-16 LTS
Java(TM) SE Runtime Environment (build 17.0.12+8-LTS-286)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.12+8-LTS-286, mixed mode, sharing)

Install Quarkus-CLI

To install Quarkus-CLI using SDKMan, type this command.

sdk install quarkus

Check the Quarkus version by typing this command.

quarkus --version

Now, we have Quarkus 3.14.2 as default Quarkus-CLI.

3.14.2

Install Maven

To install Maven using SDKMan, type this command.

sdk install maven

Check the Maven version by typing this command.

mvn --version

Now, we have Maven 3.9.9 as the default Maven version.

Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: /Users/didin/.sdkman/candidates/maven/current
Java version: 17.0.12, vendor: Oracle Corporation, runtime: /Users/didin/.sdkman/candidates/java/17.0.12-oracle
Default locale: en_ID, platform encoding: UTF-8
OS name: "mac os x", version: "14.6.1", arch: "aarch64", family: "mac"

Install Docker

First, download the Docker installer for Mac and drag and drop it to the Applications to install the Docker Desktop. Run the Docker Desktop application then log in using your Docker account. To install Docker CLI, type this command in the terminal.

brew install docker

Check the running Docker daemon by typing this command.

docker ps -a

You will see an empty Docker container list.

CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Add Plugins to IDE

We assume that you have installed VSCode on your machine. Start VSCode then go to Extension on the left toolbar. Search for these plugins then install them:

  • Java by Oracle Corporation
  • Quarkus by Red Hat
  • SonarLint by SonarSource


Step #2: Create Student Service

To create a Quarkus service using CLI, go to your project folder from the terminal and type this command to create a new project folder.

mkdir school-app
cd school-app 

Create a new service called Student Service by typing this command.

mvn io.quarkus:quarkus-maven-plugin:3.14.2:create \
    -DplatformVersion=3.14.2 \
    -DprojectGroupId=com.djamware.school \
    -DprojectArtifactId=student \
    -DclassName="com.djamware.school.student.StudentResource" \
    -Dpath="api/students" \
    -Dextensions="resteasy-reactive-jackson"

Go to the newly created Quarkus project.

cd student 

Open with VSCode by typing this command.

code .

There will be a resource called StudentResource with a REST endpoint to GET a String response. Run this REST app for the first time by typing this command.

mvn quarkus:dev

Access this REST endpoint by typing this CURL command in another terminal tab.

curl http://localhost:8080/api/students

You will see a String response like this.

Hello from Quarkus REST

There is also a generated test unit called StudentResourceTest. To re-run the test command type this 'r' key.

All 1 test is passing (0 skipped), 1 test was run in 1209ms. Tests completed at 16:24:28.

As the previous diagram shows, the Student service uses PostgreSQL for datastore. We will use Panache Hibernate ORM, and add those dependencies by typing this command.

./mvnw quarkus:add-extension -Dextensions="jdbc-postgresql,hibernate-orm-panache,hibernate-validator"

Next, create a models or entities folder and an entity file in that folder.

mkdir src/main/java/com/djamware/school/student/models
touch src/main/java/com/djamware/school/student/models/Student.java

Fill that file with these Java codes.

package com.djamware.school.student.models;

import java.util.Date;

import org.hibernate.annotations.CreationTimestamp;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;

@Entity
@Table(name = "student")
public class Student {
    @Id
    @SequenceGenerator(name = "studentSequence", sequenceName = "student_id_seq", allocationSize = 1, initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "studentSequence")
    @Column(name = "id", nullable = false)
    public Long id;

    @Column(name = "sidn", length = 16, nullable = false)
    public String sidn; // Student ID Number

    @Column(name = "name", length = 100, nullable = false)
    public String name;

    @Column(name = "email", length = 100, nullable = false)
    public String email;

    @Column(name = "phone", length = 20, nullable = false)
    public String phone;

    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    public Date createdAt;
}

Next, create a folder for repositories and a class file inside it.

mkdir src/main/java/com/djamware/school/student/repositories
touch src/main/java/com/djamware/school/student/repositories/StudentRepository.java

Fill that file with these Java codes.

package com.djamware.school.student.repositories;

import com.djamware.school.student.models.Student;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class StudentRepository implements PanacheRepository<Student> {
    public Student findBySidn(String sidn) {
        return find("sidn = ?1", sidn).firstResult();
    }
}

Next, create a DTOs folder and DTOs Java files for request and response DTO.

mkdir src/main/java/com/djamware/school/student/dtos
touch src/main/java/com/djamware/school/student/dtos/StudentRequestDto.java
touch src/main/java/com/djamware/school/student/dtos/StudentResponseDto.java

Open StudentRequestDto.java then fill this file with these Java codes.

package com.djamware.school.student.dtos;

public class StudentRequestDto {
    private String sidn;
    private String name;
    private String email;
    private String phone;

    public String getSidn() {
        return sidn;
    }

    public void setSidn(String sidn) {
        this.sidn = sidn;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }
}

Open StudentResponseDto.java then fill this file with these Java codes.

package com.djamware.school.student.dtos;

import java.util.Date;

public class StudentResponseDto {
    private Long id;
    private String sidn;
    private String name;
    private String email;
    private String phone;
    private Date createdAt;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getSidn() {
        return sidn;
    }

    public void setSidn(String sidn) {
        this.sidn = sidn;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}

Next, create a folder for services and a class file inside it.

mkdir src/main/java/com/djamware/school/student/services
touch src/main/java/com/djamware/school/student/services/StudentService.java

Add the @ApplicationScoped annotation before the class name then inject the previously created StudentRepository. 

package com.djamware.school.student.services;

import com.djamware.school.student.repositories.StudentRepository;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class StudentService {
    StudentRepository studentRepository;

    @Inject
    public StudentService(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

}

Add a method to save a new Student.   

    @Transactional
    public JsonObject saveStudent(StudentRequestDto req) throws ValidationException {
        if (req == null)
            throw new ValidationException(BAD_REQUEST);

        Student student = new Student();
        student.sidn = req.getSidn();
        student.name = req.getName();
        student.email = req.getEmail();
        student.phone = req.getPhone();
        studentRepository.persist(student);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_201_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

Add a method to update a Student by ID.

    @Transactional
    public JsonObject updateStudent(StudentRequestDto req, Long id) throws ValidationException {
        if (req == null)
            throw new ValidationException(BAD_REQUEST);

        Student student = studentRepository.findById(id);
        if (student == null)
            throw new ValidationException(DATA_NOT_FOUND);

        studentRepository.update("sidn = ?1, name = ?2, email = ?3, phone = ?4 where id = ?5", req.getSidn(),
                req.getName(), req.getEmail(), req.getPhone(), id);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

Add methods to GET list Student, a Student by ID, and a Student by SIDN.

    public JsonObject getListStudent() {
        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);
        resp.put(DATA_KEY, studentRepository.listAll());

        return resp;
    }

    public JsonObject getStudentByID(Long id) {
        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);
        resp.put(DATA_KEY, studentRepository.findById(id));

        return resp;
    }

    public JsonObject getStudentBySidn(String sidn) {
        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);
        resp.put(DATA_KEY, studentRepository.findBySidn(sidn));

        return resp;
    }

Add this method to delete a Student by ID.

    @Transactional
    public JsonObject deleteStudent(Long id) throws ValidationException {
        if (id == null)
            throw new ValidationException(BAD_REQUEST);

        Student student = studentRepository.findById(id);
        if (student != null)
            studentRepository.delete(student);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

Don't forget to add these imports.

import java.util.List;

import com.djamware.school.student.dtos.StudentRequestDto;
import com.djamware.school.student.models.Student;
import com.djamware.school.student.repositories.StudentRepository;

import io.vertx.core.json.JsonObject;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.xml.bind.ValidationException;

As you see, there are some constant variables. For that, we must create a Constants.java file in the utils folder.

mkdir src/main/java/com/djamware/school/student/utils
touch src/main/java/com/djamware/school/student/utils/Constants.java

Edit this Constants.java file then fill it with these Java codes.

package com.djamware.school.student.utils;

public class Constants {
    private Constants() {
    }

    public static final String BAD_REQUEST = "BAD_REQUEST";
    public static final String DATA_NOT_FOUND = "DATA_NOT_FOUND";
    public static final String CODE_KEY = "code";
    public static final String CODE_201_VAL = "201";
    public static final String CODE_200_VAL = "200";
    public static final String MESSAGE_KEY = "message";
    public static final String MESSAGE_VAL = "Success";
    public static final String DATA_KEY = "data";
}

Back to the StudentService.java then add these lines of imports.

import static com.djamware.school.student.utils.Constants.BAD_REQUEST;
import static com.djamware.school.student.utils.Constants.CODE_200_VAL;
import static com.djamware.school.student.utils.Constants.CODE_201_VAL;
import static com.djamware.school.student.utils.Constants.CODE_KEY;
import static com.djamware.school.student.utils.Constants.DATA_NOT_FOUND;
import static com.djamware.school.student.utils.Constants.MESSAGE_KEY;
import static com.djamware.school.student.utils.Constants.MESSAGE_VAL;
import static com.djamware.school.student.utils.Constants.DATA_KEY;

We will use Swagger Open API to describe and document our REST API. To do so, install open-API by typing this Quarkus command.

mvn quarkus:add-extension -Dextensions="smallrye-openapi"

Before implementing open API in our resources, we must define the Open API Specification (OAS) file to describe the Request and Response. Create a folder in the DTOS folder then create this OAS file.

mkdir src/main/java/com/djamware/school/student/dtos/oas
touch src/main/java/com/djamware/school/student/dtos/oas/StudentOAS.java

Fill that file with these Java codes.

package com.djamware.school.student.dtos.oas;

import java.util.Date;

import org.eclipse.microprofile.openapi.annotations.media.Schema;

public class StudentOAS {
    @Schema(name = "StudentOAS.Request")
    public class Request {
        @Schema(required = true, example = "0000111")
        public String sidn;
        @Schema(required = true, example = "Elon Dust")
        public String name;
        @Schema(required = true, example = "[email protected]")
        public String email;
        @Schema(required = true, example = "+1567655665")
        public String phone;
    }

    @Schema(name = "StudentOAS.Response")
    public class Response {
        @Schema(required = true, example = "1")
        public Long id;
        @Schema(required = true, example = "0000111")
        public String sidn;
        @Schema(required = true, example = "Elon Dust")
        public String name;
        @Schema(required = true, example = "[email protected]")
        public String email;
        @Schema(required = true, example = "+1567655665")
        public String phone;
        @Schema(required = true, example = "2024-09-21T06:50:15.445890")
        public Date createdAt;
    }

    @Schema(name = "StudentOAS.BadRequest")
    public class BadRequest {
        @Schema(example = "BAD_REQUEST", enumeration = { "BAD_REQUEST", "INVALID_BODY" })
        public String message;
    }
}

Back to the StudentResource.java then replace the existing Java codes with these. 

package com.djamware.school.student;

import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;

import com.djamware.school.student.dtos.StudentRequestDto;
import com.djamware.school.student.dtos.oas.StudentOAS;
import com.djamware.school.student.services.StudentService;

import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.xml.bind.ValidationException;

@Path("/v1/students")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class StudentResource {
    StudentService studentService;

    @Inject
    public StudentResource(StudentService studentService) {
        this.studentService = studentService;
    }

    @POST
    @Operation(summary = "Add a new Student", description = "This API will add a new Student to database")
    @RequestBody(content = {
            @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.Request.class)) })
    @APIResponses(value = {
            @APIResponse(responseCode = "201", description = "Accepted", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.BadRequest.class)))
    })
    public Response addStudent(StudentRequestDto request) throws ValidationException {
        return Response.accepted().entity(studentService.saveStudent(request)).build();
    }

    @PUT
    @Path("/{id}")
    @Operation(summary = "Update Student", description = "This API will update Student to database")
    @RequestBody(content = {
            @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.Request.class)) })
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.BadRequest.class)))
    })
    public Response updateStudent(StudentRequestDto request, @PathParam("id") Long id) throws ValidationException {
        return Response.ok().entity(studentService.updateStudent(request, id)).build();
    }

    @GET
    @Operation(summary = "Get List Student", description = "This API will get List Student from Database")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.BadRequest.class)))
    })
    public Response getListStudent() {
        return Response.ok().entity(studentService.getListStudent()).build();
    }

    @GET
    @Path("/{id}")
    @Operation(summary = "Get Student By ID", description = "This API will get Student by ID from Database")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.BadRequest.class)))
    })
    public Response getStudentByID(@PathParam("id") Long id) {
        return Response.ok().entity(studentService.getStudentByID(id)).build();
    }

    @GET
    @Path("/sidn/{sidn}")
    @Operation(summary = "Get Student by SIDN", description = "This API will get Student by  from Database")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.BadRequest.class)))
    })
    public Response getStudentBySidn(@PathParam("sidn") String sidn) {
        return Response.ok().entity(studentService.getStudentBySidn(sidn)).build();
    }

    @DELETE
    @Path("/{id}")
    @Operation(summary = "Delete Student", description = "This API will delete Student by ID")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudentOAS.BadRequest.class)))
    })
    public Response deleteStudent(@PathParam("id") Long id) throws ValidationException {
        return Response.ok().entity(studentService.deleteStudent(id)).build();
    }
}

Before running this service, we must configure the application.properties to set the server, open-API, and database. Open the application.properties then add these lines of property codes.

quarkus.http.port=8081
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/api
quarkus.smallrye-openapi.info-title=Student API
quarkus.datasource.username=djamware
quarkus.datasource.password=dj@mw@r3
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5433/school
quarkus.datasource.jdbc.max-size=16
quarkus.datasource.jdbc.driver=org.postgresql.Driver
quarkus.hibernate-orm.packages=com.djamware.school.student.models
quarkus.hibernate-orm.database.default-schema=student_schema
quarkus.hibernate-orm.database.generation=update

You can see that we are using PostgreSQL with the database name Student and the schema name student_schema. For that, create a new database and schema.

psql postgres -U djamware
CREATE DATABASE school;
\c school;
CREATE SCHEMA student_schema;

Now, we can run this service in dev mode by typing this command.

mvn quarkus:dev

The Quarkus service will run like this if no error is found.

A Simple Microservices using Quarkus, Kong, and Kafka - run quarkus

In the browser, go to http://localhost:8081/api/q/dev-ui/extensions, you will see this Quarkus dashboard page.

A Simple Microservices using Quarkus, Kong, and Kafka - swagger ui

We can choose the Swagger UI to test the endpoints that we have created previously. Now, you can test to POST, GET, UPDATE, and DELETE students from this Swagger UI page.


Step #3: Create Lecture Service

We will use the same method to create a lecturer service. Back to the school app folder, type this command to create a new Quarkus service.

cd ../
mvn io.quarkus:quarkus-maven-plugin:3.14.2:create \
    -DplatformVersion=3.14.2 \
    -DprojectGroupId=com.djamware.school \
    -DprojectArtifactId=lecturer \
    -DclassName="com.djamware.school.lecturer.LecturerResource" \
    -Dpath="api/lecturers" \
    -Dextensions="resteasy-reactive-jackson,jdbc-postgresql,hibernate-orm-panache,hibernate-validator,smallrye-openapi"

As you see, we also added some extensions that will be used for this service. They are PostgreSQL, Panache, and Open API. Next, create a models or entities folder and an entity file in that folder.

mkdir src/main/java/com/djamware/school/lecturer/models
touch src/main/java/com/djamware/school/lecturer/models/Lecturer.java

Fill that file with these Java codes.

package com.djamware.school.lecturer.models;

import java.util.Date;

import org.hibernate.annotations.CreationTimestamp;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;

@Entity
@Table(name = "lecturer")
public class Lecturer {
    @Id
    @SequenceGenerator(name = "lecturerSequence", sequenceName = "lecturer_id_seq", allocationSize = 1, initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "lecturerSequence")
    @Column(name = "id", nullable = false)
    public Long id;

    @Column(name = "lidn", length = 16, nullable = false)
    public String lidn; // Lecturer ID Number

    @Column(name = "name", length = 100, nullable = false)
    public String name;

    @Column(name = "email", length = 100, nullable = false)
    public String email;

    @Column(name = "phone", length = 20, nullable = false)
    public String phone;

    @Column(name = "major", length = 100, nullable = false)
    public String major;

    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    public Date createdAt;
}

Next, create a folder for repositories and a class file inside it.

mkdir src/main/java/com/djamware/school/lecturer/repositories
touch src/main/java/com/djamware/school/lecturer/repositories/LecturerRepository.java

Fill that file with these Java codes.

package com.djamware.school.lecturer.repositories;

import com.djamware.school.lecturer.models.Lecturer;

import io.quarkus.hibernate.orm.panache.PanacheRepository;

@ApplicationScoped
public class LecturerRepository implements PanacheRepository<Lecturer> {
    public Lecturer findByLidn(String lidn) {
        return find("lidn = ?1", lidn).firstResult();
    }
}

Next, create a DTOs folder and DTOs Java files for request and response DTO.

mkdir src/main/java/com/djamware/school/lecturer/dtos
touch src/main/java/com/djamware/school/lecturer/dtos/LecturerRequestDto.java
touch src/main/java/com/djamware/school/lecturer/dtos/LecturerResponseDto.java

Open LecturerRequestDto.java then fill this file with these Java codes.

package com.djamware.school.lecturer.dtos;

public class LecturerRequestDto {
    private String lidn;
    private String name;
    private String email;
    private String phone;
    private String major;

    public String getLidn() {
        return lidn;
    }

    public void setLidn(String lidn) {
        this.lidn = lidn;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getMajor() {
        return major;
    }

    public void setMajor(String major) {
        this.major = major;
    }
}

Open LecturerResponseDto.java then fill this file with these Java codes.

package com.djamware.school.lecturer.dtos;

import java.util.Date;

public class LecturerResponseDto {
    private Long id;
    private String sidn;
    private String name;
    private String email;
    private String phone;
    private String major;
    private Date createdAt;

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getSidn() {
        return sidn;
    }
    public void setSidn(String sidn) {
        this.sidn = sidn;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String getMajor() {
        return major;
    }
    public void setMajor(String major) {
        this.major = major;
    }
    public Date getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}

Next, create a folder for services and a class file inside it.

mkdir src/main/java/com/djamware/school/lecturer/services
touch src/main/java/com/djamware/school/lecturer/services/LecturerService.java

Add the @ApplicationScoped annotation before the class name then inject the previously created StudentRepository. 

package com.djamware.school.lecturer.services;

import com.djamware.school.lecturer.repositories.LecturerRepository;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class LecturerService {
    LecturerRepository lecturerRepository;

    public LecturerService(LecturerRepository lecturerRepository) {
        this.lecturerRepository = lecturerRepository;
    }
}

Add a method to save a new Lecturer.

    @Transactional
    public JsonObject saveLecturer(LecturerRequestDto req) throws ValidationException {
        if (req == null)
            throw new ValidationException(BAD_REQUEST);

        Lecturer lecturer = new Lecturer();
        lecturer.lidn = req.getLidn();
        lecturer.name = req.getName();
        lecturer.email = req.getEmail();
        lecturer.phone = req.getPhone();
        lecturer.major = req.getMajor();
        lecturerRepository.persist(lecturer);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

Add a method to update a Lecturer by ID.

    @Transactional
    public JsonObject updateLecturer(LecturerRequestDto req, Long id) throws ValidationException {
        if (req == null)
            throw new ValidationException(BAD_REQUEST);

        Lecturer lecturer = lecturerRepository.findById(id);
        if (lecturer == null)
            throw new ValidationException(DATA_NOT_FOUND);

        lecturerRepository.update("lidn = ?1, name = ?2, email = ?3, phone = ?4, major = ?5 where id = ?6",
                req.getLidn(),
                req.getName(), req.getEmail(), req.getPhone(), req.getMajor(), id);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

Add methods to GET list Lecturer, a Lecturer by ID, and a Lecturer by LIDN.

    public JsonObject getListLecturer() {
        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);
        resp.put(DATA_KEY, lecturerRepository.listAll());

        return resp;
    }

    public JsonObject getLecturerByID(Long id) {
        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);
        resp.put(DATA_KEY, lecturerRepository.findById(id));

        return resp;
    }

    public JsonObject getLecturerByLidn(String lidn) {
        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);
        resp.put(DATA_KEY, lecturerRepository.findByLidn(lidn));

        return resp;
    }

Add this method to delete a Lecturer by ID.

    @Transactional
    public JsonObject deleteLecturer(Long id) throws ValidationException {
        if (id == null)
            throw new ValidationException(BAD_REQUEST);

        Lecturer lecturer = lecturerRepository.findById(id);
        if (lecturer != null)
            lecturerRepository.delete(lecturer);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

Don't forget to add these imports.

import java.util.List;

import com.djamware.school.student.dtos.StudentRequestDto;
import com.djamware.school.student.models.Student;
import com.djamware.school.student.repositories.StudentRepository;

import io.vertx.core.json.JsonObject;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.xml.bind.ValidationException;

As you see, there are some constant variables. For that, we must create a Constants.java file in the utils folder.

mkdir src/main/java/com/djamware/school/lecturer/utils
touch src/main/java/com/djamware/school/lecturer/utils/Constants.java

Edit this Constants.java file then fill it with these Java codes.

package com.djamware.school.lecturer.utils;

public class Constants {
    private Constants() {
    }

    public static final String BAD_REQUEST = "BAD_REQUEST";
    public static final String DATA_NOT_FOUND = "DATA_NOT_FOUND";
    public static final String CODE_KEY = "code";
    public static final String CODE_201_VAL = "201";
    public static final String CODE_200_VAL = "200";
    public static final String MESSAGE_KEY = "message";
    public static final String MESSAGE_VAL = "Success";
    public static final String DATA_KEY = "data";
}

Back to the LecturerService.java then add these lines of imports.

import static com.djamware.school.lecturer.utils.Constants.BAD_REQUEST;
import static com.djamware.school.lecturer.utils.Constants.CODE_200_VAL;
import static com.djamware.school.lecturer.utils.Constants.CODE_201_VAL;
import static com.djamware.school.lecturer.utils.Constants.CODE_KEY;
import static com.djamware.school.lecturer.utils.Constants.DATA_NOT_FOUND;
import static com.djamware.school.lecturer.utils.Constants.MESSAGE_KEY;
import static com.djamware.school.lecturer.utils.Constants.MESSAGE_VAL;
import static com.djamware.school.lecturer.utils.Constants.DATA_KEY;

Before implementing open API in our resources, we must define the Open API Specification (OAS) file to describe the Request and Response. Create a folder in the DTOS folder then create this OAS file.

mkdir src/main/java/com/djamware/school/lecturer/dtos/oas
touch src/main/java/com/djamware/school/lecturer/dtos/oas/LecturerOAS.java

Fill that file with these Java codes.

package com.djamware.school.lecturer.dtos.oas;

import java.util.Date;

import org.eclipse.microprofile.openapi.annotations.media.Schema;

public class LecturerOAS {
    @Schema(name = "LecturerOAS.Request")
    public class Request {
        @Schema(required = true, example = "0000111")
        public String lidn;
        @Schema(required = true, example = "Elon Dust")
        public String name;
        @Schema(required = true, example = "[email protected]")
        public String email;
        @Schema(required = true, example = "+1567655665")
        public String phone;
        @Schema(required = true, example = "Math")
        public String major;
    }

    @Schema(name = "LecturerOAS.Response")
    public class Response {
        @Schema(required = true, example = "1")
        public Long id;
        @Schema(required = true, example = "0000111")
        public String sidn;
        @Schema(required = true, example = "Elon Dust")
        public String name;
        @Schema(required = true, example = "[email protected]")
        public String email;
        @Schema(required = true, example = "+1567655665")
        public String phone;
        @Schema(required = true, example = "Math")
        public String major;
        @Schema(required = true, example = "2024-09-21T06:50:15.445890")
        public Date createdAt;
    }

    @Schema(name = "LecturerOAS.BadRequest")
    public class BadRequest {
        @Schema(example = "BAD_REQUEST", enumeration = { "BAD_REQUEST", "INVALID_BODY" })
        public String message;
    }
}

Back to the LecturerResource.java then replace the existing Java codes with these. 

package com.djamware.school.lecturer;

import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;

import com.djamware.school.lecturer.dtos.LecturerRequestDto;
import com.djamware.school.lecturer.dtos.oas.LecturerOAS;
import com.djamware.school.lecturer.services.LecturerService;

import jakarta.inject.Inject;
import jakarta.validation.ValidationException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Path("/v1/lecturers")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class LecturerResource {
    LecturerService lecturerService;

    @Inject
    public LecturerResource(LecturerService lecturerService) {
        this.lecturerService = lecturerService;
    }

    @POST
    @Operation(summary = "Add a new Lecturer", description = "This API will add a new Lecturer to database")
    @RequestBody(content = {
            @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.Request.class)) })
    @APIResponses(value = {
            @APIResponse(responseCode = "201", description = "Accepted", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.BadRequest.class)))
    })
    public Response addLecturer(LecturerRequestDto request) throws ValidationException {
        return Response.accepted().entity(lecturerService.saveLecturer(request)).build();
    }

    @PUT
    @Path("/{id}")
    @Operation(summary = "Update Lecturer", description = "This API will update Lecturer to database")
    @RequestBody(content = {
            @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.Request.class)) })
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.BadRequest.class)))
    })
    public Response updateLecturer(LecturerRequestDto request, @PathParam("id") Long id) throws ValidationException {
        return Response.ok().entity(lecturerService.updateLecturer(request, id)).build();
    }

    @GET
    @Operation(summary = "Get List Lecturer", description = "This API will get List Lecturer from Database")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.BadRequest.class)))
    })
    public Response getListLecturer() {
        return Response.ok().entity(lecturerService.getListLecturer()).build();
    }

    @GET
    @Path("/{id}")
    @Operation(summary = "Get Lecturer by ID", description = "This API will get Lecturer by ID from Database")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.BadRequest.class)))
    })
    public Response getLecturerByID(@PathParam("id") Long id) {
        return Response.ok().entity(lecturerService.getLecturerByID(id)).build();
    }

    @GET
    @Path("/lidn/{lidn}")
    @Operation(summary = "Get Lecturer by LIDN", description = "This API will get Lecturer by LIDN from Database")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.BadRequest.class)))
    })
    public Response getLecturerByLidn(@PathParam("lidn") String lidn) {
        return Response.ok().entity(lecturerService.getLecturerByLidn(lidn)).build();
    }

    @DELETE
    @Path("/{id}")
    @Operation(summary = "Delete Lecturer", description = "This API will delete Lecturer by ID")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = LecturerOAS.BadRequest.class)))
    })
    public Response deleteLecturer(@PathParam("id") Long id) throws ValidationException {
        return Response.ok().entity(lecturerService.deleteLecturer(id)).build();
    }
}

Before running this service, we must configure the application.properties to set the server, open-API, and database. Open the application.properties then add these lines of property codes.

quarkus.http.port=8082
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/api
quarkus.smallrye-openapi.info-title=Lecturer API
quarkus.datasource.username=djamware
quarkus.datasource.password=dj@mw@r3
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5433/school
quarkus.datasource.jdbc.max-size=16
quarkus.datasource.jdbc.driver=org.postgresql.Driver
quarkus.hibernate-orm.packages=com.djamware.school.lecturer.models
quarkus.hibernate-orm.database.default-schema=lecturer_schema
quarkus.hibernate-orm.database.generation=update

You can see that we are using PostgreSQL with the database name Lecturer and the schema name lecturer_schema. For that, create a new database and schema.

psql postgres -U djamware
\c school;
CREATE SCHEMA lecturer_schema;
\q

Now, we can run this service in dev mode by typing this command.

mvn quarkus:dev

The Quarkus service will run like this if no error is found.

A Simple Microservices using Quarkus, Kong, and Kafka - run quarkus

In the browser, go to http://localhost:8082/api/q/dev-ui/extensions, you will see this Quarkus dashboard page.

A Simple Microservices using Quarkus, Kong, and Kafka - swagger ui

We can choose the Swagger UI to test the endpoints that we have created previously. Now, you can test to POST, GET, UPDATE, and DELETE lecturers from this Swagger UI page.


Step #4: Create Study Service

This service differs from the previous two services because it uses MongoDB as a data store. Next, back to the school folder then create a new Quarkus service.

cd ..
mvn io.quarkus:quarkus-maven-plugin:3.14.2:create \
    -DplatformVersion=3.14.2 \
    -DprojectGroupId=com.djamware.school \
    -DprojectArtifactId=study \
    -DclassName="com.djamware.school.study.StudyResource" \
    -Dpath="api/studies" \
    -Dextensions="resteasy-reactive-jackson,mongodb-panache,smallrye-openapi,rest-jackson,rest-client-jackson"

Go to the newly created Quarkus service folder.

cd study

We will access Student and Lecturer services using the RestClient library. For that, make a new client folder and client interfaces file.

mkdir src/main/java/com/djamware/school/study/clients
touch src/main/java/com/djamware/school/study/clients/StudentClient.java
touch src/main/java/com/djamware/school/study/clients/LecturerClient.java

Open StudentClient.java then fill this file with these Java codes.

package com.djamware.school.study.clients;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

@Path("/students")
@RegisterRestClient(configKey = "student-api")
public interface StudentClient {
    @GET
    @Path("/{sidn}")
    StudentResponseDto getStudentBySidn(@PathParam("sidn") String sidn);
}

You can see that the file has StudyResponseDto.java, for that, create the DTO folder inside the client folder then make the DTO file inside it.

mkdir src/main/java/com/djamware/school/study/clients/dtos
touch src/main/java/com/djamware/school/study/clients/dtos/StudentResponseDto.java

Open the StudyResponseDto.java then add these lines of Java codes.

package com.djamware.school.study.clients.dtos;

import java.util.Date;

public class StudentResponseDto {
    private Long id;
    private String sidn;
    private String name;
    private String email;
    private String phone;
    private Date createdAt;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getSidn() {
        return sidn;
    }

    public void setSidn(String sidn) {
        this.sidn = sidn;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}

Also, we need the DTO for the LecturerClient, then create a new LecturerResponseDto inside the same folder.

touch src/main/java/com/djamware/school/study/clients/dtos/LecturerResponseDto.java

Open LecturerResponseDto.java then fill this file with these Java codes.

package com.djamware.school.study.clients.dtos;

import java.util.Date;

public class LecturerResponseDto {
    private Long id;
    private String lidn;
    private String name;
    private String email;
    private String phone;
    private String major;
    private Date createdAt;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getLidn() {
        return lidn;
    }

    public void setLidn(String lidn) {
        this.lidn = lidn;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getMajor() {
        return major;
    }

    public void setMajor(String major) {
        this.major = major;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}

Next, open LecturerClient.java then fill this file with these Java codes.

package com.djamware.school.study.clients;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import com.djamware.school.study.clients.dtos.StudentResponseDto;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

@Path("/lecturers")
@RegisterRestClient(configKey = "lecturer-api")
public interface LecturerClient {
    @GET
    @Path("/{lidn}")
    StudentResponseDto getLecturerByLidn(@PathParam("lidn") String lidn);
}

We will use the Panache repository pattern to access MongoDB. Next, create a model folder and model or entity Pojo file.

mkdir src/main/java/com/djamware/school/study/model
touch src/main/java/com/djamware/school/study/model/Study.java

Open the Study.java file then add these lines of POJO of the Panache Entity.

package com.djamware.school.study.model;

import java.time.LocalDate;
import java.util.List;

import org.bson.types.ObjectId;

import io.quarkus.mongodb.panache.common.MongoEntity;

@MongoEntity(collection = "study")
public class Study {
    private ObjectId id;
    private String studyName;
    private String studyDescription;
    private LocalDate studyDate;
    private String lecturerId;
    private String lecturerName;
    private List<Student> students;

    public ObjectId getId() {
        return id;
    }

    public void setId(ObjectId id) {
        this.id = id;
    }

    public String getStudyName() {
        return studyName;
    }

    public void setStudyName(String studyName) {
        this.studyName = studyName;
    }

    public String getStudyDescription() {
        return studyDescription;
    }

    public void setStudyDescription(String studyDescription) {
        this.studyDescription = studyDescription;
    }

    public LocalDate getStudyDate() {
        return studyDate;
    }

    public void setStudyDate(LocalDate studyDate) {
        this.studyDate = studyDate;
    }

    public String getLecturerId() {
        return lecturerId;
    }

    public void setLecturerId(String lecturerId) {
        this.lecturerId = lecturerId;
    }

    public String getLecturerName() {
        return lecturerName;
    }

    public void setLecturerName(String lecturerName) {
        this.lecturerName = lecturerName;
    }

    public List<Student> getStudents() {
        return students;
    }

    public void setStudents(List<Student> students) {
        this.students = students;
    }
}

As you can see, we have included the list of students who joined the study. Add the Student.java file then fill that file with this POJO code.

package com.djamware.school.study.model;

import io.quarkus.mongodb.panache.common.MongoEntity;

@MongoEntity(collection = "student")
public class Student {
    private String sidn;
    private String name;

    public String getSidn() {
        return sidn;
    }

    public void setSidn(String sidn) {
        this.sidn = sidn;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Next, create a new folder called Repositories and new files for the Study Repository and Student Repository.

mkdir src/main/java/com/djamware/school/study/repositories
touch src/main/java/com/djamware/school/study/repositories/StudyRepository.java
touch src/main/java/com/djamware/school/study/repositories/StudentRepository.java

Open StudentRepository.java then add these lines of Java interface codes.

package com.djamware.school.study.repositories;

import com.djamware.school.study.model.Study;

import io.quarkus.mongodb.panache.PanacheMongoRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class StudyRepository implements PanacheMongoRepository<Study> {

}

Open StudyRepository.java then add these lines of Java interface codes.

package com.djamware.school.study.repositories;

import com.djamware.school.study.model.Student;

import io.quarkus.mongodb.panache.PanacheMongoRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class StudentRepository implements PanacheMongoRepository<Student> {

}

Next, create a new Services folder and file for the Study Service.

mkdir src/main/java/com/djamware/school/study/services
touch src/main/java/com/djamware/school/study/services/StudyService.java

Open StudyService.java then add these lines of Java codes.

package com.djamware.school.study.services;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.bson.types.ObjectId;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import com.djamware.school.study.clients.LecturerClient;
import com.djamware.school.study.clients.StudentClient;
import com.djamware.school.study.clients.dtos.LecturerResponseDto;
import com.djamware.school.study.clients.dtos.StudentResponseDto;
import com.djamware.school.study.dtos.StudyRequestDto;
import com.djamware.school.study.model.Student;
import com.djamware.school.study.model.Study;
import com.djamware.school.study.repositories.StudentRepository;
import com.djamware.school.study.repositories.StudyRepository;

import io.vertx.core.json.JsonObject;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.xml.bind.ValidationException;

@ApplicationScoped
public class StudyService {
    StudyRepository studyRepository;
    StudentRepository studentRepository;

    @RestClient
    StudentClient studentClient;

    @RestClient
    LecturerClient lecturerClient;

    @Inject
    public StudyService(StudentRepository studentRepository, StudyRepository studyRepository) {
        this.studentRepository = studentRepository;
        this.studyRepository = studyRepository;
    }

    @Transactional
    public JsonObject saveStudy(StudyRequestDto req) throws ValidationException {
        if (req == null)
            throw new ValidationException(BAD_REQUEST);

        Study newStudy = new Study();
        newStudy.setStudyName(req.getStudyName());
        newStudy.setStudyDescription(req.getStudyDescription());
        newStudy.setStudyDate(LocalDate.parse(req.getStudyDate(),
                DateTimeFormatter.ofPattern("yyyy-MM-dd").withLocale(new Locale("id", "ID"))));

        LecturerResponseDto lecturer = lecturerClient.getLecturerByLidn(req.getLidn());
        newStudy.setLecturerId(lecturer.getLidn());
        newStudy.setLecturerName(lecturer.getName());

        List<Student> students = new ArrayList<>();

        req.getSidn().stream().forEach(sidn -> {
            StudentResponseDto student = studentClient.getStudentBySidn(sidn);
            Student newStudent = new Student();
            newStudent.setSidn(student.getSidn());
            newStudent.setName(student.getName());
            studentRepository.persist(newStudent);
            students.add(newStudent);
        });

        newStudy.setStudents(students);
        studyRepository.persist(newStudy);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_201_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

    @Transactional
    public JsonObject updateStudy(StudyRequestDto req, ObjectId id) throws ValidationException {
        if (req == null)
            throw new ValidationException(BAD_REQUEST);

        Study study = studyRepository.findById(id);
        if (study == null)
            throw new ValidationException(DATA_NOT_FOUND);

        LecturerResponseDto lecturer = lecturerClient.getLecturerByLidn(req.getLidn());
        List<Student> students = new ArrayList<>();

        req.getSidn().stream().forEach(sidn -> {
            StudentResponseDto student = studentClient.getStudentBySidn(sidn);
            Student newStudent = new Student();
            newStudent.setSidn(student.getSidn());
            newStudent.setName(student.getName());
            studentRepository.persist(newStudent);
            students.add(newStudent);
        });

        studyRepository.update(
                "studyName = ?1, studyDescription = ?2, studyDate = ?3, lecturerId = ?4, lecturerName = ?5, students = ?6 where id = ?7",
                req.getStudyName(), req.getStudyDescription(), LocalDate.parse(req.getStudyDate(),
                        DateTimeFormatter.ofPattern("yyyy-MM-dd").withLocale(new Locale("id", "ID"))),
                lecturer.getLidn(), lecturer.getName(), students, id);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

    public JsonObject getListStudy() {
        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);
        resp.put(DATA_KEY, studyRepository.listAll());

        return resp;
    }

    public JsonObject getStudyByID(ObjectId id) {
        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);
        resp.put(DATA_KEY, studyRepository.findById(id));

        return resp;
    }

    @Transactional
    public JsonObject deleteStudy(ObjectId id) throws ValidationException {
        if (id == null)
            throw new ValidationException(BAD_REQUEST);

        Study study = studyRepository.findById(id);
        if (study != null)
            studyRepository.delete(study);

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_200_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }
}

There are many static constants in that file. Create a new util folder and a new Constants.java file.

mkdir src/main/java/com/djamware/school/study/utils
touch src/main/java/com/djamware/school/study/utils/Constants.java

Add or replace that file with these.

package com.djamware.school.study.utils;

public class Constants {
    private Constants() {
    }

    public static final String BAD_REQUEST = "BAD_REQUEST";
    public static final String DATA_NOT_FOUND = "DATA_NOT_FOUND";
    public static final String CODE_KEY = "code";
    public static final String CODE_201_VAL = "201";
    public static final String CODE_200_VAL = "200";
    public static final String MESSAGE_KEY = "message";
    public static final String MESSAGE_VAL = "Success";
    public static final String DATA_KEY = "data";
}

Back to the StudyService.java then add these lines of imports.

import static com.djamware.school.lecturer.utils.Constants.BAD_REQUEST;
import static com.djamware.school.lecturer.utils.Constants.CODE_200_VAL;
import static com.djamware.school.lecturer.utils.Constants.CODE_201_VAL;
import static com.djamware.school.lecturer.utils.Constants.CODE_KEY;
import static com.djamware.school.lecturer.utils.Constants.DATA_NOT_FOUND;
import static com.djamware.school.lecturer.utils.Constants.MESSAGE_KEY;
import static com.djamware.school.lecturer.utils.Constants.MESSAGE_VAL;
import static com.djamware.school.lecturer.utils.Constants.DATA_KEY;

Before implementing open API in our resources, we must define the Open API Specification (OAS) file to describe the Request and Response. Create a folder in the DTOS folder then create this OAS file.

mkdir src/main/java/com/djamware/school/study/dtos/oas
touch src/main/java/com/djamware/school/study/dtos/oas/StudyOAS.java

Open StudyOAS.java then add these lines of Java codes.

package com.djamware.school.study.dtos.oas;

import java.util.List;

import org.bson.types.ObjectId;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

public class StudyOAS {
    @Schema(name = "StudyOAS.Request")
    public class Request {
        @Schema(required = true, example = "DSA")
        public String studyName;
        @Schema(required = true, example = "Data Structure and Algorithm")
        public String studyDescription;
        @Schema(required = true, example = "2024-10-18 09:00:00")
        public String studyDate;
        @Schema(required = true, example = "A12345")
        public String lidn;
        @Schema(required = true, example = "S0001,S0002,S003")
        public List<String> sidn;
    }

    @Schema(name = "StudyOAS.Response")
    public class Response {
        @Schema(required = true, example = "1")
        public ObjectId id;
        @Schema(required = true, example = "DSA")
        public String studyName;
        @Schema(required = true, example = "Data Structure and Algorithm")
        public String studyDescription;
        @Schema(required = true, example = "2024-10-18 09:00:00")
        public String studyDate;
        @Schema(required = true, example = "A12345")
        public String lidn;
        @Schema(required = true, example = "S0001,S0002,S003")
        public List<String> sidn;
    }

    @Schema(name = "StudyOAS.BadRequest")
    public class BadRequest {
        @Schema(example = "BAD_REQUEST", enumeration = { "BAD_REQUEST", "INVALID_BODY" })
        public String message;
    }
}

Back to the StudyResource.java then replace the existing Java codes with these. 

package com.djamware.school.study;

import org.bson.types.ObjectId;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;

import com.djamware.school.study.dtos.StudyRequestDto;
import com.djamware.school.study.dtos.oas.StudyOAS;
import com.djamware.school.study.services.StudyService;

import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.xml.bind.ValidationException;

@Path("/v1/studies")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class StudyResource {
    StudyService studyService;

    @Inject
    public StudyResource(StudyService studyService) {
        this.studyService = studyService;
    }

    @POST
    @Operation(summary = "Add a new study", description = "This API will add a new study to database")
    @RequestBody(content = {
            @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.Request.class)) })
    @APIResponses(value = {
            @APIResponse(responseCode = "201", description = "Accepted", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.BadRequest.class)))
    })
    public Response addLecturer(StudyRequestDto request) throws ValidationException {
        return Response.accepted().entity(studyService.saveStudy(request)).build();
    }

    @PUT
    @Path("/{id}")
    @Operation(summary = "Update Study", description = "This API will update Study to database")
    @RequestBody(content = {
            @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.Request.class)) })
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.BadRequest.class)))
    })
    public Response updateLecturer(StudyRequestDto request, @PathParam("id") ObjectId id) throws ValidationException {
        return Response.ok().entity(studyService.updateStudy(request, id)).build();
    }

    @GET
    @Operation(summary = "Get List Study", description = "This API will get List Study from Database")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.BadRequest.class)))
    })
    public Response getListLecturer() {
        return Response.ok().entity(studyService.getListStudy()).build();
    }

    @GET
    @Path("/{id}")
    @Operation(summary = "Get Study by ID", description = "This API will get Study by ID from Database")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.BadRequest.class)))
    })
    public Response getLecturerByID(@PathParam("id") ObjectId id) {
        return Response.ok().entity(studyService.getStudyByID(id)).build();
    }

    @DELETE
    @Path("/{id}")
    @Operation(summary = "Delete Study", description = "This API will delete Study by ID")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.Response.class))),
            @APIResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = StudyOAS.BadRequest.class)))
    })
    public Response deleteLecturer(@PathParam("id") ObjectId id) throws ValidationException {
        return Response.ok().entity(studyService.deleteStudy(id)).build();
    }
}

Before running this service, we must configure the application.properties to set the server, open-API, and database. Open the application.properties then add these lines of property codes.

quarkus.http.port=8083
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/api
quarkus.smallrye-openapi.info-title=Study API
quarkus.mongodb.connection-string=mongodb://localhost:27017
quarkus.mongodb.database=study
quarkus.datasource.jdbc.driver=org.postgresql.Driver
quarkus.hibernate-orm.packages=com.djamware.school.study.model
quarkus.hibernate-orm.database.generation=update

Now, we can run this service in dev mode by typing this command.

mvn quarkus:dev

The Quarkus service will run like this if no error is found.

A Simple Microservices using Quarkus, Kong, and Kafka - run quarkus 

In the browser, go to http://localhost:8082/api/q/dev-ui/extensions, you will see this Quarkus dashboard page.

A Simple Microservices using Quarkus, Kong, and Kafka - swagger ui

We can choose the Swagger UI to test the endpoints that we have created previously. Now, you can test to POST, GET, UPDATE, and DELETE lecturers from this Swagger UI page.


Step #5: Run All Services as Docker Containers

Before building all services for Docker containers, we have to prepare the PostgreSQL and MongoDB Docker containers. On Mac OS we need to run the Docker app to make it work in the terminal. Next, open the terminal and create a new folder (db-docker) under the school-app folder.

cd ..
mkdir db-docker

Create 2 Docker Compose configuration files.

touch mongodb-docker-compose.yml
touch postgresql-docker-compose.yml

Open the mongodb-docker-compose.yml then add these lines of YAML codes.

version: '3.9'

services:
  mongodb:
    image: mongo:7-jammy
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: djamware
      MONGO_INITDB_ROOT_PASSWORD: djAmwAr3
    ports:
      - "27017:27017"
    networks:
      - school-network

networks:
  school-network:
    name: school-network
    external: true

Open the postgresql-docker-compose.yml then add these lines of YAML codes.

version: '3.9'

services:
  postgresql:
    image: postgres:14
    restart: always
    shm_size: 128mb
    environment:
      POSTGRES_DB: school
      POSTGRES_USER: djamware
      POSTGRES_PASSWORD: dj@mw@r3
    ports:
      - "5433:5432"
    networks:
      - school-network
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

networks:
  school-network:
    name: school-network
    external: true

Create a new SQL file under the same folder.

touch init.sql

Open that file then add these lines of SQL codes to create the required Schemas.

CREATE SCHEMA student_schema;
CREATE SCHEMA lecturer_schema;

Before running those Docker Containers, create a new Docker Network by typing this command.

docker network create school-network

Next, run both Docker Containers by typing these commands.

docker compose -f postgresql-docker-compose.yml up -d
docker compose -f mongodb-docker-compose.yml up -d

You will see on your Docker App the running containers.

A Simple Microservices using Quarkus, Kong, and Kafka - docker ui

Next, modify the "application.properties" of the Student Service to be like this.

quarkus.http.port=8081
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/api
quarkus.smallrye-openapi.info-title=Student API
quarkus.datasource.username=djamware
quarkus.datasource.password=dj@mw@r3
quarkus.datasource.jdbc.url=jdbc:postgresql://host.docker.internal:5433/school
quarkus.datasource.jdbc.max-size=16
quarkus.datasource.jdbc.driver=org.postgresql.Driver
quarkus.hibernate-orm.packages=com.djamware.school.student.models
quarkus.hibernate-orm.database.default-schema=student_schema
quarkus.hibernate-orm.database.generation=update

In the terminal, back to the student folder then type this command to build the Quarkus service.

cd ../student
mvn clean package -DskipTests

Next, build the Docker container by typing this command.

docker build -f src/main/Docker/Dockerfile.jvm -t student .

Run the Docker container by typing this command.

docker run -i --rm -p 8081:8081 --network=school-network -d student

Next, modify the "application.properties" of the Lecturer Service to be like this.

quarkus.http.port=8082
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/api
quarkus.smallrye-openapi.info-title=Lecturer API
quarkus.datasource.username=djamware
quarkus.datasource.password=dj@mw@r3
quarkus.datasource.jdbc.url=jdbc:postgresql://host.docker.internal:5433/school
quarkus.datasource.jdbc.max-size=16
quarkus.datasource.jdbc.driver=org.postgresql.Driver
quarkus.hibernate-orm.packages=com.djamware.school.lecturer.models
quarkus.hibernate-orm.database.default-schema=lecturer_schema
quarkus.hibernate-orm.database.generation=update

In the terminal, back to the student folder then type this command to build the Quarkus service.

cd ../lecturer
mvn clean package -DskipTests

Next, build the Docker container by typing this command.

docker build -f src/main/Docker/Dockerfile.jvm -t lecturer .

Run the Docker container by typing this command.

docker run -i --rm -p 8082:8082 --network=school-network -d lecturer

Next, modify the "application.properties" of the Study Service to be like this.

quarkus.http.port=8083
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/api
quarkus.smallrye-openapi.info-title=Study API
quarkus.mongodb.connection-string=mongodb://djamware:[email protected]:27017
quarkus.mongodb.database=study
# Rest Client
quarkus.tls.trust-all=true
quarkus.rest-client.student-api.url=http://localhost:8081
quarkus.rest-client.student-api.scope=jakarta.inject.Singleton
quarkus.rest-client.lecturer-api.url=http://localhost:8082
quarkus.rest-client.lecturer-api.scope=jakarta.inject.Singleton

In the terminal, back to the student folder then type this command to build the Quarkus service.

cd ../study
mvn clean package -DskipTests

Next, build the Docker container by typing this command.

docker build -f src/main/Docker/Dockerfile.jvm -t study .

Run the Docker container by typing this command.

docker run -i --rm -p 8083:8083 --network=school-network -d study

Now, all services running on different ports.


Step #6: Register All Services to Kong

While all services run, we must run another PostgreSQL service on Docker for Kong Gateway. Type this command to create a new Docker network.

docker network create kong-net

Next, run the Kong PostgreSQL Database by typing this command.

docker run -d --name kong-database \
 --network=kong-net \
 -p 5432:5432 \
 -e "POSTGRES_USER=kong" \
 -e "POSTGRES_DB=kong" \
 -e "POSTGRES_PASSWORD=kongpass" \
 postgres:13

Run another Docker command to create migration data for Kong PostgreSQL.

docker run -d --name kong-database \
 --network=kong-net \
 -p 5432:5432 \
 -e "POSTGRES_USER=kong" \
 -e "POSTGRES_DB=kong" \
 -e "POSTGRES_PASSWORD=kongpass" \
 postgres:13

Now, we can run the Kong and Kong Admin GUI by typing this command.

docker run -d --name kong-gateway \
 --network=kong-net \
 -e "KONG_DATABASE=postgres" \
 -e "KONG_PG_HOST=kong-database" \
 -e "KONG_PG_USER=kong" \
 -e "KONG_PG_PASSWORD=kongpass" \
 -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
 -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
 -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
 -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
 -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
 -e "KONG_ADMIN_GUI_URL=http://localhost:8002" \
 -p 8000:8000 \
 -p 8443:8443 \
 -p 8001:8001 \
 -p 8444:8444 \
 -p 8002:8002 \
 -p 8445:8445 \
 -p 8003:8003 \
 -p 8004:8004 \
 kong/kong-gateway:3.8.0.0

To ensure this Kong is running, type this command in the terminal.

curl -i -X GET --url http://localhost:8001/services

HTTP/1.1 200 OK
Date: Sat, 12 Oct 2024 02:13:46 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Access-Control-Allow-Origin: http://localhost:8002
X-Kong-Admin-Request-ID: ac337133b457417c97d96351535e712c
vary: Origin
Access-Control-Allow-Credentials: true
Content-Length: 23
X-Kong-Admin-Latency: 70
Server: kong/3.8.0.0-enterprise-edition

{"next":null,"data":[]}

We can view services and routes using the Kong Admin GUI by entering this URL in the browser.

A Simple Microservices using Quarkus, Kong, and Kafka - kong ui

To create the services and routes we will use CURL in the terminal. Type this command to create a new student service.

curl -i -s -X POST http://localhost:8001/services \
  --data name=student_service \
  --data url='http://192.168.1.7:8081/api/v1/students'

To view a newly created service, type this command.

curl -X GET http://localhost:8001/services/student_service

To update an existing service type this command.

curl --request PATCH \
  --url localhost:8001/services/student_service \
  --data url='http://192.168.1.7:8081/api/v1/students'

Add a route to the student service by typing this command.

curl -i -X POST http://localhost:8001/services/student_service/routes \
 --data 'paths[]=/api/v1/students' \
 --data name=student_route

View the newly created route by typing this command.

curl -X GET http://localhost:8001/services/student_service/routes/student_route

Now, the student services are accessible through the Kong Gateway.

curl --location 'http://localhost:8000/api/v1/students' \
--header 'Accept: application/json'

Next, type this command to create a new lecturer service.

curl -i -s -X POST http://localhost:8001/services \
  --data name=lecturer_service \
  --data url='http://192.168.1.7:8082/api/v1/lecturers'

To view a newly created service, type this command.

curl -X GET http://localhost:8001/services/lecturer_service

To update an existing service type this command.

curl --request PATCH \
  --url localhost:8001/services/lecturer_service \
  --data url='http://192.168.1.7:8082/api/v1/lecturers'

Add a route to the lecturer service by typing this command.

curl -i -X POST http://localhost:8001/services/lecturer_service/routes \
 --data 'paths[]=/api/v1/lecturers' \
 --data name=lecturer_route

View the newly created route by typing this command.

curl -X GET http://localhost:8001/services/lecturer_service/routes/lecturer_route

Now, the lecturer services are accessible through the Kong Gateway.

curl --location 'http://localhost:8000/api/v1/lecturers' \
--header 'Accept: application/json'

Before registering the Study Service in Kong, update the "application.properties" of the Study Service to match the Rest Client URL.

quarkus.http.port=8083
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/api
quarkus.smallrye-openapi.info-title=Study API
quarkus.mongodb.connection-string=mongodb://djamware:[email protected]:27017
quarkus.mongodb.database=study
# Rest Client
quarkus.tls.trust-all=true
quarkus.rest-client.student-api.url=http://host.docker.internal:8081
quarkus.rest-client.student-api.scope=jakarta.inject.Singleton
quarkus.rest-client.student-api.verify-host=false
quarkus.rest-client.lecturer-api.url=http://host.docker.internal:8082
quarkus.rest-client.lecturer-api.scope=jakarta.inject.Singleton
quarkus.rest-client.lecturer-api.verify-host=false

Build the Docker and re-run the Docker container.

cd ../study
mvn clean package -DskipTests
docker system prune
docker build -f src/main/Docker/Dockerfile.jvm -t study .
docker run -i --rm -p 8083:8083 --network=school-network -d study

Next, type this command to create a new study service.

curl -i -s -X POST http://localhost:8001/services \
  --data name=study_service \
  --data url='http://192.168.1.7:8083/api/v1/studies'

To view a newly created service, type this command.

curl -X GET http://localhost:8001/services/study_service

To update an existing service type this command.

curl --request PATCH \
  --url localhost:8001/services/study_service \
  --data url='http://192.168.1.7:8083/api/v1/studies'

Add a route to the study service by typing this command.

curl -i -X POST http://localhost:8001/services/study_service/routes \
 --data 'paths[]=/api/v1/studies' \
 --data name=study_route

View the newly created route by typing this command.

curl -X GET http://localhost:8001/services/study_service/routes/study_route

Now, the study services are accessible through the Kong Gateway.

curl --location 'http://localhost:8000/api/v1/studies' \
--header 'Accept: application/json'


Step #7: Add Kafka Producer and Consumer

The scenario for message exchange between services is that the Study service produces a message, and then the Student and Lecturer services consume it. Begin by creating a new entity on Student and Lecturer services to save messages consumed by Kafka. On Student Service, create a new Java file under the models folder.

touch src/main/java/com/djamware/school/student/models/StudyActivity.java

Open that file then add these lines of Java codes.

package com.djamware.school.student.models;

import org.hibernate.annotations.CreationTimestamp;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import java.util.Date;

@Entity
@Table(name = "study_activity")
public class StudyActivity {
    @Id
    @SequenceGenerator(name = "studySequence", sequenceName = "study_id_seq", allocationSize = 1, initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "studySequence")
    @Column(name = "id", nullable = false)
    public Long id;

    @Column(name = "sidn", length = 16, nullable = false)
    public String sidn;

    @Column(name = "study_name", length = 100, nullable = false)
    public String studyName;

    @Column(name = "study_start_time", nullable = false)
    public Date studyStartTime;

    @Column(name = "study_end_time", nullable = false)
    public Date studyEndTime;

    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    public Date createdAt;
}

Next, create a new Java file under the repositories folder.

touch src/main/java/com/djamware/school/student/repositories/StudyActivityRepository.java

Open that file then add these lines of Java codes.

package com.djamware.school.student.repositories;

import com.djamware.school.student.models.StudyActivity;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class StudyActivityRepository implements PanacheRepository<StudyActivity> {

}

Before consuming the Kafka messages, add this Kafka extension by typing this command.

quarkus extension add messaging-kafka

Add these lines to the "application.properties" file.

kafka.bootstrap.servers=kafka:9092
mp.messaging.incoming.study-activity.connector=smallrye-kafka

Next, create a new folder under the student folder and add a new Java file.

mkdir src/main/java/com/djamware/school/student/consumers
touch src/main/java/com/djamware/school/student/consumers/StudyActivityConsumer.java

Open that file then add these lines of Java codes.

package com.djamware.school.student.consumers;

import org.eclipse.microprofile.reactive.messaging.Incoming;

import com.djamware.school.student.dtos.StudyActivityPayload;
import com.djamware.school.student.models.StudyActivity;
import com.djamware.school.student.repositories.StudyActivityRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

@ApplicationScoped
public class StudyActivityConsumer {
    private final ObjectMapper objectMapper = new ObjectMapper();
    StudyActivityRepository studyActivityRepository;

    @Inject
    public StudyActivityConsumer(StudyActivityRepository studyActivityRepository) {
        this.studyActivityRepository = studyActivityRepository;
    }

    @Incoming("study-activity")
    @Transactional(Transactional.TxType.REQUIRED)
    public void saveStudyActivity(String message) throws JsonProcessingException {
        final StudyActivityPayload studyActivityPayload = objectMapper.readValue(message, StudyActivityPayload.class);
        studyActivityPayload.getSidn().stream().forEach(sidn -> {
            StudyActivity sa = new StudyActivity();
            sa.sidn = sidn;
            sa.lidn = studyActivityPayload.getLidn();
            sa.studyName = studyActivityPayload.getLidn();
            sa.studyStartTime = studyActivityPayload.getStudyStartTime();
            sa.studyEndTime = studyActivityPayload.getStudyEndTime();
            studyActivityRepository.persist(sa);
        });
    }
}

You see, we need to create a DTO mapped from the Kafka message. Create a new DTO file under the dtos folder.

touch src/main/java/com/djamware/school/student/dtos/StudyActivityPayload.java

Open that file then add these lines of Java codes.

package com.djamware.school.student.dtos;

import java.util.Date;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class StudyActivityPayload {
    private List<String> sidn;
    private String lidn;
    private String studyName;
    private Date studyStartTime;
    private Date studyEndTime;

    public List<String> getSidn() {
        return sidn;
    }

    public void setSidn(List<String> sidn) {
        this.sidn = sidn;
    }

    public String getStudyName() {
        return studyName;
    }

    public void setStudyName(String studyName) {
        this.studyName = studyName;
    }

    public Date getStudyStartTime() {
        return studyStartTime;
    }

    public void setStudyStartTime(Date studyStartTime) {
        this.studyStartTime = studyStartTime;
    }

    public Date getStudyEndTime() {
        return studyEndTime;
    }

    public void setStudyEndTime(Date studyEndTime) {
        this.studyEndTime = studyEndTime;
    }

    public String getLidn() {
        return lidn;
    }

    public void setLidn(String lidn) {
        this.lidn = lidn;
    }
}

Do the same as the previous step for the Lecturer Service. The pom.xml will now have this dependency.

...
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-messaging-kafka</artifactId>
        </dependency>
...

The "application.properties" will have these lines.

kafka.bootstrap.servers=kafka:9092
mp.messaging.incoming.study-activity.connector=smallrye-kafka

The models/StudyActivity.java will look like this.

package com.djamware.school.lecturer.models;

import java.util.Date;
import java.util.List;

import org.hibernate.annotations.CreationTimestamp;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;

@Entity
@Table(name = "study_activity")
public class StudyActivity {
    @Id
    @SequenceGenerator(name = "studySequence", sequenceName = "study_id_seq", allocationSize = 1, initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "studySequence")
    @Column(name = "id", nullable = false)
    public Long id;

    @Column(name = "sidn", length = 16, nullable = false)
    public List<String> sidn;

    @Column(name = "lidn", length = 16, nullable = false)
    public String lidn;

    @Column(name = "study_name", length = 100, nullable = false)
    public String studyName;

    @Column(name = "study_start_time", nullable = false)
    public Date studyStartTime;

    @Column(name = "study_end_time", nullable = false)
    public Date studyEndTime;

    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    public Date createdAt;
}

The repositories/StudyActivityRepository.java will look like this.

package com.djamware.school.lecturer.repositories;

import com.djamware.school.lecturer.models.StudyActivity;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class StudyActivityRepository implements PanacheRepository<StudyActivity> {

}

The dtos/StudyActivityPayload.java will look like this.

package com.djamware.school.lecturer.dtos;

import java.util.Date;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class StudyActivityPayload {
    private List<String> sidn;
    private String lidn;
    private String studyName;
    private String studyStartTime;
    private String studyEndTime;

    public List<String> getSidn() {
        return sidn;
    }

    public void setSidn(List<String> sidn) {
        this.sidn = sidn;
    }

    public String getStudyName() {
        return studyName;
    }

    public void setStudyName(String studyName) {
        this.studyName = studyName;
    }

    public String getStudyStartTime() {
        return studyStartTime;
    }

    public void setStudyStartTime(String studyStartTime) {
        this.studyStartTime = studyStartTime;
    }

    public String getStudyEndTime() {
        return studyEndTime;
    }

    public void setStudyEndTime(String studyEndTime) {
        this.studyEndTime = studyEndTime;
    }

    public String getLidn() {
        return lidn;
    }

    public void setLidn(String lidn) {
        this.lidn = lidn;
    }
}

The consumers/StudyActivityConsumer.java will look like this.

package com.djamware.school.lecturer.consumers;

import org.eclipse.microprofile.reactive.messaging.Incoming;

import com.djamware.school.lecturer.dtos.StudyActivityPayload;
import com.djamware.school.lecturer.models.StudyActivity;
import com.djamware.school.lecturer.repositories.StudyActivityRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

@ApplicationScoped
public class StudyActivityConsumer {
    private final ObjectMapper objectMapper = new ObjectMapper();
    StudyActivityRepository studyActivityRepository;

    @Inject
    public StudyActivityConsumer(StudyActivityRepository studyActivityRepository) {
        this.studyActivityRepository = studyActivityRepository;
    }

    @Incoming("study-activity")
    @Transactional(Transactional.TxType.REQUIRED)
    public void saveStudyActivity(String message) throws JsonProcessingException {
        final StudyActivityPayload studyActivityPayload = objectMapper.readValue(message, StudyActivityPayload.class);
        StudyActivity sa = new StudyActivity();
        sa.sidn = studyActivityPayload.getSidn();
        sa.lidn = studyActivityPayload.getLidn();
        sa.studyName = studyActivityPayload.getLidn();
        sa.studyStartTime = studyActivityPayload.getStudyStartTime();
        sa.studyEndTime = studyActivityPayload.getStudyEndTime();
        studyActivityRepository.persist(sa);
    }
}

Open the Study Service to create a Kafka Message Producer. Type this command to install Kafka dependency.

quarkus extension add messaging-kafka

Open "application.properties" and then add these lines to the Kafka configuration.

mp.messaging.outgoing.study-activity.connector=smallrye-kafka
mp.messaging.outgoing.study-activity.topic=study-activity
mp.messaging.outgoing.study-activity.value.serializer=org.apache.kafka.common.serialization.StringSerializer

We will produce a Kafka message when creating a new study. Open the StudyService.java then add this import.

import static com.djamware.school.study.utils.Constants.LIDN_KEY;
import static com.djamware.school.study.utils.Constants.MESSAGE_KEY;
import static com.djamware.school.study.utils.Constants.MESSAGE_VAL;
import static com.djamware.school.study.utils.Constants.SIDN_KEY;
import static com.djamware.school.study.utils.Constants.STUDY_END_TIME_KEY;
import static com.djamware.school.study.utils.Constants.STUDY_NAME_KEY;
import static com.djamware.school.study.utils.Constants.STUDY_START_TIME_KEY;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;

Add send message to Kafka, so, the save study method looks like this.

    @Transactional
    public JsonObject saveStudy(StudyRequestDto req) throws ValidationException, JsonProcessingException {
        if (req == null)
            throw new ValidationException(BAD_REQUEST);

        Study newStudy = new Study();
        newStudy.setStudyName(req.getStudyName());
        newStudy.setStudyDescription(req.getStudyDescription());
        newStudy.setStudyDate(LocalDate.parse(req.getStudyDate(),
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withLocale(new Locale("id", "ID"))));

        LecturerResponseDto lecturer = lecturerClient.getLecturerByLidn(req.getLidn());
        newStudy.setLecturerId(lecturer.getLidn());
        newStudy.setLecturerName(lecturer.getName());

        List<Student> students = new ArrayList<>();

        req.getSidn().stream().forEach(sidn -> {
            StudentResponseDto student = studentClient.getStudentBySidn(sidn);
            Student newStudent = new Student();
            newStudent.setSidn(student.getSidn());
            newStudent.setName(student.getName());
            studentRepository.persist(newStudent);
            students.add(newStudent);
        });

        newStudy.setStudents(students);
        studyRepository.persist(newStudy);

        // Send message to Kafka
        Map<String, Object> payload = new HashMap<>();
        payload.put(SIDN_KEY, req.getSidn());
        payload.put(LIDN_KEY, req.getLidn());
        payload.put(STUDY_NAME_KEY, req.getStudyName());
        payload.put(STUDY_START_TIME_KEY, req.getStudyDate());
        payload.put(STUDY_END_TIME_KEY, req.getStudyDate());
        emitterSendStudyActivity.send(mapper.writeValueAsString(payload));

        JsonObject resp = new JsonObject();
        resp.put(CODE_KEY, CODE_201_VAL);
        resp.put(MESSAGE_KEY, MESSAGE_VAL);

        return resp;
    }

Don't forget to add variables to the Constants.java.

    public static final String SIDN_KEY = "sidn";
    public static final String LIDN_KEY = "lidn";
    public static final String STUDY_NAME_KEY = "studyName";
    public static final String STUDY_START_TIME_KEY = "studyStartTime";
    public static final String STUDY_END_TIME_KEY = "studyEndTime";

In the terminal, back to the db-docker folder then create a new YAML file.

cd ../db-docker
touch kafka-docker-compose.yml

Open the "kafka-docker-compose.yml" file and add these lines of YAML codes.

version: "3.3"
services:
    zookeeper:
        image: bitnami/zookeeper
        networks:
            - "quarkus-app"
        ports:
            - "2181:2181"
        environment:
            - ALLOW_ANONYMOUS_LOGIN=yes
    kafka:
        image: bitnami/kafka
        networks:
            - "quarkus-app"
        ports:
            - "9092:9092"
            - "9093:9093"
        environment:
            - KAFKA_BROKER_ID=1
            - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
            - KAFKA_LISTENERS=PLAINTEXT://:9092,EXTERNAL://:9093
            - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://host.docker.internal:9092,EXTERNAL://localhost:9093
            - KAFKA_ADVERTISED_HOST_NAME=127.0.0.1
            - KAFKA_ADVERTISED_PORT=9092
            - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
            - ALLOW_PLAINTEXT_LISTENER=yes
        links:
            - zookeeper

    kafka-ui:
        image: provectuslabs/kafka-ui:latest
        hostname: kafka-ui
        ports:
            - "127.0.0.1:9003:8080"
        expose:
            - "9000"
        restart: "no"
        environment:
            - KAFKA_CLUSTERS_0_NAME=kafka:9092
            - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092
        networks:
            - quarkus-app
        links:
            - kafka

networks:
    quarkus-app:
        driver: bridge
        ipam:
            driver: default
            config:
                - subnet: 10.5.0.0/16
                  ip_range: 10.5.0.0/24
                  gateway: 10.5.0.1
                  aux_addresses:
                      kafka: 10.5.0.2
                      zookeeper: 10.5.0.3

Next, run this Kafka Docker Compose by typing this command.

docker compose -f kafka-docker-compose.yml up -d

We need to update the Kafka connection for each service. Open and edit "application.properties" in the Student Service. Update this line to be like this.

kafka.bootstrap.servers=host.docker.internal:9092

Do the same for "application.properties" in the Lecturer Service and Study Service.

kafka.bootstrap.servers=host.docker.internal:9092

Build and run the Docker container for all services.

cd ../student
mvn clean package -DskipTests
docker build -f src/main/Docker/Dockerfile.jvm -t student .
docker run -i --rm -p 8081:8081 --network=school-network -d student --sysctl net.ipv6.conf.all.disable_ipv6=1
cd ../lecturer
mvn clean package -DskipTests
docker build -f src/main/Docker/Dockerfile.jvm -t lecturer .
docker run -i --rm -p 8082:8082 --network=school-network -d lecturer --sysctl net.ipv6.conf.all.disable_ipv6=1
cd ../study
mvn clean package -DskipTests
docker build -f src/main/Docker/Dockerfile.jvm -t study .
docker run -i --rm -p 8083:8083 --network=school-network -d study --sysctl net.ipv6.conf.all.disable_ipv6=1

Now, we can test the Kafka by running the POST study endpoint of the study service.

curl --location 'http://localhost:8000/api/v1/studies' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
  "studyName": "DSA",
  "studyDescription": "Data Structure and Algorithm",
  "studyDate": "2024-10-22 10:00:00",
  "lidn": "L0001",
  "sidn": [
    "S0001"
  ]
}'

The send Kafka message was sent and consumed, go to http://127.0.0.1:9003/ in the browser.

A Simple Microservices using Quarkus, Kong, and Kafka - kafka admin ui

Go to Topics -> study-activity and you will see messages and consumer data. To make sure data is saved to the Student and Lecturer Database, open the database and show the data on the study-activity table.

A Simple Microservices using Quarkus, Kong, and Kafka - dbeaver

That it's,  a comprehensive step-by-step tutorial on building Microservices using Java, Quarkus, Kafka, PostgreSQL, MongoDB, Docker, and Kong API Gateway. You can get the full source code from our GitHub.

That just the basic. If you need more deep learning about Java and Spring Framework you can take the following cheap course:

Thanks!

Loading…