├── .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 | [](https://circleci.com/gh/piomin/sample-spring-cloud-webflux)
4 | [](https://sonarcloud.io/dashboard?id=piomin_sample-spring-cloud-webflux)
5 | [](https://sonarcloud.io/dashboard?id=piomin_sample-spring-cloud-webflux)
6 | [](https://sonarcloud.io/dashboard?id=piomin_sample-spring-cloud-webflux)
7 | [](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 | }
--------------------------------------------------------------------------------