├── organization-service ├── src │ └── main │ │ ├── resources │ │ ├── import.sql │ │ └── application.properties │ │ └── java │ │ └── pl │ │ └── piomin │ │ └── services │ │ └── quarkus │ │ └── organization │ │ ├── repository │ │ └── OrganizationRepository.java │ │ ├── client │ │ ├── EmployeeClient.java │ │ └── DepartmentClient.java │ │ ├── model │ │ ├── Department.java │ │ ├── Employee.java │ │ └── Organization.java │ │ ├── config │ │ └── OrganizationBeanProducer.java │ │ ├── lifecycle │ │ └── OrganizationLifecycle.java │ │ └── resource │ │ └── OrganizationResource.java └── pom.xml ├── .gitignore ├── department-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── import.sql │ │ │ └── application.properties │ │ └── java │ │ │ └── pl │ │ │ └── piomin │ │ │ └── services │ │ │ └── quarkus │ │ │ └── department │ │ │ ├── repository │ │ │ └── DepartmentRepository.java │ │ │ ├── client │ │ │ └── EmployeeClient.java │ │ │ ├── config │ │ │ └── DepartmentBeanProducer.java │ │ │ ├── model │ │ │ ├── Employee.java │ │ │ └── Department.java │ │ │ ├── lifecycle │ │ │ └── DepartmentLifecycle.java │ │ │ └── resource │ │ │ └── DepartmentResource.java │ └── test │ │ └── java │ │ └── pl │ │ └── piomin │ │ └── services │ │ └── quarkus │ │ └── department │ │ ├── DisableExternalProfile.java │ │ ├── ConsulResource.java │ │ ├── DepartmentResourceTests.java │ │ └── DepartmentResourceConsulTests.java └── pom.xml ├── docker-compose.yml ├── renovate.json ├── employee-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── import.sql │ │ │ └── application.properties │ │ └── java │ │ │ └── pl │ │ │ └── piomin │ │ │ └── services │ │ │ └── quarkus │ │ │ └── employee │ │ │ ├── repository │ │ │ └── EmployeeRepository.java │ │ │ ├── config │ │ │ └── EmployeeBeanProducer.java │ │ │ ├── resource │ │ │ └── EmployeeResource.java │ │ │ ├── lifecycle │ │ │ └── EmployeeLifecycle.java │ │ │ └── model │ │ │ └── Employee.java │ └── test │ │ └── java │ │ └── pl │ │ └── piomin │ │ └── services │ │ └── quarkus │ │ └── employee │ │ ├── DisableExternalProfile.java │ │ └── EmployeeResourceTests.java └── pom.xml ├── gateway-service ├── src │ └── main │ │ ├── java │ │ └── pl │ │ │ └── piomin │ │ │ └── services │ │ │ └── gateway │ │ │ └── GatewayApplication.java │ │ └── resources │ │ └── application.yml └── pom.xml ├── .circleci └── config.yml ├── readme.md └── pom.xml /organization-service/src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO organization (name) VALUES ('Test1'); 2 | INSERT INTO organization (name) VALUES ('Test2'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /department-service/target/ 3 | /employee-service/target/ 4 | /gateway-service/target/ 5 | /organization-service/target/ -------------------------------------------------------------------------------- /department-service/src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO department (name, organizationId) VALUES ('Test1', 1); 2 | INSERT INTO department (name, organizationId) VALUES ('Test2', 1); 3 | INSERT INTO department (name, organizationId) VALUES ('Test3', 1); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | consul: 3 | image: hashicorp/consul:1.22 4 | environment: 5 | - CONSUL_BIND_INTERFACE=eth0 6 | volumes: 7 | - consul_data:/consul/data 8 | ports: 9 | - "8500:8500" 10 | - "8600:8600/udp" 11 | restart: always 12 | volumes: 13 | consul_data: -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base",":dependencyDashboard" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | } 11 | ], 12 | "prCreation": "not-pending" 13 | } -------------------------------------------------------------------------------- /employee-service/src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO employee (name, age, position, departmentId, organizationId) 2 | VALUES ('Test1', 10, 'Developer', 1, 1); 3 | INSERT INTO employee (name, age, position, departmentId, organizationId) 4 | VALUES ('Test2', 20, 'Architect', 1, 1); 5 | INSERT INTO employee (name, age, position, departmentId, organizationId) 6 | VALUES ('Test3', 30, 'Manager', 1, 1); -------------------------------------------------------------------------------- /gateway-service/src/main/java/pl/piomin/services/gateway/GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class GatewayApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(GatewayApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/repository/OrganizationRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.repository; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | 5 | import io.quarkus.hibernate.orm.panache.PanacheRepository; 6 | import pl.piomin.services.quarkus.organization.model.Organization; 7 | 8 | @ApplicationScoped 9 | public class OrganizationRepository implements PanacheRepository { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /employee-service/src/test/java/pl/piomin/services/quarkus/employee/DisableExternalProfile.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.employee; 2 | 3 | import io.quarkus.test.junit.QuarkusTestProfile; 4 | 5 | import java.util.Map; 6 | 7 | 8 | public class DisableExternalProfile implements QuarkusTestProfile { 9 | 10 | @Override 11 | public Map getConfigOverrides() { 12 | return Map.of( 13 | "quarkus.consul-config.enabled", "false", 14 | "quarkus.datasource.db-kind", "h2", 15 | "quarkus.hibernate-orm.database.generation", "drop-and-create"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /department-service/src/main/java/pl/piomin/services/quarkus/department/repository/DepartmentRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department.repository; 2 | 3 | import java.util.List; 4 | 5 | import jakarta.enterprise.context.ApplicationScoped; 6 | 7 | import io.quarkus.hibernate.orm.panache.PanacheRepository; 8 | import pl.piomin.services.quarkus.department.model.Department; 9 | 10 | @ApplicationScoped 11 | public class DepartmentRepository implements PanacheRepository { 12 | 13 | public List findByOrganization(Long organizationId){ 14 | return find("organizationId", organizationId).list(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /department-service/src/test/java/pl/piomin/services/quarkus/department/DisableExternalProfile.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department; 2 | 3 | import io.quarkus.test.junit.QuarkusTestProfile; 4 | 5 | import java.util.Map; 6 | 7 | 8 | public class DisableExternalProfile implements QuarkusTestProfile { 9 | 10 | @Override 11 | public Map getConfigOverrides() { 12 | return Map.of( 13 | "quarkus.consul-config.enabled", "false", 14 | "department.name", "abc", 15 | "quarkus.datasource.db-kind", "h2", 16 | "quarkus.hibernate-orm.database.generation", "drop-and-create"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /organization-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | quarkus.application.name = organization-service 2 | quarkus.application.version = 1.1 3 | quarkus.consul-config.enabled = true 4 | quarkus.consul-config.properties-value-keys = config/${quarkus.application.name} 5 | 6 | ## Exported to the Consul Key-Value Store 7 | #quarkus.http.port = 0 8 | #quarkus.datasource.db-kind = h2 9 | #quarkus.hibernate-orm.database.generation = drop-and-create 10 | #quarkus.hibernate-orm.sql-load-script = src/main/resources/import.sql 11 | #quarkus.stork.employee-service.service-discovery.type = consul 12 | #quarkus.stork.department-service.service-discovery.type = consul 13 | #quarkus.stork.organization-service.service-registrar.type = consul -------------------------------------------------------------------------------- /employee-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | quarkus.application.name = employee-service 2 | quarkus.application.version = 1.1 3 | quarkus.consul-config.enabled = true 4 | quarkus.consul-config.properties-value-keys = config/${quarkus.application.name} 5 | 6 | ## Exported to the Consul Key-Value Store 7 | #quarkus.http.port = 0 8 | #quarkus.datasource.db-kind = h2 9 | #quarkus.hibernate-orm.database.generation = drop-and-create 10 | #quarkus.hibernate-orm.sql-load-script = src/main/resources/import.sql 11 | #quarkus.stork.employee-service.service-registrar.type = consul 12 | 13 | #%test.quarkus.datasource.jdbc.url = jdbc:h2:mem:testdb 14 | %test.quarkus.datasource.db-kind = h2 15 | %test.quarkus.hibernate-orm.database.generation = drop-and-create -------------------------------------------------------------------------------- /department-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | quarkus.application.name = department-service 2 | quarkus.application.version = 1.1 3 | quarkus.consul-config.enabled = true 4 | quarkus.consul-config.properties-value-keys = config/${quarkus.application.name} 5 | 6 | ## Exported to the Consul Key-Value Store 7 | #quarkus.http.port = 0 8 | #quarkus.datasource.db-kind = h2 9 | #quarkus.hibernate-orm.database.generation = drop-and-create 10 | #quarkus.hibernate-orm.sql-load-script = src/main/resources/import.sql 11 | #quarkus.stork.employee-service.service-discovery.type = consul 12 | #quarkus.stork.department-service.service-registrar.type = consul 13 | 14 | %test.quarkus.datasource.jdbc.url = jdbc:h2:mem:testdb 15 | #%test.quarkus.consul-config.enabled = false -------------------------------------------------------------------------------- /employee-service/src/main/java/pl/piomin/services/quarkus/employee/repository/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.employee.repository; 2 | 3 | import java.util.List; 4 | 5 | import jakarta.enterprise.context.ApplicationScoped; 6 | 7 | import io.quarkus.hibernate.orm.panache.PanacheRepository; 8 | import pl.piomin.services.quarkus.employee.model.Employee; 9 | 10 | @ApplicationScoped 11 | public class EmployeeRepository implements PanacheRepository { 12 | 13 | public List findByDepartment(Long departmentId){ 14 | return find("departmentId", departmentId).list(); 15 | } 16 | 17 | public List findByOrganization(Long organizationId){ 18 | return find("organizationId", organizationId).list(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /gateway-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: gateway-service 4 | cloud: 5 | gateway: 6 | discovery: 7 | locator: 8 | enabled: true 9 | routes: 10 | - id: employee-service 11 | uri: lb://employee-service 12 | predicates: 13 | - Path=/api/employees/** 14 | filters: 15 | - StripPrefix=1 16 | - id: department-service 17 | uri: lb://department-service 18 | predicates: 19 | - Path=/api/departments/** 20 | filters: 21 | - StripPrefix=1 22 | - id: organization-service 23 | uri: lb://organization-service 24 | predicates: 25 | - Path=/api/organizations/** 26 | filters: 27 | - StripPrefix=1 -------------------------------------------------------------------------------- /department-service/src/main/java/pl/piomin/services/quarkus/department/client/EmployeeClient.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department.client; 2 | 3 | import java.util.List; 4 | 5 | import jakarta.ws.rs.GET; 6 | import jakarta.ws.rs.Path; 7 | import jakarta.ws.rs.PathParam; 8 | import jakarta.ws.rs.Produces; 9 | import jakarta.ws.rs.core.MediaType; 10 | 11 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 12 | import pl.piomin.services.quarkus.department.model.Employee; 13 | 14 | @Path("/employees") 15 | @RegisterRestClient(baseUri = "stork://employee-service") 16 | public interface EmployeeClient { 17 | 18 | @GET 19 | @Path("/department/{departmentId}") 20 | @Produces(MediaType.APPLICATION_JSON) 21 | List findByDepartment(@PathParam("departmentId") Long departmentId); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/client/EmployeeClient.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.client; 2 | 3 | import jakarta.ws.rs.GET; 4 | import jakarta.ws.rs.Path; 5 | import jakarta.ws.rs.PathParam; 6 | import jakarta.ws.rs.Produces; 7 | import jakarta.ws.rs.core.MediaType; 8 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 9 | import pl.piomin.services.quarkus.organization.model.Employee; 10 | 11 | import java.util.List; 12 | 13 | @Path("/employees") 14 | @RegisterRestClient(baseUri = "stork://employee-service") 15 | public interface EmployeeClient { 16 | 17 | @GET 18 | @Path("/organization/{organizationId}") 19 | @Produces(MediaType.APPLICATION_JSON) 20 | List findByOrganization(@PathParam("organizationId") Long organizationId); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | jobs: 4 | analyze: 5 | docker: 6 | - image: 'cimg/openjdk:25.0' 7 | steps: 8 | - checkout 9 | - run: 10 | name: Analyze on SonarCloud 11 | command: mvn verify sonar:sonar -DskipTests 12 | test: 13 | executor: machine_executor_amd64 14 | steps: 15 | - checkout 16 | - run: 17 | name: Maven Tests 18 | command: mvn test 19 | 20 | orbs: 21 | maven: circleci/maven@2.1.1 22 | 23 | executors: 24 | jdk: 25 | docker: 26 | - image: 'cimg/openjdk:25.0' 27 | machine_executor_amd64: 28 | machine: 29 | image: ubuntu-2204:current 30 | environment: 31 | architecture: "amd64" 32 | platform: "linux/amd64" 33 | 34 | workflows: 35 | maven_test: 36 | jobs: 37 | - test 38 | - analyze: 39 | context: SonarCloud -------------------------------------------------------------------------------- /employee-service/src/main/java/pl/piomin/services/quarkus/employee/config/EmployeeBeanProducer.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.employee.config; 2 | 3 | import io.quarkus.arc.lookup.LookupIfProperty; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.ext.consul.ConsulClient; 6 | import io.vertx.ext.consul.ConsulClientOptions; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.inject.Produces; 9 | import org.eclipse.microprofile.config.inject.ConfigProperty; 10 | 11 | @ApplicationScoped 12 | public class EmployeeBeanProducer { 13 | 14 | @ConfigProperty(name = "consul.host", defaultValue = "localhost") String host; 15 | @ConfigProperty(name = "consul.port", defaultValue = "8500") int port; 16 | 17 | @Produces 18 | @LookupIfProperty(name = "quarkus.stork.employee-service.service-registrar.type", stringValue = "consul") 19 | public ConsulClient consulClient(Vertx vertx) { 20 | return ConsulClient.create(vertx, new ConsulClientOptions() 21 | .setHost(host) 22 | .setPort(port)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /department-service/src/main/java/pl/piomin/services/quarkus/department/config/DepartmentBeanProducer.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department.config; 2 | 3 | import io.quarkus.arc.lookup.LookupIfProperty; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.ext.consul.ConsulClient; 6 | import io.vertx.ext.consul.ConsulClientOptions; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.inject.Produces; 9 | import org.eclipse.microprofile.config.inject.ConfigProperty; 10 | 11 | @ApplicationScoped 12 | public class DepartmentBeanProducer { 13 | 14 | @ConfigProperty(name = "consul.host", defaultValue = "localhost") String host; 15 | @ConfigProperty(name = "consul.port", defaultValue = "8500") int port; 16 | 17 | @Produces 18 | @LookupIfProperty(name = "quarkus.stork.department-service.service-registrar.type", stringValue = "consul") 19 | public ConsulClient consulClient(Vertx vertx) { 20 | return ConsulClient.create(vertx, new ConsulClientOptions() 21 | .setHost(host) 22 | .setPort(port)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/client/DepartmentClient.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.client; 2 | 3 | import java.util.List; 4 | 5 | import jakarta.inject.Singleton; 6 | import jakarta.ws.rs.GET; 7 | import jakarta.ws.rs.Path; 8 | import jakarta.ws.rs.PathParam; 9 | import jakarta.ws.rs.Produces; 10 | import jakarta.ws.rs.core.MediaType; 11 | 12 | import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; 13 | import pl.piomin.services.quarkus.organization.model.Department; 14 | 15 | @Path("/departments") 16 | @RegisterRestClient(baseUri = "stork://department-service") 17 | public interface DepartmentClient { 18 | 19 | @GET 20 | @Path("/organization/{organizationId}") 21 | @Produces(MediaType.APPLICATION_JSON) 22 | List findByOrganization(@PathParam("organizationId") Long organizationId); 23 | 24 | @GET 25 | @Path("/organization/{organizationId}/with-employees") 26 | @Produces(MediaType.APPLICATION_JSON) 27 | List findByOrganizationWithEmployees(@PathParam("organizationId") Long organizationId); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/model/Department.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @JsonIgnoreProperties(ignoreUnknown = true) 9 | public class Department { 10 | 11 | private Long id; 12 | private String name; 13 | private List employees = new ArrayList<>(); 14 | 15 | public Department() { 16 | } 17 | 18 | public Department(Long id, String name, List employees) { 19 | this.id = id; 20 | this.name = name; 21 | this.employees = employees; 22 | } 23 | 24 | public Long getId() { 25 | return id; 26 | } 27 | 28 | public void setId(Long id) { 29 | this.id = id; 30 | } 31 | 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | public void setName(String name) { 37 | this.name = name; 38 | } 39 | 40 | public List getEmployees() { 41 | return employees; 42 | } 43 | 44 | public void setEmployees(List employees) { 45 | this.employees = employees; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "Department [id=" + id + ", name=" + name + ", employees=" + employees + "]"; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /department-service/src/main/java/pl/piomin/services/quarkus/department/model/Employee.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class Employee { 7 | 8 | private Long id; 9 | private String name; 10 | private int age; 11 | private String position; 12 | 13 | public Employee() { 14 | } 15 | 16 | public Employee(Long id, String name, int age, String position) { 17 | this.id = id; 18 | this.name = name; 19 | this.age = age; 20 | this.position = position; 21 | } 22 | 23 | public Long getId() { 24 | return id; 25 | } 26 | 27 | public void setId(Long id) { 28 | this.id = id; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public void setName(String name) { 36 | this.name = name; 37 | } 38 | 39 | public int getAge() { 40 | return age; 41 | } 42 | 43 | public void setAge(int age) { 44 | this.age = age; 45 | } 46 | 47 | public String getPosition() { 48 | return position; 49 | } 50 | 51 | public void setPosition(String position) { 52 | this.position = position; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return "Employee [id=" + id + ", name=" + name + ", age=" + age + ", position=" + position + "]"; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/model/Employee.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class Employee { 7 | 8 | private Long id; 9 | private String name; 10 | private int age; 11 | private String position; 12 | 13 | public Employee() { 14 | } 15 | 16 | public Employee(Long id, String name, int age, String position) { 17 | this.id = id; 18 | this.name = name; 19 | this.age = age; 20 | this.position = position; 21 | } 22 | 23 | public Long getId() { 24 | return id; 25 | } 26 | 27 | public void setId(Long id) { 28 | this.id = id; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public void setName(String name) { 36 | this.name = name; 37 | } 38 | 39 | public int getAge() { 40 | return age; 41 | } 42 | 43 | public void setAge(int age) { 44 | this.age = age; 45 | } 46 | 47 | public String getPosition() { 48 | return position; 49 | } 50 | 51 | public void setPosition(String position) { 52 | this.position = position; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return "Employee [id=" + id + ", name=" + name + ", age=" + age + ", position=" + position + "]"; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /department-service/src/test/java/pl/piomin/services/quarkus/department/ConsulResource.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department; 2 | 3 | import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; 4 | import org.testcontainers.consul.ConsulContainer; 5 | import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; 6 | 7 | import java.util.Map; 8 | 9 | public class ConsulResource implements QuarkusTestResourceLifecycleManager { 10 | 11 | private ConsulContainer consulContainer; 12 | 13 | @Override 14 | public Map start() { 15 | consulContainer = new ConsulContainer("hashicorp/consul:latest") 16 | .withConsulCommand( 17 | """ 18 | kv put config/department-service - < employees = new ArrayList<>(); 22 | 23 | public Department() { 24 | } 25 | 26 | public Department(Long id, Long organizationId, String name) { 27 | this.id = id; 28 | this.organizationId = organizationId; 29 | this.name = name; 30 | } 31 | 32 | public Long getId() { 33 | return id; 34 | } 35 | 36 | public void setId(Long id) { 37 | this.id = id; 38 | } 39 | 40 | public Long getOrganizationId() { 41 | return organizationId; 42 | } 43 | 44 | public void setOrganizationId(Long organizationId) { 45 | this.organizationId = organizationId; 46 | } 47 | 48 | public String getName() { 49 | return name; 50 | } 51 | 52 | public void setName(String name) { 53 | this.name = name; 54 | } 55 | 56 | public List getEmployees() { 57 | return employees; 58 | } 59 | 60 | public void setEmployees(List employees) { 61 | this.employees = employees; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "Department [id=" + id + ", organizationId=" + organizationId + ", name=" + name + "]"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/model/Organization.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.model; 2 | 3 | import jakarta.persistence.*; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | @Entity 10 | public class Organization { 11 | 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | private Long id; 15 | @NotBlank 16 | private String name; 17 | @NotBlank 18 | private String address; 19 | @Transient 20 | private List departments = new ArrayList<>(); 21 | @Transient 22 | private List employees = new ArrayList<>(); 23 | 24 | public Long getId() { 25 | return id; 26 | } 27 | 28 | public void setId(Long id) { 29 | this.id = id; 30 | } 31 | 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | public void setName(String name) { 37 | this.name = name; 38 | } 39 | 40 | public String getAddress() { 41 | return address; 42 | } 43 | 44 | public void setAddress(String address) { 45 | this.address = address; 46 | } 47 | 48 | public List getDepartments() { 49 | return departments; 50 | } 51 | 52 | public void setDepartments(List departments) { 53 | this.departments = departments; 54 | } 55 | 56 | public List getEmployees() { 57 | return employees; 58 | } 59 | 60 | public void setEmployees(List employees) { 61 | this.employees = employees; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "Organization [id=" + id + ", name=" + name + ", address=" + address + ", departments=" + departments + ", employees=" + employees + "]"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/config/OrganizationBeanProducer.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.config; 2 | 3 | import io.quarkus.arc.lookup.LookupIfProperty; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.ext.consul.ConsulClient; 6 | import io.vertx.ext.consul.ConsulClientOptions; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.inject.Produces; 9 | import org.eclipse.microprofile.config.inject.ConfigProperty; 10 | 11 | @ApplicationScoped 12 | public class OrganizationBeanProducer { 13 | 14 | @ConfigProperty(name = "consul.host", defaultValue = "localhost") String host; 15 | @ConfigProperty(name = "consul.port", defaultValue = "8500") int port; 16 | 17 | @Produces 18 | @LookupIfProperty(name = "quarkus.stork.organization-service.service-registrar.type", stringValue = "consul") 19 | public ConsulClient consulClient(Vertx vertx) { 20 | return ConsulClient.create(vertx, new ConsulClientOptions() 21 | .setHost(host) 22 | .setPort(port)); 23 | } 24 | 25 | // @Produces 26 | // Consul consulClient = Consul.builder().build(); 27 | // 28 | // @Produces 29 | // LoadBalancedFilter employeeFilter = new LoadBalancedFilter(consulClient); 30 | // 31 | // @Produces 32 | // LoadBalancedFilter departmentFilter = new LoadBalancedFilter(consulClient); 33 | // 34 | // @Produces 35 | // public EmployeeClient employeeClient() throws URISyntaxException { 36 | // URIBuilder builder = new URIBuilder("http://employee"); 37 | // return RestClientBuilder.newBuilder() 38 | // .baseUri(builder.build()) 39 | // .register(employeeFilter) 40 | // .build(EmployeeClient.class); 41 | // } 42 | // 43 | // @Produces 44 | // public DepartmentClient departmentClient() throws URISyntaxException { 45 | // URIBuilder builder = new URIBuilder("http://department"); 46 | // return RestClientBuilder.newBuilder() 47 | // .baseUri(builder.build()) 48 | // .register(departmentFilter) 49 | // .build(DepartmentClient.class); 50 | // } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /employee-service/src/test/java/pl/piomin/services/quarkus/employee/EmployeeResourceTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.employee; 2 | 3 | import io.quarkus.test.junit.QuarkusTest; 4 | import io.quarkus.test.junit.TestProfile; 5 | import io.restassured.http.ContentType; 6 | import org.junit.jupiter.api.MethodOrderer; 7 | import org.junit.jupiter.api.Order; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.TestMethodOrder; 10 | import pl.piomin.services.quarkus.employee.model.Employee; 11 | 12 | import static io.restassured.RestAssured.given; 13 | import static io.restassured.RestAssured.when; 14 | import static org.hamcrest.Matchers.is; 15 | import static org.hamcrest.Matchers.notNullValue; 16 | 17 | @QuarkusTest 18 | @TestProfile(DisableExternalProfile.class) 19 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 20 | public class EmployeeResourceTests { 21 | 22 | @Test 23 | @Order(1) 24 | void add() { 25 | Employee e = new Employee(); 26 | e.setName("Test"); 27 | e.setAge(40); 28 | e.setDepartmentId(1L); 29 | e.setOrganizationId(1L); 30 | e.setPosition("developer"); 31 | 32 | given().body(e).contentType(ContentType.JSON) 33 | .when().post("/employees").then() 34 | .statusCode(200) 35 | .body("id", notNullValue()); 36 | } 37 | 38 | @Test 39 | @Order(2) 40 | void findAll() { 41 | when().get("/employees").then() 42 | .statusCode(200) 43 | .body("size()", is(4)); 44 | } 45 | 46 | @Test 47 | @Order(2) 48 | void findById() { 49 | when().get("/employees/{id}", 1).then() 50 | .statusCode(200) 51 | .body("id", is(1)); 52 | } 53 | 54 | @Test 55 | @Order(2) 56 | void findByDepartmentId() { 57 | when().get("/employees/department/{departmentId}", 1).then() 58 | .statusCode(200) 59 | .body("size()", is(4)); 60 | } 61 | 62 | @Test 63 | @Order(2) 64 | void findByDepartmentIdNotFound() { 65 | when().get("/employees/department/{departmentId}", 100).then() 66 | .statusCode(200) 67 | .body("size()", is(0)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /department-service/src/test/java/pl/piomin/services/quarkus/department/DepartmentResourceTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department; 2 | 3 | import io.quarkus.test.junit.QuarkusTest; 4 | import io.quarkus.test.junit.TestProfile; 5 | import io.restassured.http.ContentType; 6 | import org.instancio.Instancio; 7 | import org.instancio.Select; 8 | import org.junit.jupiter.api.MethodOrderer; 9 | import org.junit.jupiter.api.Order; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.TestMethodOrder; 12 | import pl.piomin.services.quarkus.department.model.Department; 13 | 14 | import static io.restassured.RestAssured.given; 15 | import static io.restassured.RestAssured.when; 16 | import static org.hamcrest.Matchers.is; 17 | import static org.hamcrest.Matchers.notNullValue; 18 | 19 | @QuarkusTest 20 | @TestProfile(DisableExternalProfile.class) 21 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 22 | public class DepartmentResourceTests { 23 | 24 | @Test 25 | @Order(1) 26 | void add() { 27 | Department d = new Department(); 28 | d.setName("test"); 29 | d.setOrganizationId(1L); 30 | 31 | given().body(d).contentType(ContentType.JSON) 32 | .when().post("/departments").then() 33 | .statusCode(200) 34 | .body("id", notNullValue()); 35 | } 36 | 37 | @Test 38 | @Order(2) 39 | void findAll() { 40 | when().get("/departments").then() 41 | .statusCode(200) 42 | .body("size()", is(4)); 43 | } 44 | 45 | @Test 46 | @Order(2) 47 | void findById() { 48 | when().get("/departments/{id}", 1).then() 49 | .statusCode(200) 50 | .body("id", is(1)); 51 | } 52 | 53 | @Test 54 | @Order(2) 55 | void findByOrganizationId() { 56 | when().get("/departments/organization/{organizationId}", 1).then() 57 | .statusCode(200) 58 | .body("size()", is(4)); 59 | } 60 | 61 | @Test 62 | @Order(2) 63 | void findByOrganizationIdNotFound() { 64 | when().get("/departments/organization/{organizationId}", 100).then() 65 | .statusCode(200) 66 | .body("size()", is(0)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /employee-service/src/main/java/pl/piomin/services/quarkus/employee/resource/EmployeeResource.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.employee.resource; 2 | 3 | import java.util.List; 4 | 5 | import jakarta.transaction.Transactional; 6 | import jakarta.validation.Valid; 7 | import jakarta.ws.rs.GET; 8 | import jakarta.ws.rs.POST; 9 | import jakarta.ws.rs.Path; 10 | import jakarta.ws.rs.PathParam; 11 | import jakarta.ws.rs.Produces; 12 | import jakarta.ws.rs.core.MediaType; 13 | 14 | import org.jboss.logging.Logger; 15 | import pl.piomin.services.quarkus.employee.model.Employee; 16 | import pl.piomin.services.quarkus.employee.repository.EmployeeRepository; 17 | 18 | @Path("/employees") 19 | @Produces(MediaType.APPLICATION_JSON) 20 | public class EmployeeResource { 21 | 22 | private Logger logger; 23 | private EmployeeRepository repository; 24 | 25 | public EmployeeResource(Logger logger, 26 | EmployeeRepository repository) { 27 | this.logger = logger; 28 | this.repository = repository; 29 | } 30 | 31 | @POST 32 | @Transactional 33 | public Employee add(@Valid Employee employee) { 34 | logger.infof("Employee add: %s", employee); 35 | repository.persist(employee); 36 | return employee; 37 | } 38 | 39 | @Path("/{id}") 40 | @GET 41 | public Employee findById(@PathParam("id") Long id) { 42 | logger.infof("Employee find: id=%s", id); 43 | return repository.findById(id); 44 | } 45 | 46 | @GET 47 | public List findAll() { 48 | logger.infof("Employee find"); 49 | return repository.findAll().list(); 50 | } 51 | 52 | @Path("/department/{departmentId}") 53 | @GET 54 | public List findByDepartment(@PathParam("departmentId") Long departmentId) { 55 | logger.infof("Employee find: departmentId=%s", departmentId); 56 | return repository.findByDepartment(departmentId); 57 | } 58 | 59 | @Path("/organization/{organizationId}") 60 | @GET 61 | public List findByOrganization(@PathParam("organizationId") Long organizationId) { 62 | logger.infof("Employee find: organizationId=%s", organizationId); 63 | return repository.findByOrganization(organizationId); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /employee-service/src/main/java/pl/piomin/services/quarkus/employee/lifecycle/EmployeeLifecycle.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.employee.lifecycle; 2 | 3 | import io.quarkus.runtime.ShutdownEvent; 4 | import io.quarkus.runtime.StartupEvent; 5 | import io.vertx.ext.consul.ConsulClient; 6 | import io.vertx.ext.consul.ServiceOptions; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.event.Observes; 9 | import jakarta.enterprise.inject.Instance; 10 | import org.eclipse.microprofile.config.ConfigProvider; 11 | import org.eclipse.microprofile.config.inject.ConfigProperty; 12 | import org.jboss.logging.Logger; 13 | 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | @ApplicationScoped 18 | public class EmployeeLifecycle { 19 | 20 | @ConfigProperty(name = "quarkus.application.name") 21 | private String appName; 22 | private int port; 23 | 24 | private Logger logger; 25 | private Instance consulClient; 26 | private ScheduledExecutorService executor; 27 | 28 | public EmployeeLifecycle(Logger logger, 29 | Instance consulClient, 30 | ScheduledExecutorService executor) { 31 | this.logger = logger; 32 | this.consulClient = consulClient; 33 | this.executor = executor; 34 | } 35 | 36 | void onStart(@Observes StartupEvent ev) { 37 | if (consulClient.isResolvable()) { 38 | executor.schedule(() -> { 39 | port = ConfigProvider.getConfig().getValue("quarkus.http.port", Integer.class); 40 | consulClient.get().registerService(new ServiceOptions() 41 | .setPort(port) 42 | .setAddress("localhost") 43 | .setName(appName) 44 | .setId(appName + "-" + port), 45 | result -> logger.infof("Service %s-%d registered", appName, port)); 46 | }, 3000, TimeUnit.MILLISECONDS); 47 | } 48 | } 49 | 50 | void onStop(@Observes ShutdownEvent ev) { 51 | if (consulClient.isResolvable()) { 52 | consulClient.get().deregisterService(appName + "-" + port, 53 | result -> logger.infof("Service %s-%d deregistered", appName, port)); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /department-service/src/main/java/pl/piomin/services/quarkus/department/lifecycle/DepartmentLifecycle.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department.lifecycle; 2 | 3 | import io.quarkus.runtime.ShutdownEvent; 4 | import io.quarkus.runtime.StartupEvent; 5 | import io.vertx.ext.consul.ConsulClient; 6 | import io.vertx.ext.consul.ServiceOptions; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.event.Observes; 9 | import jakarta.enterprise.inject.Instance; 10 | import org.eclipse.microprofile.config.ConfigProvider; 11 | import org.eclipse.microprofile.config.inject.ConfigProperty; 12 | import org.jboss.logging.Logger; 13 | 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | @ApplicationScoped 18 | public class DepartmentLifecycle { 19 | 20 | @ConfigProperty(name = "quarkus.application.name") 21 | private String appName; 22 | private int port; 23 | 24 | private Logger logger; 25 | private Instance consulClient; 26 | private ScheduledExecutorService executor; 27 | 28 | public DepartmentLifecycle(Logger logger, 29 | Instance consulClient, 30 | ScheduledExecutorService executor) { 31 | this.logger = logger; 32 | this.consulClient = consulClient; 33 | this.executor = executor; 34 | } 35 | 36 | void onStart(@Observes StartupEvent ev) { 37 | if (consulClient.isResolvable()) { 38 | executor.schedule(() -> { 39 | port = ConfigProvider.getConfig().getValue("quarkus.http.port", Integer.class); 40 | consulClient.get().registerService(new ServiceOptions() 41 | .setPort(port) 42 | .setAddress("localhost") 43 | .setName(appName) 44 | .setId(appName + "-" + port), 45 | result -> logger.infof("Service %s-%d registered", appName, port)); 46 | }, 3000, TimeUnit.MILLISECONDS); 47 | } 48 | } 49 | 50 | void onStop(@Observes ShutdownEvent ev) { 51 | if (consulClient.isResolvable()) { 52 | consulClient.get().deregisterService(appName + "-" + port, 53 | result -> logger.infof("Service %s-%d deregistered", appName, port)); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/lifecycle/OrganizationLifecycle.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.lifecycle; 2 | 3 | import io.quarkus.runtime.ShutdownEvent; 4 | import io.quarkus.runtime.StartupEvent; 5 | import io.vertx.ext.consul.ConsulClient; 6 | import io.vertx.ext.consul.ServiceOptions; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.event.Observes; 9 | import jakarta.enterprise.inject.Instance; 10 | import org.eclipse.microprofile.config.ConfigProvider; 11 | import org.eclipse.microprofile.config.inject.ConfigProperty; 12 | import org.jboss.logging.Logger; 13 | 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | @ApplicationScoped 18 | public class OrganizationLifecycle { 19 | 20 | @ConfigProperty(name = "quarkus.application.name") 21 | private String appName; 22 | private int port; 23 | 24 | private Logger logger; 25 | private Instance consulClient; 26 | private ScheduledExecutorService executor; 27 | 28 | public OrganizationLifecycle(Logger logger, 29 | Instance consulClient, 30 | ScheduledExecutorService executor) { 31 | this.logger = logger; 32 | this.consulClient = consulClient; 33 | this.executor = executor; 34 | } 35 | 36 | void onStart(@Observes StartupEvent ev) { 37 | if (consulClient.isResolvable()) { 38 | executor.schedule(() -> { 39 | port = ConfigProvider.getConfig().getValue("quarkus.http.port", Integer.class); 40 | consulClient.get().registerService(new ServiceOptions() 41 | .setPort(port) 42 | .setAddress("localhost") 43 | .setName(appName) 44 | .setId(appName + "-" + port), 45 | result -> logger.infof("Service %s-%d registered", appName, port)); 46 | }, 3000, TimeUnit.MILLISECONDS); 47 | } 48 | } 49 | 50 | void onStop(@Observes ShutdownEvent ev) { 51 | if (consulClient.isResolvable()) { 52 | consulClient.get().deregisterService(appName + "-" + port, 53 | result -> logger.infof("Service %s-%d deregistered", appName, port)); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /gateway-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 4.0.1 10 | 11 | 12 | pl.piomin.samples 13 | gateway-service 14 | 1.0 15 | 16 | 17 | 21 18 | 2025.1.0 19 | 20 | 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter-gateway-server-webflux 25 | 26 | 27 | org.springframework.cloud 28 | spring-cloud-starter-loadbalancer 29 | 30 | 31 | org.springframework.cloud 32 | spring-cloud-starter-consul-discovery 33 | 34 | 35 | 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-maven-plugin 41 | 42 | 43 | 44 | build-info 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.springframework.cloud 56 | spring-cloud-dependencies 57 | ${spring-cloud.version} 58 | pom 59 | import 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /employee-service/src/main/java/pl/piomin/services/quarkus/employee/model/Employee.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.employee.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.GenerationType; 6 | import jakarta.persistence.Id; 7 | import jakarta.validation.constraints.Max; 8 | import jakarta.validation.constraints.Min; 9 | import jakarta.validation.constraints.NotBlank; 10 | import jakarta.validation.constraints.NotNull; 11 | 12 | @Entity 13 | public class Employee { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | private Long id; 18 | @NotNull 19 | private Long organizationId; 20 | @NotNull 21 | private Long departmentId; 22 | @NotBlank 23 | private String name; 24 | @Min(1) 25 | @Max(100) 26 | private int age; 27 | @NotBlank 28 | private String position; 29 | 30 | public Employee() { 31 | } 32 | 33 | public Employee(Long id, Long organizationId, Long departmentId, String name, int age, String position) { 34 | this.id = id; 35 | this.organizationId = organizationId; 36 | this.departmentId = departmentId; 37 | this.name = name; 38 | this.age = age; 39 | this.position = position; 40 | } 41 | 42 | public Long getId() { 43 | return id; 44 | } 45 | 46 | public void setId(Long id) { 47 | this.id = id; 48 | } 49 | 50 | public String getName() { 51 | return name; 52 | } 53 | 54 | public void setName(String name) { 55 | this.name = name; 56 | } 57 | 58 | public int getAge() { 59 | return age; 60 | } 61 | 62 | public void setAge(int age) { 63 | this.age = age; 64 | } 65 | 66 | public String getPosition() { 67 | return position; 68 | } 69 | 70 | public void setPosition(String position) { 71 | this.position = position; 72 | } 73 | 74 | public Long getOrganizationId() { 75 | return organizationId; 76 | } 77 | 78 | public void setOrganizationId(Long organizationId) { 79 | this.organizationId = organizationId; 80 | } 81 | 82 | public Long getDepartmentId() { 83 | return departmentId; 84 | } 85 | 86 | public void setDepartmentId(Long departmentId) { 87 | this.departmentId = departmentId; 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return "Employee [id=" + id + ", name=" + name + ", age=" + age + ", position=" + position + "]"; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Quarkus Microservices with Consul Discovery Demo Project [![Twitter](https://img.shields.io/twitter/follow/piotr_minkowski.svg?style=social&logo=twitter&label=Follow%20Me)](https://twitter.com/piotr_minkowski) 2 | 3 | In this project I'm demonstrating you the most interesting features of [Quarkus Project](https://quarkus.io/) for building microservice-based architecture. 4 | 5 | 6 | ## Getting Started 7 | Currently, you may find here some examples of microservices implementation using different projects from Quarkus. Here's a full list of available examples in this repository: 8 | 1. Using Quarkus for building microservices that may be easily deployed outside Kubernetes. Integrating Quarkus with Consul discovery and KV store. The example is available in the branch [master](https://github.com/piomin/sample-quarkus-microservices-consul/tree/master). A detailed guide may be found in the following article: Detailed description can be found here: [Quarkus Microservices with Consul Discovery](https://piotrminkowski.com/2020/11/24/quarkus-microservices-with-consul-discovery/) 9 | 2. [Latest] Quarkus with SmallRye Stork and Mutiny Consul Client. The example is available in the branch [Consul with Quarkus and SmallRye Stork](https://github.com/piomin/sample-quarkus-microservices-consul/tree/master). 10 | 11 | ## Usage 12 | 1. Maven 3.6.3+ 13 | 2. JDK 21+ 14 | 3. Run Consul with Docker: 15 | ```shell 16 | docker run -d --name=consul -e CONSUL_BIND_INTERFACE=eth0 -p 8500:8500 consul 17 | ``` 18 | 4. Run applications: 19 | ```shell 20 | mvn compile quarkus:dev 21 | ``` 22 | 23 | ## Architecture 24 | Our sample microservices-based system consists of the following modules: 25 | - **gateway-service** - a module that uses Spring Cloud Gateway for running Spring Boot application that acts as a proxy/gateway in our architecture. 26 | - **employee-service** - a module containing the first of our sample microservices that allows to perform CRUD operation on in-memory repository of employees 27 | - **department-service** - a module containing the second of our sample microservices that allows to perform CRUD operation on in-memory repository of departments. It communicates with employee-service. 28 | - **organization-service** - a module containing the third of our sample microservices that allows to perform CRUD operation on in-memory repository of organizations. It communicates with both employee-service and department-service. 29 | 30 | The following picture illustrates the architecture described above. 31 | 32 |
-------------------------------------------------------------------------------- /department-service/src/test/java/pl/piomin/services/quarkus/department/DepartmentResourceConsulTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department; 2 | 3 | import io.quarkus.test.common.QuarkusTestResource; 4 | import io.quarkus.test.junit.QuarkusTest; 5 | import io.restassured.http.ContentType; 6 | import io.smallrye.mutiny.Uni; 7 | import io.vertx.ext.consul.ConsulClient; 8 | import io.vertx.ext.consul.Service; 9 | import io.vertx.ext.consul.ServiceList; 10 | import jakarta.inject.Inject; 11 | import org.eclipse.microprofile.config.inject.ConfigProperty; 12 | import org.junit.jupiter.api.MethodOrderer; 13 | import org.junit.jupiter.api.Order; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.TestMethodOrder; 16 | import pl.piomin.services.quarkus.department.model.Department; 17 | 18 | import java.time.Duration; 19 | import java.util.List; 20 | 21 | import static io.restassured.RestAssured.given; 22 | import static io.restassured.RestAssured.when; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.hamcrest.Matchers.notNullValue; 25 | import static org.junit.jupiter.api.Assertions.assertEquals; 26 | 27 | @QuarkusTest 28 | @QuarkusTestResource(ConsulResource.class) 29 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 30 | public class DepartmentResourceConsulTests { 31 | 32 | @ConfigProperty(name = "department.name", defaultValue = "") 33 | private String name; 34 | @Inject 35 | ConsulClient consulClient; 36 | 37 | @Test 38 | @Order(1) 39 | void add() { 40 | Department d = new Department(); 41 | d.setOrganizationId(1L); 42 | d.setName(name); 43 | 44 | given().body(d).contentType(ContentType.JSON) 45 | .when().post("/departments").then() 46 | .statusCode(200) 47 | .body("id", notNullValue()) 48 | .body("name", is(name)); 49 | } 50 | 51 | @Test 52 | @Order(2) 53 | void findAll() { 54 | when().get("/departments").then() 55 | .statusCode(200) 56 | .body("size()", is(4)); 57 | } 58 | 59 | @Test 60 | @Order(3) 61 | void checkRegister() throws InterruptedException { 62 | Thread.sleep(5000); 63 | Uni uni = Uni.createFrom().completionStage(() -> consulClient.catalogServices().toCompletionStage()); 64 | List services = uni.await().atMost(Duration.ofSeconds(3)).getList(); 65 | final long count = services.stream() 66 | .filter(svc -> svc.getName().equals("department-service")).count(); 67 | assertEquals(1 ,count); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | pl.piomin.samples 8 | sample-quarkus-microservices-consul 9 | pom 10 | 1.0 11 | 12 | 13 | piomin_sample-quarkus-microservices-consul 14 | piomin 15 | https://sonarcloud.io 16 | 3.30.4 17 | 2.4.0 18 | UTF-8 19 | 21 20 | 21 21 | 3.5.4 22 | 23 | 24 | 25 | employee-service 26 | department-service 27 | organization-service 28 | gateway-service 29 | 30 | 31 | 32 | 33 | 34 | org.jacoco 35 | jacoco-maven-plugin 36 | 0.8.14 37 | 38 | 39 | 40 | prepare-agent 41 | 42 | 43 | 44 | report 45 | test 46 | 47 | report 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | io.quarkus.platform 59 | quarkus-bom 60 | ${quarkus.version} 61 | pom 62 | import 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /department-service/src/main/java/pl/piomin/services/quarkus/department/resource/DepartmentResource.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.department.resource; 2 | 3 | import java.util.List; 4 | 5 | import jakarta.transaction.Transactional; 6 | import jakarta.validation.Valid; 7 | import jakarta.ws.rs.GET; 8 | import jakarta.ws.rs.POST; 9 | import jakarta.ws.rs.Path; 10 | import jakarta.ws.rs.PathParam; 11 | import jakarta.ws.rs.Produces; 12 | import jakarta.ws.rs.core.MediaType; 13 | 14 | import org.eclipse.microprofile.rest.client.inject.RestClient; 15 | import org.jboss.logging.Logger; 16 | import pl.piomin.services.quarkus.department.client.EmployeeClient; 17 | import pl.piomin.services.quarkus.department.model.Department; 18 | import pl.piomin.services.quarkus.department.repository.DepartmentRepository; 19 | 20 | @Path("/departments") 21 | @Produces(MediaType.APPLICATION_JSON) 22 | public class DepartmentResource { 23 | 24 | private Logger logger; 25 | private DepartmentRepository repository; 26 | private EmployeeClient employeeClient; 27 | 28 | public DepartmentResource(Logger logger, 29 | DepartmentRepository repository, 30 | @RestClient EmployeeClient employeeClient) { 31 | this.logger = logger; 32 | this.repository = repository; 33 | this.employeeClient = employeeClient; 34 | } 35 | 36 | @Path("/") 37 | @POST 38 | @Transactional 39 | public Department add(@Valid Department department) { 40 | logger.infof("Department add: %s", department); 41 | repository.persist(department); 42 | return department; 43 | } 44 | 45 | @Path("/{id}") 46 | @GET 47 | public Department findById(@PathParam("id") Long id) { 48 | logger.infof("Department find: id=%d", id); 49 | return repository.findById(id); 50 | } 51 | 52 | @GET 53 | public List findAll() { 54 | logger.infof("Department find"); 55 | return repository.findAll().list(); 56 | } 57 | 58 | @Path("/organization/{organizationId}") 59 | @GET 60 | public List findByOrganization(@PathParam("organizationId") Long organizationId) { 61 | logger.infof("Department find: organizationId=%d", organizationId); 62 | return repository.findByOrganization(organizationId); 63 | } 64 | 65 | @Path("/organization/{organizationId}/with-employees") 66 | @GET 67 | public List findByOrganizationWithEmployees(@PathParam("organizationId") Long organizationId) { 68 | logger.infof("Department find with employees: organizationId=%d", organizationId); 69 | List departments = repository.findByOrganization(organizationId); 70 | departments.forEach(d -> d.setEmployees(employeeClient.findByDepartment(d.getId()))); 71 | return departments; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /organization-service/src/main/java/pl/piomin/services/quarkus/organization/resource/OrganizationResource.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.quarkus.organization.resource; 2 | 3 | import java.util.List; 4 | 5 | import jakarta.validation.Valid; 6 | import jakarta.ws.rs.GET; 7 | import jakarta.ws.rs.POST; 8 | import jakarta.ws.rs.Path; 9 | import jakarta.ws.rs.PathParam; 10 | import jakarta.ws.rs.Produces; 11 | import jakarta.ws.rs.core.MediaType; 12 | 13 | import org.eclipse.microprofile.rest.client.inject.RestClient; 14 | import org.jboss.logging.Logger; 15 | import pl.piomin.services.quarkus.organization.client.DepartmentClient; 16 | import pl.piomin.services.quarkus.organization.client.EmployeeClient; 17 | import pl.piomin.services.quarkus.organization.model.Organization; 18 | import pl.piomin.services.quarkus.organization.repository.OrganizationRepository; 19 | 20 | @Path("/organizations") 21 | @Produces(MediaType.APPLICATION_JSON) 22 | public class OrganizationResource { 23 | 24 | private Logger logger; 25 | private OrganizationRepository repository; 26 | private DepartmentClient departmentClient; 27 | private EmployeeClient employeeClient; 28 | 29 | public OrganizationResource(Logger logger, 30 | OrganizationRepository repository, 31 | @RestClient DepartmentClient departmentClient, 32 | @RestClient EmployeeClient employeeClient) { 33 | this.logger = logger; 34 | this.repository = repository; 35 | this.departmentClient = departmentClient; 36 | this.employeeClient = employeeClient; 37 | } 38 | 39 | @POST 40 | public Organization add(@Valid Organization organization) { 41 | logger.infof("Organization add: %s", organization); 42 | repository.persist(organization); 43 | return organization; 44 | } 45 | 46 | @GET 47 | public List findAll() { 48 | logger.info("Organization find"); 49 | return repository.findAll().list(); 50 | } 51 | 52 | @Path("/{id}") 53 | @GET 54 | public Organization findById(@PathParam("id") Long id) { 55 | logger.infof("Organization find: id=%s", id); 56 | return repository.findById(id); 57 | } 58 | 59 | @Path("/{id}/with-departments") 60 | @GET 61 | public Organization findByIdWithDepartments(@PathParam("id") Long id) { 62 | logger.infof("Organization find with departments: id=%d", id); 63 | Organization organization = repository.findById(id); 64 | organization.setDepartments(departmentClient.findByOrganization(organization.getId())); 65 | return organization; 66 | } 67 | 68 | @Path("/{id}/with-departments-and-employees") 69 | @GET 70 | public Organization findByIdWithDepartmentsAndEmployees(@PathParam("id") Long id) { 71 | logger.infof("Organization find with departments and employees: id=%d", id); 72 | Organization organization = repository.findById(id); 73 | organization.setDepartments(departmentClient.findByOrganizationWithEmployees(organization.getId())); 74 | return organization; 75 | } 76 | 77 | @Path("/{id}/with-employees") 78 | @GET 79 | public Organization findByIdWithEmployees(@PathParam("id") Long id) { 80 | logger.infof("Organization find with employees: id=%d", id); 81 | Organization organization = repository.findById(id); 82 | organization.setEmployees(employeeClient.findByOrganization(organization.getId())); 83 | return organization; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /employee-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | sample-quarkus-microservices-consul 7 | pl.piomin.samples 8 | 1.0 9 | 10 | 4.0.0 11 | employee-service 12 | 13 | 14 | ${project.artifactId} 15 | 16 | 17 | 18 | 19 | 20 | io.quarkus.platform 21 | quarkus-maven-plugin 22 | ${quarkus.version} 23 | 24 | 25 | 26 | build 27 | 28 | 29 | 30 | 31 | 32 | maven-surefire-plugin 33 | ${surefire-plugin.version} 34 | 35 | 36 | 37 | 38 | 39 | 40 | io.quarkus 41 | quarkus-rest-jackson 42 | 43 | 44 | io.quarkus 45 | quarkus-hibernate-validator 46 | 47 | 48 | io.quarkus 49 | quarkus-hibernate-orm-panache 50 | 51 | 52 | io.quarkus 53 | quarkus-jdbc-h2 54 | 55 | 56 | io.quarkus 57 | quarkus-micrometer-registry-prometheus 58 | 59 | 60 | com.h2database 61 | h2 62 | runtime 63 | 64 | 65 | io.quarkus 66 | quarkus-smallrye-stork 67 | 68 | 69 | io.smallrye.reactive 70 | smallrye-mutiny-vertx-consul-client 71 | 72 | 73 | io.smallrye.stork 74 | stork-service-discovery-consul 75 | 76 | 77 | io.smallrye.stork 78 | stork-service-registration-consul 79 | 80 | 81 | io.quarkus 82 | quarkus-scheduler 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | io.quarkiverse.config 91 | quarkus-config-consul 92 | ${quarkus-consul.version} 93 | 94 | 95 | io.rest-assured 96 | rest-assured 97 | test 98 | 99 | 100 | io.quarkus 101 | quarkus-junit5 102 | test 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /organization-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | sample-quarkus-microservices-consul 7 | pl.piomin.samples 8 | 1.0 9 | 10 | 4.0.0 11 | 12 | organization-service 13 | 14 | 15 | ${project.artifactId} 16 | 17 | 18 | 19 | 20 | 21 | io.quarkus.platform 22 | quarkus-maven-plugin 23 | ${quarkus.version} 24 | 25 | 26 | 27 | build 28 | 29 | 30 | 31 | 32 | 33 | maven-surefire-plugin 34 | ${surefire-plugin.version} 35 | 36 | 37 | 38 | 39 | 40 | 41 | io.quarkus 42 | quarkus-rest-jackson 43 | 44 | 45 | io.quarkus 46 | quarkus-rest-client-jackson 47 | 48 | 49 | io.quarkus 50 | quarkus-hibernate-validator 51 | 52 | 53 | io.quarkus 54 | quarkus-smallrye-openapi 55 | 56 | 57 | io.quarkus 58 | quarkus-hibernate-orm-panache 59 | 60 | 61 | io.quarkus 62 | quarkus-jdbc-h2 63 | 64 | 65 | io.quarkus 66 | quarkus-micrometer-registry-prometheus 67 | 68 | 69 | com.h2database 70 | h2 71 | runtime 72 | 73 | 74 | io.smallrye.stork 75 | stork-service-discovery-consul 76 | 77 | 78 | io.smallrye.stork 79 | stork-service-registration-consul 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | io.quarkiverse.config 88 | quarkus-config-consul 89 | ${quarkus-consul.version} 90 | 91 | 92 | io.rest-assured 93 | rest-assured 94 | test 95 | 96 | 97 | io.quarkus 98 | quarkus-junit5 99 | test 100 | 101 | 102 | org.instancio 103 | instancio-junit 104 | 5.5.1 105 | test 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /department-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | sample-quarkus-microservices-consul 7 | pl.piomin.samples 8 | 1.0 9 | 10 | 4.0.0 11 | 12 | department-service 13 | 14 | 15 | ${project.artifactId} 16 | 17 | 18 | 19 | 20 | 21 | io.quarkus.platform 22 | quarkus-maven-plugin 23 | ${quarkus.version} 24 | 25 | 26 | 27 | build 28 | 29 | 30 | 31 | 32 | 33 | maven-surefire-plugin 34 | ${surefire-plugin.version} 35 | 36 | 37 | 38 | 39 | 40 | 41 | io.quarkus 42 | quarkus-rest-jackson 43 | 44 | 45 | io.quarkus 46 | quarkus-rest-client-jackson 47 | 48 | 49 | io.quarkus 50 | quarkus-hibernate-validator 51 | 52 | 53 | io.quarkus 54 | quarkus-hibernate-orm-panache 55 | 56 | 57 | io.quarkus 58 | quarkus-jdbc-h2 59 | 60 | 61 | io.quarkus 62 | quarkus-micrometer-registry-prometheus 63 | 64 | 65 | com.h2database 66 | h2 67 | runtime 68 | 69 | 70 | io.quarkus 71 | quarkus-smallrye-stork 72 | 73 | 74 | io.smallrye.reactive 75 | smallrye-mutiny-vertx-consul-client 76 | 77 | 78 | io.smallrye.stork 79 | stork-service-discovery-consul 80 | 81 | 82 | io.smallrye.stork 83 | stork-service-registration-consul 84 | 85 | 86 | io.smallrye.stork 87 | stork-load-balancer-least-response-time 88 | 89 | 90 | io.quarkus 91 | quarkus-scheduler 92 | 93 | 94 | io.quarkiverse.config 95 | quarkus-config-consul 96 | ${quarkus-consul.version} 97 | 98 | 99 | io.rest-assured 100 | rest-assured 101 | test 102 | 103 | 104 | io.quarkus 105 | quarkus-junit5 106 | test 107 | 108 | 109 | io.quarkus 110 | quarkus-junit5-mockito 111 | test 112 | 113 | 114 | org.instancio 115 | instancio-junit 116 | 5.5.1 117 | test 118 | 119 | 120 | org.testcontainers 121 | consul 122 | 1.21.4 123 | test 124 | 125 | 126 | org.testcontainers 127 | junit-jupiter 128 | 1.21.4 129 | test 130 | 131 | 132 | 133 | --------------------------------------------------------------------------------