Organizing Microservices with EJB, Beans, and Testing in Quarkus
In the evolving world of microservices, managing code effectively and maintaining separation of concerns is crucial. As we deepen our understanding of microservices, concepts such as POJO (Plain Old Java Object), EJB (Enterprise Java Beans), and well-structured project architecture become indispensable. In this blog, we’ll explore how to organize your microservices codebase using Beans, EJB concepts, and the Repository pattern. We’ll also touch upon automated testing using REST Assured and Quarkus JUnit5.
Let’s dive into the code structure, including a Student Resource, Student Repository, and Testing using Quarkus. By the end of this blog, you will understand how to organize your microservices application for better scalability and maintainability.
Why POJO and Why Bean?
To begin with, POJO (Plain Old Java Object) refers to simple Java objects that do not follow any special restriction or rule but encapsulate fields and methods. While POJOs are straightforward, when we shift to enterprise applications, we prefer using Beans to encapsulate business logic.
Beans provide a standard, reusable way to handle business logic and lifecycle management. In the context of Quarkus and microservices, using beans helps us organize, inject dependencies, and manage resources efficiently.
Blueprint Class — Student Model
The first step is to define a model for students. This model (or blueprint) will represent the data structure of a Student object. Here’s a simple example:
package com.harsh.tech;
public class Students {
public int id;
public String name;
public int phone;
public String course;
public Students(int id, String name, int phone, String course) {
this.id = id;
this.name = name;
this.phone = phone;
this.course = course;
}
}
This class defines the attributes (id, name, phone, and course) that each student will have. This is similar to the model class we created in the previous blog but separated here for better structure.
Separation of Concerns: Student Repository
To keep the code modular and avoid directly exposing logic in resource classes, we use the Repository Pattern. This separates our data handling logic from our service or resource layer.
Here’s the StudentRepository
class that handles data operations:
package com.harsh.tech;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class StudentRepository {
public String hello() {
return "Hello RestEasy";
}
public List<Students> getAllStudents() {
return List.of(
new Students(1, "Harsh", 11111, "DevOps"),
new Students(2, "Rahul", 22222, "Machine Learning"),
new Students(3, "Ankit", 33333, "Artificial Intelligence"),
new Students(4, "Manan", 44444, "Python Programming")
);
}
public int count() {
return getAllStudents().size();
}
public Optional<Students> getStudent(int id) {
return getAllStudents().stream().filter(Students -> Students.id == id).findFirst();
}
}
- @ApplicationScoped: This annotation marks the class as a bean whose state can be shared across the application. This ensures that the methods in the repository can be used by other components.
- The repository includes methods for retrieving all students, counting the number of students, and finding a student by their ID.
Student Resource — Exposing Endpoints
The StudentResource class is where we expose the repository functions through RESTful endpoints. This class utilizes dependency injection to include the StudentRepository
class.
package com.harsh.tech;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.Optional;
@Path("/main")
public class StudentResource {
@Inject
StudentRepository repository;
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/all")
public List<Students> getAll() {
return repository.getAllStudents();
}
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/count")
public int getSize() {
return repository.count();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/search/{x}")
public Optional<Students> get(@PathParam("x") int id) {
return repository.getStudent(id);
}
}
- @Inject: This injects the
StudentRepository
class into theStudentResource
class. - Endpoints:
/all
- returns the list of all students./count
- returns the total number of students./search/{x}
- allows searching for a student by ID using a Path Paramet
Testing with Quarkus and REST Assured
Testing is essential to ensure that our API endpoints work as expected. For this, we use REST Assured for API testing and Quarkus JUnit5 for running our tests.
Here’s a test case to check if the /count
endpoint returns the correct number of students:
package com.harsh.tech;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.ws.rs.core.MediaType;
import org.apache.http.HttpHeaders;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
class StudentsTest {
@Test
public void shouldCount() {
given()
.header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN)
.when().get("/main/count")
.then()
.statusCode(200)
.body(is("4"));
}
}
Run the test case using :
mvn test
- @QuarkusTest: This annotation enables the integration testing support provided by Quarkus.
- given(): This method sets up a request with the required headers and endpoints.
- when().get(): Executes the HTTP GET request.
- .then(): Defines the assertions for the response, such as verifying that the status code is 200 (OK) and the body contains the expected value (
4
in this case).
By writing such tests, we can ensure that our RESTful APIs are functioning correctly and returning the expected results.
Conclusion
As microservice-based applications grow in complexity, managing the structure and flow of your code becomes crucial. By using Beans to encapsulate business logic, separating concerns through the Repository Pattern, and thoroughly testing APIs using tools like REST Assured, we can maintain a clean, scalable, and testable codebase.
In this blog, we organized the code into three main components:
- Model Class: Defines the blueprint for student objects.
- Repository Class: Handles data operations and business logic.
- Resource Class: Exposes REST endpoints for client interaction.
We also demonstrated how to test these endpoints using Quarkus and REST Assured, ensuring the application is reliable and works as expected.
The tools and patterns covered in this blog will help you build robust and well-organized microservices in Java. Whether you’re building APIs, handling business logic, or writing tests, keeping your microservices modular and maintainable is key to long-term success.