Build and Deploy Quarkus Microservices on OpenShift: A Step‑by‑Step Guide
This article walks you through creating a Quarkus‑based microservice with REST endpoints, input validation, unit testing, inter‑service communication, health checks, Swagger UI, local execution, and finally deploying the services to OpenShift using S2I and native image builds.
Quarkus is introduced as a next‑generation Kubernetes‑native Java framework built on CDI, JAX‑RS, and Eclipse MicroProfile, offering fast startup, low memory usage, and extensibility such as Hibernate, Kafka, and Vert.x.
Creating the Application
A Maven command using quarkus-maven-plugin with the -Dextensions flag generates a new project. The essential pom.xml snippet defines the Quarkus version, Java 11 source/target, and the required dependencies.
mvn io.quarkus:quarkus-maven-plugin:0.21.1:create \
-DprojectGroupId=pl.piomin.services \
-DprojectArtifactId=employee-service \
-DclassName="pl.piomin.services.employee.controller.EmployeeController" \
-Dpath="/employees" \
-Dextensions="resteasy-jackson,hibernate-validator"The generated pom.xml includes the Quarkus BOM and the Maven plugin configuration.
REST Controller and Entity
The EmployeeController uses JAX‑RS annotations from javax.ws.rs and CDI for injection. It provides CRUD endpoints with logging via SLF4J.
@Path("/employees")
@Produces(MediaType.APPLICATION_JSON)
public class EmployeeController {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeController.class);
@Inject EmployeeRepository repository;
@POST
public Employee add(@Valid Employee employee) {
LOGGER.info("Employee add: {}", employee);
return repository.add(employee);
}
@Path("/{id}")
@GET
public Employee findById(@PathParam("id") Long id) {
LOGGER.info("Employee find: id={}", id);
return repository.findById(id);
}
@GET
public Set<Employee> findAll() {
LOGGER.info("Employee find");
return repository.findAll();
}
// additional methods omitted for brevity
}The Employee entity is annotated with Bean Validation constraints such as @NotNull, @NotBlank, @Min, and @Max.
public class Employee {
private Long id;
@NotNull private Long organizationId;
@NotNull private Long departmentId;
@NotBlank private String name;
@Min(1) @Max(100) private int age;
@NotBlank private String position;
// getters and setters
}Repository
The in‑memory EmployeeRepository is a CDI bean ( @ApplicationScoped) that stores employees in a HashSet and provides basic CRUD operations.
@ApplicationScoped
public class EmployeeRepository {
private Set<Employee> employees = new HashSet<>();
public EmployeeRepository() {
add(new Employee(1L,1L,"John Smith",30,"Developer"));
add(new Employee(1L,1L,"Paul Walker",40,"Architect"));
}
public Employee add(Employee employee) {
employee.setId((long)(employees.size()+1));
employees.add(employee);
return employee;
}
public Employee findById(Long id) {
return employees.stream().filter(e->e.getId().equals(id)).findFirst().orElse(null);
}
public Set<Employee> findAll() { return employees; }
// other query methods omitted
}Unit Testing
Tests use Quarkus JUnit5 support and RestAssured. The pom.xml must include quarkus-junit5 and rest-assured dependencies.
@QuarkusTest
public class OrganizationControllerTests {
@Inject OrganizationRepository repository;
@Test
public void testFindAll() {
given().when().get("/organizations").then().statusCode(200).body(notNullValue());
}
// additional test methods omitted for brevity
}Inter‑Service Communication
Quarkus provides a declarative REST client via MicroProfile. An interface annotated with @RegisterRestClient defines the remote calls.
@Path("/departments")
@RegisterRestClient
public interface DepartmentClient {
@GET
@Path("/organization/{organizationId}")
@Produces(MediaType.APPLICATION_JSON)
List<Department> findByOrganization(@PathParam("organizationId") Long organizationId);
// other methods omitted
}The OrganizationController injects the client with @RestClient and uses it to aggregate data from other services.
@Path("/organizations")
@Produces(MediaType.APPLICATION_JSON)
public class OrganizationController {
@Inject OrganizationRepository repository;
@Inject @RestClient DepartmentClient departmentClient;
@Inject @RestClient EmployeeClient employeeClient;
@Path("/{id}/with-departments")
@GET
public Organization findByIdWithDepartments(@PathParam("id") Long id) {
Organization org = repository.findById(id);
org.setDepartments(departmentClient.findByOrganization(org.getId()));
return org;
}
// other aggregation methods omitted
}Health Checks and OpenAPI
Adding quarkus-smallrye-health and quarkus-smallrye-openapi dependencies enables liveness/readiness endpoints ( /health/live, /health/ready) and Swagger UI. A simple readiness bean implements HealthCheck and is annotated with @Readiness.
@ApplicationScoped
@Readiness
public class ReadinessHealthcheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
return HealthCheckResponse.named("Employee Health Check").up().build();
}
}Setting quarkus.swagger-ui.always-include=true makes the UI available in production.
Running Locally
The services run on different ports (default 8080 for employee-service, custom ports for department and organization) to avoid conflicts. Maven command mvn compile quarkus:dev starts the application with live reload.
Deploying to OpenShift
Minishift is started with sufficient memory and CPU. Quarkus native image builds require JDK 8, so the pom.xml is adjusted accordingly and a native profile adds the GraalVM native image plugin.
<properties>
<quarkus.version>0.21.1</quarkus.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<profiles>
<profile>
<id>native</id>
<activation>
<property><name>native</name></property>
</activation>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals><goal>native-image</goal></goals>
<configuration>
<enableHttpUrlHandler>true</enableHttpUrlHandler>
</configuration>
</execution>
</executions>
</plugin>
<!-- failsafe plugin omitted for brevity -->
</plugins>
</build>
</profile>
</profiles>In application.properties the client URLs are changed to service names instead of localhost:
quarkus.swagger-ui.always-include=true
pl.piomin.services.organization.client.DepartmentClient/mp-rest/url=http://department:8080
pl.piomin.services.organization.client.EmployeeClient/mp-rest/url=http://employee:8080Deployment uses the Quarkus S2I builder image quay.io/quarkus/ubi-quarkus-native-s2i with oc new-app commands for each microservice, specifying the --context-dir for the respective module. Services are exposed via oc expose svc and reachable through a DNS route such as http://${APP_NAME}-myproject.192.168.99.100.nip.io. Health checks can be enabled on OpenShift as well.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
