diff --git a/account-service/src/main/java/pl/piomin/services/account/AccountApplication.java b/account-service/src/main/java/pl/piomin/services/account/AccountApplication.java index 5b9b8c6..819c5e8 100644 --- a/account-service/src/main/java/pl/piomin/services/account/AccountApplication.java +++ b/account-service/src/main/java/pl/piomin/services/account/AccountApplication.java @@ -1,15 +1,13 @@ package pl.piomin.services.account; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import pl.piomin.services.account.service.AccountService; import pl.piomin.services.messaging.Order; +import tools.jackson.databind.ObjectMapper; import java.util.function.Consumer; @@ -20,8 +18,11 @@ public class AccountApplication { private ObjectMapper mapper = new ObjectMapper(); - @Autowired - AccountService service; + private final AccountService service; + + public AccountApplication(AccountService service) { + this.service = service; + } public static void main(String[] args) { SpringApplication.run(AccountApplication.class, args); @@ -30,18 +31,9 @@ public static void main(String[] args) { @Bean public Consumer input() { return order -> { - try { - LOGGER.info("Order received: {}", mapper.writeValueAsString(order)); - service.process(order); - } catch (JsonProcessingException e) { - LOGGER.error("Error deserializing", e); - } + LOGGER.info("Order received: {}", mapper.writeValueAsString(order)); + service.process(order); }; } - -// @Bean -// public Sampler defaultSampler() { -// return new AlwaysSampler(); -// } } diff --git a/account-service/src/main/java/pl/piomin/services/account/controller/AccountController.java b/account-service/src/main/java/pl/piomin/services/account/controller/AccountController.java index 301ca9b..b7b1108 100644 --- a/account-service/src/main/java/pl/piomin/services/account/controller/AccountController.java +++ b/account-service/src/main/java/pl/piomin/services/account/controller/AccountController.java @@ -1,24 +1,14 @@ package pl.piomin.services.account.controller; -import java.util.Collections; -import java.util.List; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - +import org.springframework.web.bind.annotation.*; import pl.piomin.services.account.model.Account; import pl.piomin.services.account.repository.AccountRepository; +import tools.jackson.databind.ObjectMapper; + +import java.util.Collections; +import java.util.List; @RestController public class AccountController { @@ -27,8 +17,11 @@ public class AccountController { private ObjectMapper mapper = new ObjectMapper(); - @Autowired - AccountRepository repository; + private final AccountRepository repository; + + public AccountController(AccountRepository repository) { + this.repository = repository; + } @PostMapping public Account add(@RequestBody Account account) { @@ -41,7 +34,7 @@ public Account update(@RequestBody Account account) { } @PutMapping("/withdraw/{id}/{amount}") - public Account withdraw(@PathVariable("id") Long id, @PathVariable("amount") int amount) throws JsonProcessingException { + public Account withdraw(@PathVariable("id") Long id, @PathVariable int amount) { Account account = repository.findById(id); LOGGER.info("Account found: {}", mapper.writeValueAsString(account)); account.setBalance(account.getBalance() - amount); @@ -50,12 +43,12 @@ public Account withdraw(@PathVariable("id") Long id, @PathVariable("amount") int } @GetMapping("/{id}") - public Account findById(@PathVariable("id") Long id) { + public Account findById(@PathVariable Long id) { return repository.findById(id); } @GetMapping("/customer/{customerId}") - public List findByCustomerId(@PathVariable("customerId") Long customerId) { + public List findByCustomerId(@PathVariable Long customerId) { return repository.findByCustomer(customerId); } diff --git a/account-service/src/main/java/pl/piomin/services/account/messaging/OrderSender.java b/account-service/src/main/java/pl/piomin/services/account/messaging/OrderSender.java index 1a16588..05b9edf 100644 --- a/account-service/src/main/java/pl/piomin/services/account/messaging/OrderSender.java +++ b/account-service/src/main/java/pl/piomin/services/account/messaging/OrderSender.java @@ -1,6 +1,5 @@ package pl.piomin.services.account.messaging; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.function.StreamBridge; import org.springframework.integration.support.MessageBuilder; import org.springframework.stereotype.Service; @@ -9,8 +8,11 @@ @Service public class OrderSender { - @Autowired - private StreamBridge source; + private final StreamBridge source; + + public OrderSender(StreamBridge source) { + this.source = source; + } public boolean send(Order order) { return this.source.send("output", MessageBuilder.withPayload(order).build()); diff --git a/account-service/src/main/java/pl/piomin/services/account/service/AccountService.java b/account-service/src/main/java/pl/piomin/services/account/service/AccountService.java index 52bb0e6..fcc3218 100644 --- a/account-service/src/main/java/pl/piomin/services/account/service/AccountService.java +++ b/account-service/src/main/java/pl/piomin/services/account/service/AccountService.java @@ -4,17 +4,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import pl.piomin.services.account.messaging.OrderSender; import pl.piomin.services.account.model.Account; import pl.piomin.services.account.repository.AccountRepository; import pl.piomin.services.messaging.Order; import pl.piomin.services.messaging.OrderStatus; +import tools.jackson.databind.ObjectMapper; @Service public class AccountService { @@ -23,12 +20,15 @@ public class AccountService { private ObjectMapper mapper = new ObjectMapper(); - @Autowired - AccountRepository accountRepository; - @Autowired - OrderSender orderSender; + private final AccountRepository accountRepository; + private final OrderSender orderSender; + + public AccountService(AccountRepository accountRepository, OrderSender orderSender) { + this.accountRepository = accountRepository; + this.orderSender = orderSender; + } - public void process(final Order order) throws JsonProcessingException { + public void process(final Order order) { LOGGER.info("Order processed: {}", mapper.writeValueAsString(order)); List accounts = accountRepository.findByCustomer(order.getCustomerId()); Account account = accounts.get(0); diff --git a/account-service/src/test/java/pl/piomin/services/account/OrderReceiverTest.java b/account-service/src/test/java/pl/piomin/services/account/OrderReceiverTest.java index 9ffa6bf..06b6caf 100644 --- a/account-service/src/test/java/pl/piomin/services/account/OrderReceiverTest.java +++ b/account-service/src/test/java/pl/piomin/services/account/OrderReceiverTest.java @@ -1,7 +1,5 @@ package pl.piomin.services.account; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +12,7 @@ import org.springframework.messaging.Message; import pl.piomin.services.messaging.Order; import pl.piomin.services.messaging.OrderStatus; +import tools.jackson.databind.ObjectMapper; import java.util.Collections; @@ -34,7 +33,7 @@ public class OrderReceiverTest { private ObjectMapper mapper; @Test - public void testAccepted() throws JsonProcessingException { + public void testAccepted() { Order o = new Order(); o.setId(1L); o.setAccountId(1L); @@ -52,7 +51,7 @@ public void testAccepted() throws JsonProcessingException { } @Test - public void testRejected() throws JsonProcessingException { + public void testRejected() { Order o = new Order(); o.setId(1L); o.setAccountId(1L); diff --git a/order-service/pom.xml b/order-service/pom.xml index e4c497b..b6f8af7 100644 --- a/order-service/pom.xml +++ b/order-service/pom.xml @@ -72,6 +72,16 @@ junit-jupiter test + + org.springframework.boot + spring-boot-resttestclient + test + + + org.springframework.boot + spring-boot-restclient + test + diff --git a/order-service/src/main/java/pl/piomin/services/order/OrderApplication.java b/order-service/src/main/java/pl/piomin/services/order/OrderApplication.java index 8ba407e..f7401e4 100644 --- a/order-service/src/main/java/pl/piomin/services/order/OrderApplication.java +++ b/order-service/src/main/java/pl/piomin/services/order/OrderApplication.java @@ -1,10 +1,8 @@ package pl.piomin.services.order; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -20,8 +18,11 @@ public class OrderApplication { private static final Logger LOGGER = LoggerFactory.getLogger(OrderApplication.class); private ObjectMapper mapper = new ObjectMapper(); - @Autowired - OrderService service; + private final OrderService service; + + public OrderApplication(OrderService service) { + this.service = service; + } public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); @@ -30,12 +31,8 @@ public static void main(String[] args) { @Bean public Consumer input() { return order -> { - try { - LOGGER.info("Order received: {}", mapper.writeValueAsString(order)); - service.process(order); - } catch (JsonProcessingException e) { - LOGGER.error("Error deserializing", e); - } + LOGGER.info("Order received: {}", mapper.writeValueAsString(order)); + service.process(order); }; } diff --git a/order-service/src/main/java/pl/piomin/services/order/controller/OrderController.java b/order-service/src/main/java/pl/piomin/services/order/controller/OrderController.java index 8f6d44f..ba39fe2 100644 --- a/order-service/src/main/java/pl/piomin/services/order/controller/OrderController.java +++ b/order-service/src/main/java/pl/piomin/services/order/controller/OrderController.java @@ -4,15 +4,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import pl.piomin.services.messaging.Order; import pl.piomin.services.messaging.OrderStatus; @@ -26,13 +24,16 @@ public class OrderController { private ObjectMapper mapper = new ObjectMapper(); - @Autowired - OrderRepository repository; - @Autowired - OrderSender sender; + private final OrderRepository repository; + private final OrderSender sender; + + public OrderController(OrderRepository repository, OrderSender sender) { + this.repository = repository; + this.sender = sender; + } @PostMapping("/") - public Order process(@RequestBody Order order) throws JsonProcessingException { + public Order process(@RequestBody Order order) { Order o = repository.add(order); LOGGER.info("Order saved: {}", mapper.writeValueAsString(order)); boolean isSent = sender.send(o); diff --git a/order-service/src/main/java/pl/piomin/services/order/messaging/OrderSender.java b/order-service/src/main/java/pl/piomin/services/order/messaging/OrderSender.java index 0f6466c..15172c9 100644 --- a/order-service/src/main/java/pl/piomin/services/order/messaging/OrderSender.java +++ b/order-service/src/main/java/pl/piomin/services/order/messaging/OrderSender.java @@ -1,6 +1,5 @@ package pl.piomin.services.order.messaging; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.function.StreamBridge; import org.springframework.integration.support.MessageBuilder; import org.springframework.stereotype.Service; @@ -9,8 +8,11 @@ @Service public class OrderSender { - @Autowired - private StreamBridge source; + private final StreamBridge source; + + public OrderSender(StreamBridge source) { + this.source = source; + } public boolean send(Order order) { return this.source.send("output", MessageBuilder.withPayload(order).build()); diff --git a/order-service/src/main/java/pl/piomin/services/order/service/OrderService.java b/order-service/src/main/java/pl/piomin/services/order/service/OrderService.java index 4962f5a..df7d57e 100644 --- a/order-service/src/main/java/pl/piomin/services/order/service/OrderService.java +++ b/order-service/src/main/java/pl/piomin/services/order/service/OrderService.java @@ -2,11 +2,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import pl.piomin.services.messaging.Order; import pl.piomin.services.messaging.OrderStatus; @@ -19,10 +17,13 @@ public class OrderService { private ObjectMapper mapper = new ObjectMapper(); - @Autowired - OrderRepository repository; + private final OrderRepository repository; + + public OrderService(OrderRepository repository) { + this.repository = repository; + } - public void process(final Order order) throws JsonProcessingException { + public void process(final Order order) { LOGGER.info("Order processed: {}", mapper.writeValueAsString(order)); Order o = repository.findById(order.getId()); if (o.getStatus() != OrderStatus.REJECTED) { diff --git a/order-service/src/test/java/pl/piomin/services/order/OrderControllerTest.java b/order-service/src/test/java/pl/piomin/services/order/OrderControllerTest.java index edb616d..d405538 100644 --- a/order-service/src/test/java/pl/piomin/services/order/OrderControllerTest.java +++ b/order-service/src/test/java/pl/piomin/services/order/OrderControllerTest.java @@ -2,8 +2,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.testcontainers.containers.RabbitMQContainer; import org.testcontainers.junit.jupiter.Container; @@ -19,6 +20,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers +@AutoConfigureTestRestTemplate public class OrderControllerTest { @Autowired diff --git a/pom.xml b/pom.xml index 0a98890..afb57ad 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.11 + 4.0.3 @@ -26,7 +26,7 @@ org.springframework.cloud spring-cloud-dependencies - 2025.0.0 + 2025.1.1 pom import diff --git a/product-service/src/main/java/pl/piomin/services/product/ProductApplication.java b/product-service/src/main/java/pl/piomin/services/product/ProductApplication.java index 62f28b3..e0f6242 100644 --- a/product-service/src/main/java/pl/piomin/services/product/ProductApplication.java +++ b/product-service/src/main/java/pl/piomin/services/product/ProductApplication.java @@ -1,16 +1,14 @@ package pl.piomin.services.product; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.filter.CommonsRequestLoggingFilter; import pl.piomin.services.messaging.Order; import pl.piomin.services.product.service.ProductService; +import tools.jackson.databind.ObjectMapper; import java.util.function.Consumer; @@ -21,9 +19,12 @@ public class ProductApplication { private ObjectMapper mapper = new ObjectMapper(); - @Autowired - ProductService service; - + private final ProductService service; + + public ProductApplication(ProductService service) { + this.service = service; + } + public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } @@ -31,12 +32,8 @@ public static void main(String[] args) { @Bean public Consumer input() { return order -> { - try { - LOGGER.info("Order received: {}", mapper.writeValueAsString(order)); - service.process(order); - } catch (JsonProcessingException e) { - LOGGER.error("Error deserializing", e); - } + LOGGER.info("Order received: {}", mapper.writeValueAsString(order)); + service.process(order); }; } diff --git a/product-service/src/main/java/pl/piomin/services/product/controller/ProductController.java b/product-service/src/main/java/pl/piomin/services/product/controller/ProductController.java index dba41a8..60ce978 100644 --- a/product-service/src/main/java/pl/piomin/services/product/controller/ProductController.java +++ b/product-service/src/main/java/pl/piomin/services/product/controller/ProductController.java @@ -5,7 +5,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -14,11 +13,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import pl.piomin.services.product.model.Product; import pl.piomin.services.product.repository.ProductRepository; +import tools.jackson.databind.ObjectMapper; @RestController public class ProductController { @@ -27,8 +24,11 @@ public class ProductController { private ObjectMapper mapper = new ObjectMapper(); - @Autowired - ProductRepository repository; + private final ProductRepository repository; + + public ProductController(ProductRepository repository) { + this.repository = repository; + } @PostMapping public Product add(@RequestBody Product product) { @@ -46,7 +46,7 @@ public Product findById(@PathVariable("id") Long id) { } @PostMapping("/ids") - public List find(@RequestBody List ids) throws JsonProcessingException { + public List find(@RequestBody List ids) { List products = repository.find(ids); LOGGER.info("Products found: {}", mapper.writeValueAsString(Collections.singletonMap("count", products.size()))); return products; diff --git a/product-service/src/main/java/pl/piomin/services/product/messaging/OrderSender.java b/product-service/src/main/java/pl/piomin/services/product/messaging/OrderSender.java index e666530..ce04c09 100644 --- a/product-service/src/main/java/pl/piomin/services/product/messaging/OrderSender.java +++ b/product-service/src/main/java/pl/piomin/services/product/messaging/OrderSender.java @@ -1,6 +1,5 @@ package pl.piomin.services.product.messaging; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.function.StreamBridge; import org.springframework.integration.support.MessageBuilder; import org.springframework.stereotype.Service; @@ -9,8 +8,11 @@ @Service public class OrderSender { - @Autowired - private StreamBridge source; + private final StreamBridge source; + + public OrderSender(StreamBridge source) { + this.source = source; + } public boolean send(Order order) { return this.source.send("output", MessageBuilder.withPayload(order).build()); diff --git a/product-service/src/main/java/pl/piomin/services/product/service/ProductService.java b/product-service/src/main/java/pl/piomin/services/product/service/ProductService.java index 629d571..5541da9 100644 --- a/product-service/src/main/java/pl/piomin/services/product/service/ProductService.java +++ b/product-service/src/main/java/pl/piomin/services/product/service/ProductService.java @@ -4,17 +4,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import pl.piomin.services.messaging.Order; import pl.piomin.services.messaging.OrderStatus; import pl.piomin.services.product.messaging.OrderSender; import pl.piomin.services.product.model.Product; import pl.piomin.services.product.repository.ProductRepository; +import tools.jackson.databind.ObjectMapper; @Service public class ProductService { @@ -23,12 +20,15 @@ public class ProductService { private ObjectMapper mapper = new ObjectMapper(); - @Autowired - ProductRepository productRepository; - @Autowired - OrderSender orderSender; + private final ProductRepository productRepository; + private final OrderSender orderSender; + + public ProductService(ProductRepository productRepository, OrderSender orderSender) { + this.productRepository = productRepository; + this.orderSender = orderSender; + } - public void process(final Order order) throws JsonProcessingException { + public void process(final Order order) { LOGGER.info("Order processed: {}", mapper.writeValueAsString(order)); for (Long productId : order.getProductIds()) { Product product = productRepository.findById(productId); diff --git a/product-service/src/test/java/pl/piomin/services/product/OrderReceiverTest.java b/product-service/src/test/java/pl/piomin/services/product/OrderReceiverTest.java index 96ba257..ebfee9b 100644 --- a/product-service/src/test/java/pl/piomin/services/product/OrderReceiverTest.java +++ b/product-service/src/test/java/pl/piomin/services/product/OrderReceiverTest.java @@ -1,7 +1,5 @@ package pl.piomin.services.product; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +12,7 @@ import org.springframework.messaging.Message; import pl.piomin.services.messaging.Order; import pl.piomin.services.messaging.OrderStatus; +import tools.jackson.databind.ObjectMapper; import java.util.Collections; @@ -34,7 +33,7 @@ public class OrderReceiverTest { private ObjectMapper mapper; @Test - public void testProcessing() throws JsonProcessingException { + public void testProcessing() { Order o = new Order(); o.setId(1L); o.setAccountId(1L); diff --git a/readme.md b/readme.md index 7dcbc73..bc19685 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,277 @@ -## Building and testing message-driven microservices using Spring Cloud Stream [![Twitter](https://img.shields.io/twitter/follow/piotr_minkowski.svg?style=social&logo=twitter&label=Follow%20Me)](https://twitter.com/piotr_minkowski) - -[![CircleCI](https://circleci.com/gh/piomin/sample-message-driven-microservices.svg?style=svg)](https://circleci.com/gh/piomin/sample-message-driven-microservices) - -[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-black.svg)](https://sonarcloud.io/dashboard?id=piomin_sample-message-driven-microservices) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-message-driven-microservices&metric=bugs)](https://sonarcloud.io/dashboard?id=piomin_sample-message-driven-microservices) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-message-driven-microservices&metric=coverage)](https://sonarcloud.io/dashboard?id=piomin_sample-message-driven-microservices) -[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-message-driven-microservices&metric=ncloc)](https://sonarcloud.io/dashboard?id=piomin_sample-message-driven-microservices) - -Detailed description can be found here: [Building and testing message-driven microservices using Spring Cloud Stream](https://piotrminkowski.com/2018/06/15/building-and-testing-message-driven-microservices-using-spring-cloud-stream/) +## Building and testing message-driven microservices using Spring Cloud Stream [![Twitter](https://img.shields.io/twitter/follow/piotr_minkowski.svg?style=social&logo=twitter&label=Follow%20Me)](https://twitter.com/piotr_minkowski) + +[![CircleCI](https://circleci.com/gh/piomin/sample-message-driven-microservices.svg?style=svg)](https://circleci.com/gh/piomin/sample-message-driven-microservices) + +[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-black.svg)](https://sonarcloud.io/dashboard?id=piomin_sample-message-driven-microservices) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-message-driven-microservices&metric=bugs)](https://sonarcloud.io/dashboard?id=piomin_sample-message-driven-microservices) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-message-driven-microservices&metric=coverage)](https://sonarcloud.io/dashboard?id=piomin_sample-message-driven-microservices) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=piomin_sample-message-driven-microservices&metric=ncloc)](https://sonarcloud.io/dashboard?id=piomin_sample-message-driven-microservices) + +Detailed description can be found here: [Building and testing message-driven microservices using Spring Cloud Stream](https://piotrminkowski.com/2018/06/15/building-and-testing-message-driven-microservices-using-spring-cloud-stream/) + + +## Table of Contents +- [Architecture Overview](#architecture-overview) +- [Technology Stack](#technology-stack) +- [Services Description](#services-description) +- [Message Flow](#message-flow) +- [Prerequisites](#prerequisites) +- [Running the Applications](#running-the-applications) +- [API Endpoints](#api-endpoints) +- [Testing](#testing) +- [Configuration](#configuration) +- [Monitoring and Management](#monitoring-and-management) + +## Architecture Overview + +This project demonstrates a message-driven microservices architecture using Spring Cloud Stream with RabbitMQ as the message broker. The system consists of three core business services that communicate asynchronously through message queues. + +```mermaid +graph TB + subgraph "Client Layer" + C["Client/API Consumer"] + end + subgraph "Microservices" + OS["Order Service
:8090"] + AS["Account Service
:8091"] + PS["Product Service
:8093"] + end + subgraph "Message Broker" + RMQ["RabbitMQ
:5672"] + OIN["orders-in queue"] + OOUT["orders-out queue"] + end + subgraph "Common" + MC["messaging-common
Shared DTOs"] + end + C --> OS + C --> AS + C --> PS + OS --> OIN + OIN --> AS + OIN --> PS + AS --> OOUT + PS --> OOUT + OOUT --> OS + OS -.-> MC + AS -.-> MC + PS -.-> MC +``` + +## Technology Stack + +- **Java**: 21 +- **Spring Boot**: 3.5.0 +- **Spring Cloud**: 2025.0.0 +- **Spring Cloud Stream**: Message-driven microservices framework +- **RabbitMQ**: Message broker for asynchronous communication +- **Maven**: Build and dependency management +- **Docker & Docker Compose**: Containerization and orchestration +- **Testcontainers**: Integration testing with real dependencies +- **CircleCI**: Continuous Integration +- **SonarCloud**: Code quality and security analysis +- **JaCoCo**: Code coverage analysis + +## Services Description + +### Order Service (port 8090) +The central orchestrator service that manages customer orders. It receives order requests via REST API and publishes order events. + +**Key Features:** +- REST API for order management +- Order event publishing with customer-based partitioning +- Order status tracking and updates + +### Account Service (port 8091) +Manages customer accounts and financial transactions. It listens for order events and processes withdrawals. + +**Key Features:** +- Account management REST API +- Partitioned message consumption +- Account balance withdrawals + +### Product Service (port 8093) +Handles product catalog and inventory. It processes order events to update availability. + +**Key Features:** +- Product management REST API +- Partitioned message consumption +- Inventory tracking + +### Messaging Common +Shared library with common DTOs and enums for consistent message contracts. + +**Components:** +- `Order` – Order data model +- `OrderStatus` – Enumeration (NEW, PROCESSING, ACCEPTED, DONE, REJECTED) + +## Message Flow + +The services communicate through two main message destinations: + +1. **orders-in**: Order Service publishes new orders +2. **orders-out**: Account and Product Services publish processing results + +**Message Flow Pattern:** +``` +1. Client creates order → Order Service +2. Order Service publishes order → orders-in queue +3. Account Service processes withdrawal → orders-out queue +4. Product Service updates inventory → orders-out queue +5. Order Service receives updates → Final order status +``` + +**Partitioning Strategy:** +- Partitioned by `customerId` for ordered processing +- Supports multiple instances per service with load balancing + +## Prerequisites + +- **Java 21** or higher +- **Maven 3.6+** +- **Docker & Docker Compose** +- **Git** + +## Running the Applications + +### 1. Clone the Repository +```bash +git clone https://github.com/piomin/sample-message-driven-microservices.git +cd sample-message-driven-microservices +``` + +### 2. Start RabbitMQ +```bash +docker-compose up -d +``` +- RabbitMQ AMQP on 5672 +- Management UI on http://localhost:15672 (guest/guest) + +### 3. Build the Project +```bash +mvn clean compile +``` + +### 4. Start the Services +**Option A: Separate terminals** +```bash +cd order-service && mvn spring-boot:run +cd account-service && mvn spring-boot:run +cd product-service && mvn spring-boot:run +``` +**Option B: Background** +```bash +cd order-service && mvn spring-boot:run & +cd account-service && mvn spring-boot:run & +cd product-service && mvn spring-boot:run & +``` + +### 5. Verify Health +```bash +curl http://localhost:8090/actuator/health +curl http://localhost:8091/actuator/health +curl http://localhost:8093/actuator/health +``` + +### Multiple Instances (Partitioned) +```bash +SPRING_PROFILES_ACTIVE=instance1 mvn spring-boot:run -pl account-service +SPRING_PROFILES_ACTIVE=instance2 mvn spring-boot:run -pl account-service + +SPRING_PROFILES_ACTIVE=instance1 mvn spring-boot:run -pl product-service +SPRING_PROFILES_ACTIVE=instance2 mvn spring-boot:run -pl product-service +``` + +## API Endpoints + +### Order Service (http://localhost:8090) +| Method | Endpoint | Description | +|--------|---------------------------------|----------------------------| +| POST | `/orders` | Create a new order | +| PUT | `/orders` | Update an order | +| GET | `/orders/{id}` | Get order by ID | +| GET | `/orders/customer/{customerId}` | Get orders by customer ID | + +### Account Service (http://localhost:8091) +| Method | Endpoint | Description | +|--------|------------------------------------------|-------------------------------| +| POST | `/accounts` | Create a new account | +| PUT | `/accounts` | Update an account | +| PUT | `/accounts/withdraw/{id}/{amount}` | Withdraw amount from account | +| GET | `/accounts/{id}` | Get account by ID | +| GET | `/accounts/customer/{customerId}` | Get accounts by customer ID | + +### Product Service (http://localhost:8093) +| Method | Endpoint | Description | +|--------|----------------------|-------------------------| +| POST | `/products` | Create a new product | +| PUT | `/products` | Update a product | +| GET | `/products/{id}` | Get product by ID | +| GET | `/products/ids` | Get products by IDs | + +### Example API Usage +```bash +curl -X POST http://localhost:8091/accounts \ + -H "Content-Type: application/json" \ + -d '{"customerId":1,"balance":1000}' + +curl -X POST http://localhost:8093/products \ + -H "Content-Type: application/json" \ + -d '{"name":"Sample","price":50,"count":100}' + +curl -X POST http://localhost:8090/orders \ + -H "Content-Type: application/json" \ + -d '{"customerId":1,"productIds":[1],"price":50}' +``` + +## Testing + +### Running Tests +```bash +mvn test +mvn test -pl order-service +mvn test -pl account-service +mvn test -pl product-service +``` + +### Test Structure +- `OrderControllerTest` – Order Service REST endpoints +- `OrderReceiverTest` – Account Service message handling +- `OrderReceiverTest` – Product Service message handling + +### Code Coverage +```bash +mvn jacoco:report +``` +Coverage reports in `target/site/jacoco/index.html` per module. + +## Configuration + +### Environment Variables +| Variable | Default | Description | +|--------------------------|--------------------|------------------------------------| +| `PORT` | service-specific | Override default service port | +| `SPRING_PROFILES_ACTIVE` | | Activate specific Spring profiles | + +### RabbitMQ Configuration +- Host: localhost +- Port: 5672 +- Username: guest +- Password: guest + +### Service Discovery +Eureka available at http://localhost:8761/eureka/ (optional) + +### Message Destinations +- **orders-in**: Direct exchange for new orders +- **orders-out**: Topic exchange for processing results +- **Partitioning**: 2 partitions by `customerId` + +## Monitoring and Management + +All services expose Actuator endpoints: +- Health: `/actuator/health` +- Metrics: `/actuator/metrics` +- Stream bindings: `/actuator/bindings` + +**RabbitMQ Management UI:** http://localhost:15672 (guest/guest)