├── .circleci └── config.yml ├── .gitignore ├── account-service ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── pl │ │ │ └── piomin │ │ │ └── services │ │ │ └── account │ │ │ ├── AccountApplication.java │ │ │ ├── controller │ │ │ └── AccountController.java │ │ │ ├── model │ │ │ └── Account.java │ │ │ └── repository │ │ │ └── AccountRepository.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── pl │ └── piomin │ └── services │ └── account │ └── AccountControllerTests.java ├── customer-service ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── pl │ │ │ └── piomin │ │ │ └── service │ │ │ └── customer │ │ │ ├── CustomerApplication.java │ │ │ ├── controller │ │ │ └── CustomerController.java │ │ │ ├── model │ │ │ ├── Account.java │ │ │ └── Customer.java │ │ │ ├── repository │ │ │ └── CustomerRepository.java │ │ │ └── utils │ │ │ └── CustomerMapper.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── pl │ └── piomin │ └── service │ └── customer │ └── CustomerControllerTests.java ├── discovery-service ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── pl │ │ │ └── piomin │ │ │ └── services │ │ │ └── discovery │ │ │ └── DiscoveryApplication.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── pl │ └── piomin │ └── services │ └── discovery │ └── DiscoveryServerTests.java ├── gateway-service ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── pl │ │ │ └── piomin │ │ │ └── services │ │ │ └── gateway │ │ │ └── GatewayApplication.java │ └── resources │ │ └── application.yml │ └── test │ ├── java │ └── pl │ │ └── piomin │ │ └── services │ │ └── gateway │ │ ├── AccountServiceConf.java │ │ ├── AccountServiceInstanceListSupplier.java │ │ └── GatewayTests.java │ └── resources │ └── application-test.yml ├── pom.xml ├── readme.md └── renovate.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | analyze: 5 | docker: 6 | - image: 'cimg/openjdk:21.0.6' 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: Install OpenJDK 21 18 | command: | 19 | java -version 20 | sudo apt-get update && sudo apt-get install openjdk-21-jdk 21 | sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java 22 | sudo update-alternatives --set javac /usr/lib/jvm/java-21-openjdk-amd64/bin/javac 23 | java -version 24 | export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 25 | - run: 26 | name: Maven Tests 27 | command: mvn test 28 | 29 | executors: 30 | machine_executor_amd64: 31 | machine: 32 | image: ubuntu-2204:2023.10.1 33 | environment: 34 | architecture: "amd64" 35 | platform: "linux/amd64" 36 | 37 | workflows: 38 | build-and-deploy: 39 | jobs: 40 | - test 41 | - analyze: 42 | context: SonarCloud -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.settings/ 3 | -------------------------------------------------------------------------------- /account-service/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.classpath 3 | /.project 4 | /.settings/ 5 | -------------------------------------------------------------------------------- /account-service/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | pl.piomin.services 7 | sample-spring-cloud-webflux 8 | 1.1-SNAPSHOT 9 | 10 | 11 | account-service 12 | 13 | 14 | ${project.artifactId} 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-webflux 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter-netflix-eureka-client 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-data-mongodb-reactive 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-test 33 | test 34 | 35 | 36 | io.projectreactor 37 | reactor-test 38 | test 39 | 40 | 41 | org.testcontainers 42 | mongodb 43 | test 44 | 45 | 46 | org.testcontainers 47 | junit-jupiter 48 | test 49 | 50 | 51 | org.instancio 52 | instancio-junit 53 | 5.4.1 54 | test 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.testcontainers 62 | testcontainers-bom 63 | 1.21.1 64 | pom 65 | import 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /account-service/src/main/java/pl/piomin/services/account/AccountApplication.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.account; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @SpringBootApplication 8 | @EnableDiscoveryClient 9 | public class AccountApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(AccountApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /account-service/src/main/java/pl/piomin/services/account/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.account.controller; 2 | 3 | 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import pl.piomin.services.account.model.Account; 14 | import pl.piomin.services.account.repository.AccountRepository; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | 18 | @RestController 19 | public class AccountController { 20 | 21 | private static final Logger LOGGER = LoggerFactory.getLogger(AccountController.class); 22 | 23 | @Autowired 24 | private AccountRepository repository; 25 | 26 | @GetMapping("/customer/{customer}") 27 | public Flux findByCustomer(@PathVariable("customer") String customerId) { 28 | LOGGER.info("findByCustomer: customerId={}", customerId); 29 | return repository.findByCustomerId(customerId); 30 | } 31 | 32 | @GetMapping("/") 33 | public Flux findAll() { 34 | LOGGER.info("findAll"); 35 | return repository.findAll(); 36 | } 37 | 38 | @GetMapping("/{id}") 39 | public Mono findById(@PathVariable("id") String id) { 40 | LOGGER.info("findById: id={}", id); 41 | return repository.findById(id); 42 | } 43 | 44 | @PostMapping("/") 45 | public Mono create(@RequestBody Account account) { 46 | LOGGER.info("create: {}", account); 47 | return repository.save(account); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /account-service/src/main/java/pl/piomin/services/account/model/Account.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.account.model; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | @Document 7 | public class Account { 8 | 9 | @Id 10 | private String id; 11 | private String number; 12 | private String customerId; 13 | private int amount; 14 | 15 | public Account() { 16 | 17 | } 18 | 19 | public Account(String number, String customerId, int amount) { 20 | this.number = number; 21 | this.customerId = customerId; 22 | this.amount = amount; 23 | } 24 | 25 | 26 | public String getId() { 27 | return id; 28 | } 29 | 30 | public void setId(String id) { 31 | this.id = id; 32 | } 33 | 34 | public String getNumber() { 35 | return number; 36 | } 37 | 38 | public void setNumber(String number) { 39 | this.number = number; 40 | } 41 | 42 | public String getCustomerId() { 43 | return customerId; 44 | } 45 | 46 | public void setCustomerId(String customerId) { 47 | this.customerId = customerId; 48 | } 49 | 50 | public int getAmount() { 51 | return amount; 52 | } 53 | 54 | public void setAmount(int amount) { 55 | this.amount = amount; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "Account [id=" + id + ", number=" + number + ", customerId=" + customerId + ", amount=" + amount + "]"; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /account-service/src/main/java/pl/piomin/services/account/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.account.repository; 2 | 3 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 4 | import pl.piomin.services.account.model.Account; 5 | import reactor.core.publisher.Flux; 6 | 7 | public interface AccountRepository extends ReactiveCrudRepository { 8 | 9 | Flux findByCustomerId(String customerId); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /account-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 2222 3 | 4 | spring: 5 | application: 6 | name: account-service 7 | data: 8 | mongodb: 9 | uri: mongodb://localhost:27017/test -------------------------------------------------------------------------------- /account-service/src/test/java/pl/piomin/services/account/AccountControllerTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.account; 2 | 3 | import org.junit.jupiter.api.MethodOrderer; 4 | import org.junit.jupiter.api.Order; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.TestMethodOrder; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.test.context.DynamicPropertyRegistry; 11 | import org.springframework.test.context.DynamicPropertySource; 12 | import org.testcontainers.containers.MongoDBContainer; 13 | import org.testcontainers.junit.jupiter.Container; 14 | import org.testcontainers.junit.jupiter.Testcontainers; 15 | import pl.piomin.services.account.model.Account; 16 | 17 | import static org.junit.jupiter.api.Assertions.*; 18 | 19 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 20 | properties = {"spring.cloud.discovery.enabled=false"}) 21 | @Testcontainers 22 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 23 | public class AccountControllerTests { 24 | 25 | static String id; 26 | 27 | @Container 28 | static MongoDBContainer mongodb = new MongoDBContainer("mongo:4.4"); 29 | 30 | @DynamicPropertySource 31 | static void registerMongoProperties(DynamicPropertyRegistry registry) { 32 | String uri = mongodb.getConnectionString() + "/test"; 33 | registry.add("spring.data.mongodb.uri", () -> uri); 34 | } 35 | 36 | @Autowired 37 | TestRestTemplate restTemplate; 38 | 39 | @Test 40 | @Order(1) 41 | void add() { 42 | Account account = new Account("123456", "1", 10000); 43 | account = restTemplate.postForObject("/", account, Account.class); 44 | assertNotNull(account); 45 | assertNotNull(account.getId()); 46 | id = account.getId(); 47 | } 48 | 49 | @Test 50 | @Order(2) 51 | void findById() { 52 | Account account = restTemplate.getForObject("/{id}", Account.class, id); 53 | assertNotNull(account); 54 | assertNotNull(account.getId()); 55 | assertEquals(id, account.getId()); 56 | } 57 | 58 | @Test 59 | @Order(2) 60 | void findAll() { 61 | Account[] accounts = restTemplate.getForObject("/", Account[].class); 62 | assertTrue(accounts.length > 0); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /customer-service/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.classpath 3 | /.project 4 | /.settings/ 5 | -------------------------------------------------------------------------------- /customer-service/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | pl.piomin.services 6 | customer-service 7 | 1.1-SNAPSHOT 8 | 9 | 10 | pl.piomin.services 11 | sample-spring-cloud-webflux 12 | 1.1-SNAPSHOT 13 | 14 | 15 | 16 | ${project.artifactId} 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-webflux 23 | 24 | 25 | org.springframework.cloud 26 | spring-cloud-starter-netflix-eureka-client 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-mongodb-reactive 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | org.testcontainers 39 | mongodb 40 | test 41 | 42 | 43 | org.testcontainers 44 | junit-jupiter 45 | test 46 | 47 | 48 | io.projectreactor 49 | reactor-test 50 | test 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.testcontainers 58 | testcontainers-bom 59 | 1.21.1 60 | pom 61 | import 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /customer-service/src/main/java/pl/piomin/service/customer/CustomerApplication.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.service.customer; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.client.loadbalancer.LoadBalanced; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.web.reactive.function.client.WebClient; 9 | 10 | @SpringBootApplication 11 | @EnableDiscoveryClient 12 | public class CustomerApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(CustomerApplication.class, args); 16 | } 17 | 18 | @Bean 19 | @LoadBalanced 20 | public WebClient.Builder loadBalancedWebClientBuilder() { 21 | return WebClient.builder(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /customer-service/src/main/java/pl/piomin/service/customer/controller/CustomerController.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.service.customer.controller; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RestController; 11 | import org.springframework.web.reactive.function.client.WebClient; 12 | 13 | import pl.piomin.service.customer.model.Account; 14 | import pl.piomin.service.customer.model.Customer; 15 | import pl.piomin.service.customer.repository.CustomerRepository; 16 | import pl.piomin.service.customer.utils.CustomerMapper; 17 | import reactor.core.publisher.Flux; 18 | import reactor.core.publisher.Mono; 19 | 20 | @RestController 21 | public class CustomerController { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(CustomerController.class); 24 | 25 | @Autowired 26 | private CustomerRepository repository; 27 | @Autowired 28 | private WebClient.Builder webClientBuilder; 29 | 30 | @GetMapping("/{id}") 31 | public Mono findById(@PathVariable("id") String id) { 32 | LOGGER.info("findById: id={}", id); 33 | return repository.findById(id); 34 | } 35 | 36 | @GetMapping("/") 37 | public Flux findAll() { 38 | LOGGER.info("findAll"); 39 | return repository.findAll(); 40 | } 41 | 42 | @GetMapping("/{id}/with-accounts") 43 | public Mono findByIdWithAccounts(@PathVariable("id") String id) { 44 | LOGGER.info("findByIdWithAccounts: id={}", id); 45 | Flux accounts = webClientBuilder.build().get().uri("http://account-service/customer/{customer}", id).retrieve().bodyToFlux(Account.class); 46 | return accounts 47 | .collectList() 48 | .map(Customer::new) 49 | .mergeWith(repository.findById(id)) 50 | .collectList() 51 | .map(CustomerMapper::map); 52 | } 53 | 54 | @PostMapping("/") 55 | public Mono create(@RequestBody Customer customer) { 56 | LOGGER.info("create: {}", customer); 57 | return repository.save(customer); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /customer-service/src/main/java/pl/piomin/service/customer/model/Account.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.service.customer.model; 2 | 3 | public class Account { 4 | 5 | private String id; 6 | private String number; 7 | private int amount; 8 | 9 | public Account() { 10 | 11 | } 12 | 13 | public Account(String id, String number, int amount) { 14 | this.id = id; 15 | this.number = number; 16 | this.amount = amount; 17 | } 18 | 19 | public String getId() { 20 | return id; 21 | } 22 | 23 | public void setId(String id) { 24 | this.id = id; 25 | } 26 | 27 | public String getNumber() { 28 | return number; 29 | } 30 | 31 | public void setNumber(String number) { 32 | this.number = number; 33 | } 34 | 35 | public int getAmount() { 36 | return amount; 37 | } 38 | 39 | public void setAmount(int amount) { 40 | this.amount = amount; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return "Account [id=" + id + ", number=" + number + ", amount=" + amount + "]"; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /customer-service/src/main/java/pl/piomin/service/customer/model/Customer.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.service.customer.model; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.annotation.Transient; 7 | import org.springframework.data.mongodb.core.mapping.Document; 8 | 9 | @Document 10 | public class Customer { 11 | 12 | @Id 13 | private String id; 14 | private String firstName; 15 | private String lastName; 16 | private int age; 17 | @Transient 18 | private List accounts; 19 | 20 | public Customer() { 21 | 22 | } 23 | 24 | public Customer(List accounts) { 25 | this.accounts = accounts; 26 | } 27 | 28 | public Customer(String firstName, String lastName, int age) { 29 | this.firstName = firstName; 30 | this.lastName = lastName; 31 | this.age = age; 32 | } 33 | 34 | public String getId() { 35 | return id; 36 | } 37 | 38 | public void setId(String id) { 39 | this.id = id; 40 | } 41 | 42 | public String getFirstName() { 43 | return firstName; 44 | } 45 | 46 | public void setFirstName(String firstName) { 47 | this.firstName = firstName; 48 | } 49 | 50 | public String getLastName() { 51 | return lastName; 52 | } 53 | 54 | public void setLastName(String lastName) { 55 | this.lastName = lastName; 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 List getAccounts() { 67 | return accounts; 68 | } 69 | 70 | public void addAccount(Account account) { 71 | this.accounts.add(account); 72 | } 73 | 74 | public void setAccounts(List accounts) { 75 | this.accounts = accounts; 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return "Customer [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + "]"; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /customer-service/src/main/java/pl/piomin/service/customer/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.service.customer.repository; 2 | 3 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 4 | 5 | import pl.piomin.service.customer.model.Customer; 6 | 7 | public interface CustomerRepository extends ReactiveCrudRepository { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /customer-service/src/main/java/pl/piomin/service/customer/utils/CustomerMapper.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.service.customer.utils; 2 | 3 | import java.util.List; 4 | 5 | import pl.piomin.service.customer.model.Customer; 6 | 7 | public class CustomerMapper { 8 | 9 | public static Customer map(List customers) { 10 | Customer customer = new Customer(); 11 | for (Customer c : customers) { 12 | if (c.getAccounts() != null) customer.setAccounts(c.getAccounts()); 13 | if (c.getAge() != 0) customer.setAge(c.getAge()); 14 | if (c.getFirstName() != null) customer.setFirstName(c.getFirstName()); 15 | if (c.getLastName() != null) customer.setFirstName(c.getLastName()); 16 | } 17 | return customer; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /customer-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 3333 3 | 4 | spring: 5 | application: 6 | name: customer-service 7 | data: 8 | mongodb: 9 | uri: mongodb://localhost:27017/test -------------------------------------------------------------------------------- /customer-service/src/test/java/pl/piomin/service/customer/CustomerControllerTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.service.customer; 2 | 3 | import org.junit.jupiter.api.MethodOrderer; 4 | import org.junit.jupiter.api.Order; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.TestMethodOrder; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.test.context.DynamicPropertyRegistry; 11 | import org.springframework.test.context.DynamicPropertySource; 12 | import org.testcontainers.containers.MongoDBContainer; 13 | import org.testcontainers.junit.jupiter.Container; 14 | import org.testcontainers.junit.jupiter.Testcontainers; 15 | import pl.piomin.service.customer.model.Customer; 16 | 17 | import static org.junit.jupiter.api.Assertions.*; 18 | 19 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 20 | properties = {"spring.cloud.discovery.enabled=false"}) 21 | @Testcontainers 22 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 23 | public class CustomerControllerTests { 24 | 25 | static String id; 26 | 27 | @Container 28 | static MongoDBContainer mongodb = new MongoDBContainer("mongo:4.4"); 29 | 30 | @DynamicPropertySource 31 | static void registerMongoProperties(DynamicPropertyRegistry registry) { 32 | String uri = mongodb.getConnectionString() + "/test"; 33 | registry.add("spring.data.mongodb.uri", () -> uri); 34 | } 35 | 36 | @Autowired 37 | TestRestTemplate restTemplate; 38 | 39 | @Test 40 | @Order(1) 41 | void add() { 42 | Customer customer = new Customer("Test1", "Test2", 10); 43 | customer = restTemplate.postForObject("/", customer, Customer.class); 44 | assertNotNull(customer); 45 | assertNotNull(customer.getId()); 46 | id = customer.getId(); 47 | } 48 | 49 | @Test 50 | @Order(2) 51 | void findById() { 52 | Customer customer = restTemplate.getForObject("/{id}", Customer.class, id); 53 | assertNotNull(customer); 54 | assertNotNull(customer.getId()); 55 | assertEquals(id, customer.getId()); 56 | } 57 | 58 | @Test 59 | @Order(2) 60 | void findAll() { 61 | Customer[] customers = restTemplate.getForObject("/", Customer[].class); 62 | assertTrue(customers.length > 0); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /discovery-service/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.settings/ 3 | /.classpath 4 | /.project 5 | -------------------------------------------------------------------------------- /discovery-service/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | pl.piomin.services 7 | sample-spring-cloud-webflux 8 | 1.1-SNAPSHOT 9 | 10 | 11 | discovery-service 12 | 13 | 14 | ${project.artifactId} 15 | 16 | 17 | 18 | 19 | org.springframework.cloud 20 | spring-cloud-starter-netflix-eureka-server 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-test 25 | test 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /discovery-service/src/main/java/pl/piomin/services/discovery/DiscoveryApplication.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.discovery; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | import org.springframework.boot.builder.SpringApplicationBuilder; 5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 | 7 | @SpringBootApplication 8 | @EnableEurekaServer 9 | public class DiscoveryApplication { 10 | 11 | public static void main(String[] args) { 12 | new SpringApplicationBuilder(DiscoveryApplication.class).run(args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /discovery-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8761 3 | 4 | spring: 5 | application: 6 | name: discovery-service 7 | 8 | eureka: 9 | instance: 10 | hostname: localhost 11 | client: 12 | registerWithEureka: false 13 | fetchRegistry: false 14 | serviceUrl: 15 | defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ -------------------------------------------------------------------------------- /discovery-service/src/test/java/pl/piomin/services/discovery/DiscoveryServerTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.discovery; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.web.client.TestRestTemplate; 7 | import org.springframework.http.ResponseEntity; 8 | 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) 12 | public class DiscoveryServerTests { 13 | 14 | @Autowired 15 | TestRestTemplate restTemplate; 16 | 17 | @Test 18 | public void findAccounts() { 19 | ResponseEntity response = restTemplate 20 | .getForEntity("/", String.class); 21 | assertEquals(200, response.getStatusCodeValue()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gateway-service/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.classpath 3 | /.project 4 | /.settings/ 5 | -------------------------------------------------------------------------------- /gateway-service/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | pl.piomin.services 7 | sample-spring-cloud-webflux 8 | 1.1-SNAPSHOT 9 | 10 | 11 | gateway-service 12 | 13 | 14 | ${artifactId} 15 | 16 | 17 | 18 | 19 | org.springframework.cloud 20 | spring-cloud-starter-gateway 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter-netflix-eureka-client 25 | 26 | 27 | io.micrometer 28 | micrometer-tracing-bridge-otel 29 | 30 | 31 | io.opentelemetry 32 | opentelemetry-exporter-zipkin 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-test 37 | test 38 | 39 | 40 | io.specto 41 | hoverfly-java-junit5 42 | 0.20.2 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /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 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @SpringBootApplication 8 | @EnableDiscoveryClient 9 | public class GatewayApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(GatewayApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /gateway-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8090 3 | 4 | eureka: 5 | client: 6 | registerWithEureka: false 7 | serviceUrl: 8 | defaultZone: http://localhost:8761/eureka/ 9 | 10 | logging: 11 | pattern: 12 | console: "%d{yyyy-MM-dd HH:mm:ss} ${LOG_LEVEL_PATTERN:-%5p} %m%n" 13 | 14 | spring: 15 | application: 16 | name: gateway-service 17 | cloud: 18 | gateway: 19 | discovery: 20 | locator: 21 | enabled: true 22 | routes: 23 | - id: account-service 24 | uri: lb://account-service 25 | predicates: 26 | - Path=/account/** 27 | filters: 28 | - RewritePath=/account/(?.*), /$\{path} 29 | - id: customer-service 30 | uri: lb://customer-service 31 | predicates: 32 | - Path=/customer/** 33 | filters: 34 | - RewritePath=/customer/(?.*), /$\{path} -------------------------------------------------------------------------------- /gateway-service/src/test/java/pl/piomin/services/gateway/AccountServiceConf.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.gateway; 2 | 3 | import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Primary; 6 | 7 | public class AccountServiceConf { 8 | 9 | @Bean 10 | @Primary 11 | ServiceInstanceListSupplier serviceInstanceListSupplier() { 12 | return new AccountServiceInstanceListSuppler("account-service"); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /gateway-service/src/test/java/pl/piomin/services/gateway/AccountServiceInstanceListSupplier.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.gateway; 2 | 3 | import org.springframework.cloud.client.DefaultServiceInstance; 4 | import org.springframework.cloud.client.ServiceInstance; 5 | import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; 6 | import reactor.core.publisher.Flux; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | class AccountServiceInstanceListSuppler implements ServiceInstanceListSupplier { 12 | 13 | private final String serviceId; 14 | 15 | AccountServiceInstanceListSuppler(String serviceId) { 16 | this.serviceId = serviceId; 17 | } 18 | 19 | @Override 20 | public String getServiceId() { 21 | return serviceId; 22 | } 23 | 24 | @Override 25 | public Flux> get() { 26 | return Flux.just(Arrays 27 | .asList(new DefaultServiceInstance(serviceId + "1", serviceId, "localhost", 8080, false))); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gateway-service/src/test/java/pl/piomin/services/gateway/GatewayTests.java: -------------------------------------------------------------------------------- 1 | package pl.piomin.services.gateway; 2 | 3 | import io.specto.hoverfly.junit.core.Hoverfly; 4 | import io.specto.hoverfly.junit.core.config.LogLevel; 5 | import io.specto.hoverfly.junit5.HoverflyExtension; 6 | import io.specto.hoverfly.junit5.api.HoverflyConfig; 7 | import io.specto.hoverfly.junit5.api.HoverflyCore; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.boot.test.web.client.TestRestTemplate; 14 | import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; 15 | import org.springframework.http.ResponseEntity; 16 | 17 | import java.util.concurrent.TimeUnit; 18 | 19 | import static io.specto.hoverfly.junit.core.SimulationSource.dsl; 20 | import static io.specto.hoverfly.junit.dsl.HoverflyDsl.service; 21 | import static io.specto.hoverfly.junit.dsl.ResponseCreators.success; 22 | import static io.specto.hoverfly.junit.dsl.matchers.HoverflyMatchers.any; 23 | 24 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 25 | @HoverflyCore(config = @HoverflyConfig(logLevel = LogLevel.DEBUG, webServer = true, proxyPort = 8080)) 26 | @ExtendWith(HoverflyExtension.class) 27 | @LoadBalancerClient(name = "account-service", configuration = AccountServiceConf.class) 28 | public class GatewayTests { 29 | 30 | @Autowired 31 | TestRestTemplate restTemplate; 32 | 33 | @Test 34 | public void findAccounts(Hoverfly hoverfly) { 35 | hoverfly.simulate(dsl( 36 | service("http://localhost:8080") 37 | .andDelay(200, TimeUnit.MILLISECONDS).forAll() 38 | .get(any()) 39 | .willReturn(success("[{\"id\":\"1\",\"number\":\"1234567890\",\"balance\":5000}]", "application/json")))); 40 | 41 | ResponseEntity response = restTemplate 42 | .getForEntity("/account/1", String.class); 43 | Assertions.assertEquals(200, response.getStatusCodeValue()); 44 | Assertions.assertNotNull(response.getBody()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /gateway-service/src/test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | discovery: 4 | client: 5 | simple: 6 | instances: 7 | - account-service: 8 | uri: account-service:8080 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | pl.piomin.services 5 | sample-spring-cloud-webflux 6 | 1.1-SNAPSHOT 7 | pom 8 | 9 | 10 | org.springframework.boot 11 | spring-boot-starter-parent 12 | 3.5.0 13 | 14 | 15 | 16 | 17 | 21 18 | piomin_sample-spring-cloud-webflux 19 | piomin 20 | https://sonarcloud.io 21 | 22 | 23 | 24 | account-service 25 | discovery-service 26 | gateway-service 27 | customer-service 28 | 29 | 30 | 31 | 32 | 33 | org.springframework.cloud 34 | spring-cloud-dependencies 35 | 2025.0.0 36 | pom 37 | import 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-maven-plugin 47 | 48 | 49 | org.jacoco 50 | jacoco-maven-plugin 51 | 0.8.13 52 | 53 | 54 | 55 | prepare-agent 56 | 57 | 58 | 59 | report 60 | test 61 | 62 | report 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Reactive Microservices with Spring WebFlux and Spring Cloud 2 | 3 | [![CircleCI](https://circleci.com/gh/piomin/sample-spring-cloud-webflux.svg?style=svg)](https://circleci.com/gh/piomin/sample-spring-cloud-webflux) 4 | [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-black.svg)](https://sonarcloud.io/dashboard?id=piomin_sample-spring-cloud-webflux) 5 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-spring-cloud-webflux&metric=bugs)](https://sonarcloud.io/dashboard?id=piomin_sample-spring-cloud-webflux) 6 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-spring-cloud-webflux&metric=coverage)](https://sonarcloud.io/dashboard?id=piomin_sample-spring-cloud-webflux) 7 | [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-spring-cloud-webflux&metric=ncloc)](https://sonarcloud.io/dashboard?id=piomin_sample-spring-cloud-webflux) 8 | 9 | A sample microservices architecture built with **Spring WebFlux** and **Spring Cloud** demonstrating reactive programming patterns, service discovery, and API gateway implementation. 10 | 11 | **Detailed description:** [Reactive Microservices with Spring WebFlux and Spring Cloud](https://piotrminkowski.com/2018/05/04/reactive-microservices-with-spring-webflux-and-spring-cloud/) 12 | 13 | ## 🏗️ Architecture Overview 14 | 15 | This project demonstrates a reactive microservices architecture with the following components: 16 | 17 | ```mermaid 18 | graph TB 19 | Client["Client Application"] 20 | Gateway["Gateway Service
:8090"] 21 | Discovery["Discovery Service
Eureka Server
:8761"] 22 | Account["Account Service
:2222"] 23 | Customer["Customer Service
:3333"] 24 | MongoDB[(MongoDB
:27017)] 25 | 26 | Client --> Gateway 27 | Gateway --> Account 28 | Gateway --> Customer 29 | Customer --> Account 30 | Account --> Discovery 31 | Customer --> Discovery 32 | Gateway --> Discovery 33 | Account --> MongoDB 34 | Customer --> MongoDB 35 | 36 | classDef serviceBox fill:#e1f5fe,stroke:#01579b,stroke-width:2px; 37 | classDef dbBox fill:#f3e5f5,stroke:#4a148c,stroke-width:2px; 38 | classDef gatewayBox fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px; 39 | 40 | class Account, Customer, Discovery serviceBox 41 | class MongoDB dbBox 42 | class Gateway gatewayBox 43 | ``` 44 | 45 | ### Architecture Patterns 46 | 47 | - **Reactive Programming**: Built with Spring WebFlux for non-blocking, asynchronous processing 48 | - **Service Discovery**: Netflix Eureka for dynamic service registration and discovery 49 | - **API Gateway**: Spring Cloud Gateway for routing, load balancing, and cross-cutting concerns 50 | - **Microservices**: Domain-driven service boundaries with independent data stores 51 | 52 | ## 🚀 Services 53 | 54 | ### Discovery Service (Eureka Server) 55 | - **Port**: 8761 56 | - **Purpose**: Service registry for microservices discovery 57 | - **Technology**: Netflix Eureka Server 58 | - **Endpoint**: `http://localhost:8761` (Eureka Dashboard) 59 | 60 | ### Gateway Service (API Gateway) 61 | - **Port**: 8090 62 | - **Purpose**: Single entry point, routing, and load balancing 63 | - **Technology**: Spring Cloud Gateway 64 | - **Routes**: 65 | - `/account/**` → Account Service 66 | - `/customer/**` → Customer Service 67 | 68 | ### Account Service 69 | - **Port**: 2222 70 | - **Purpose**: Manages customer accounts and financial data 71 | - **Technology**: Spring WebFlux, MongoDB Reactive 72 | - **Database**: MongoDB collection for accounts 73 | 74 | ### Customer Service 75 | - **Port**: 3333 76 | - **Purpose**: Manages customer information and aggregates account data 77 | - **Technology**: Spring WebFlux, MongoDB Reactive, WebClient 78 | - **Database**: MongoDB collection for customers 79 | - **Dependencies**: Calls Account Service for account aggregation 80 | 81 | ## 🛠️ Technology Stack 82 | 83 | | Technology | Version | Purpose | 84 | |---------------------|------------|----------------------------------| 85 | | Java | 21 | Runtime environment | 86 | | Spring Boot | 3.4.6 | Application framework | 87 | | Spring Cloud | 2024.0.1 | Microservices infrastructure | 88 | | Spring WebFlux | — | Reactive web framework | 89 | | Spring Cloud Gateway| — | API Gateway | 90 | | Netflix Eureka | — | Service discovery | 91 | | MongoDB | 4.0+ | NoSQL database | 92 | | Maven | 3.6+ | Build tool | 93 | 94 | ## 📋 Prerequisites 95 | 96 | Before running the application locally, ensure you have: 97 | 98 | - **Java 21** or higher 99 | - **Maven 3.6+** 100 | - **MongoDB 4.0+** running on `localhost:27017` 101 | 102 | ### MongoDB Setup 103 | 104 | 1. **Install MongoDB**: 105 | ```bash 106 | # macOS (Homebrew) 107 | brew install mongodb/brew/mongodb-community 108 | # Ubuntu/Debian 109 | sudo apt-get install mongodb 110 | # Windows 111 | Download from https://www.mongodb.com/try/download/community 112 | ``` 113 | 2. **Start MongoDB**: 114 | ```bash 115 | # macOS/Linux 116 | sudo systemctl start mongod 117 | # or 118 | brew services start mongodb/brew/mongodb-community 119 | # Windows 120 | net start MongoDB 121 | ``` 122 | 3. **Verify**: 123 | ```bash 124 | mongosh --eval "db.adminCommand('ismaster')" 125 | ``` 126 | 127 | ## 🚀 Running Locally 128 | 129 | ### Option 1: Manual Startup (Development) 130 | 131 | 1. Clone: 132 | ```bash 133 | git clone https://github.com/piomin/sample-spring-cloud-webflux.git 134 | cd sample-spring-cloud-webflux 135 | ``` 136 | 2. Build: 137 | ```bash 138 | mvn clean install 139 | ``` 140 | 3. Start services in order: 141 | 1. Discovery Service: 142 | ```bash 143 | cd discovery-service 144 | mvn spring-boot:run 145 | ``` 146 | 2. Account Service (new terminal): 147 | ```bash 148 | cd account-service 149 | mvn spring-boot:run 150 | ``` 151 | 3. Customer Service (new terminal): 152 | ```bash 153 | cd customer-service 154 | mvn spring-boot:run 155 | ``` 156 | 4. Gateway Service (new terminal): 157 | ```bash 158 | cd gateway-service 159 | mvn spring-boot:run 160 | ``` 161 | 162 | ### Option 2: Using JARs 163 | 164 | 1. Package: 165 | ```bash 166 | mvn clean package 167 | ``` 168 | 2. Run: 169 | ```bash 170 | java -jar discovery-service/target/discovery-service-1.1-SNAPSHOT.jar 171 | java -jar account-service/target/account-service-1.1-SNAPSHOT.jar 172 | java -jar customer-service/target/customer-service-1.1-SNAPSHOT.jar 173 | java -jar gateway-service/target/gateway-service-1.1-SNAPSHOT.jar 174 | ``` 175 | 176 | ### Verification 177 | 178 | 1. Eureka Dashboard: `http://localhost:8761` 179 | 2. Gateway Health: `http://localhost:8090/actuator/health` 180 | 3. API endpoints: (see API section) 181 | 182 | ## 📡 API Documentation 183 | 184 | All API calls go through Gateway at `http://localhost:8090`. 185 | 186 | ### Account Service APIs 187 | 188 | | Method | Endpoint | Description | 189 | |--------|----------------------------|---------------------| 190 | | GET | `/account/` | List all accounts | 191 | | GET | `/account/{id}` | Get account by ID | 192 | | GET | `/account/customer/{id}` | Accounts by client | 193 | | POST | `/account/` | Create new account | 194 | 195 | **Create Account Example**: 196 | ```bash 197 | curl -X POST http://localhost:8090/account/ \ 198 | -H "Content-Type: application/json" \ 199 | -d '{ 200 | "number": "1234567890", 201 | "amount": 5000, 202 | "customerId": "1" 203 | }' 204 | ``` 205 | 206 | ### Customer Service APIs 207 | 208 | | Method | Endpoint | Description | 209 | |--------|--------------------------------------|---------------------------| 210 | | GET | `/customer/` | List all customers | 211 | | GET | `/customer/{id}` | Get customer by ID | 212 | | GET | `/customer/{id}/with-accounts` | Customer + accounts | 213 | | POST | `/customer/` | Create new customer | 214 | 215 | **Create Customer Example**: 216 | ```bash 217 | curl -X POST http://localhost:8090/customer/ \ 218 | -H "Content-Type: application/json" \ 219 | -d '{"name":"John Doe","type":"INDIVIDUAL"}' 220 | ``` 221 | 222 | ## 🔧 Development & Troubleshooting 223 | 224 | ### Project Structure 225 | ``` 226 | sample-spring-cloud-webflux/ 227 | ├── discovery-service/ 228 | ├── gateway-service/ 229 | ├── account-service/ 230 | ├── customer-service/ 231 | ├── pom.xml 232 | └── readme.md 233 | ``` 234 | 235 | ### Common Issues 236 | 237 | - MongoDB not running: start service on `localhost:27017` 238 | - Eureka startup: ensure discovery-service runs first 239 | - Port conflicts: verify ports 8761, 8090, 2222, 3333 240 | 241 | ## 🤝 Contributing 242 | 243 | 1. Fork the repo 244 | 2. Create a branch 245 | 3. Make changes & add tests 246 | 4. Submit a PR 247 | 248 | ## 📄 License 249 | 250 | Licensed under the MIT License. See LICENSE for details. -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------