google-site-verification: google61fe8ba583a51912.html
Microservices with Spring Boot: Complete Guide

This comprehensive guide covers building microservices with Spring Boot, including service discovery, API gateway, configuration management, and inter-service communication.

1. Service Discovery with Eureka

Eureka Server

Dependencies (pom.xml)

<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2022.0.4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

Eureka Server Application

@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }

Application Configuration (application.yml)

server: port: 8761 spring: application: name: eureka-server security: user: name: admin password: ${EUREKA_PASSWORD:admin123} eureka: client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@localhost:8761/eureka/ server: enable-self-preservation: true eviction-interval-timer-in-ms: 15000 renewal-percent-threshold: 0.85 management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always

Security Configuration

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeHttpRequests(authz -> authz .anyRequest().authenticated() ) .httpBasic(); return http.build(); } }

2. API Gateway with Spring Cloud Gateway

API Gateway Service

Dependencies

<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> </dependency> </dependencies>

Gateway Application

@SpringBootApplication @EnableDiscoveryClient public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } }

Gateway Configuration (application.yml)

server: port: 8080 spring: application: name: api-gateway cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true routes: - id: user-service uri: lb://user-service predicates: - Path=/api/users/** filters: - name: CircuitBreaker args: name: userService fallbackUri: forward:/fallback/user-service - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 - StripPrefix=1 - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: - name: CircuitBreaker args: name: orderService fallbackUri: forward:/fallback/order-service - StripPrefix=1 - id: product-service uri: lb://product-service predicates: - Path=/api/products/** filters: - StripPrefix=1 default-filters: - name: GlobalFilter - name: RequestLoggingFilter eureka: client: service-url: defaultZone: http://admin:admin123@localhost:8761/eureka/ instance: prefer-ip-address: true resilience4j: circuitbreaker: instances: userService: register-health-indicator: true sliding-window-size: 10 minimum-number-of-calls: 5 permitted-number-of-calls-in-half-open-state: 3 wait-duration-in-open-state: 10s failure-rate-threshold: 50 management: endpoints: web: exposure: include: gateway,health,metrics endpoint: gateway: enabled: true health: show-details: always logging: level: org.springframework.cloud.gateway: DEBUG

Custom Global Filter

@Component public class GlobalFilter implements GlobalFilter, Ordered { private static final Logger logger = LoggerFactory.getLogger(GlobalFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); // Log incoming request logger.info("Incoming request: {} {}", request.getMethod(), request.getURI()); // Add correlation ID if not present String correlationId = request.getHeaders().getFirst("X-Correlation-ID"); if (correlationId == null) { correlationId = UUID.randomUUID().toString(); } ServerHttpRequest modifiedRequest = request.mutate() .header("X-Correlation-ID", correlationId) .header("X-Gateway-Timestamp", String.valueOf(System.currentTimeMillis())) .build(); // Add correlation ID to response headers ServerHttpResponse response = exchange.getResponse(); response.getHeaders().add("X-Correlation-ID", correlationId); return chain.filter(exchange.mutate().request(modifiedRequest).build()); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } } @Component public class RequestLoggingFilter implements GatewayFilter, Ordered { private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { long startTime = System.currentTimeMillis(); return chain.filter(exchange).then(Mono.fromRunnable(() -> { long duration = System.currentTimeMillis() - startTime; ServerHttpResponse response = exchange.getResponse(); ServerHttpRequest request = exchange.getRequest(); logger.info("Request completed: {} {} - Status: {} - Duration: {}ms", request.getMethod(), request.getURI(), response.getStatusCode(), duration); })); } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }

Fallback Controller

@RestController public class FallbackController { @GetMapping("/fallback/user-service") public ResponseEntity<Map<String, Object>> userServiceFallback() { Map<String, Object> response = new HashMap<>(); response.put("timestamp", LocalDateTime.now()); response.put("message", "User service is temporarily unavailable. Please try again later."); response.put("status", 503); return ResponseEntity.status(503).body(response); } @GetMapping("/fallback/order-service") public ResponseEntity<Map<String, Object>> orderServiceFallback() { Map<String, Object> response = new HashMap<>(); response.put("timestamp", LocalDateTime.now()); response.put("message", "Order service is temporarily unavailable. Please try again later."); response.put("status", 503); return ResponseEntity.status(503).body(response); } }

3. User Service Implementation

User Service Domain

Dependencies

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.1.0</version> </dependency> </dependencies>

User Entity

@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String userId; @Column(nullable = false) private String firstName; @Column(nullable = false) private String lastName; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String phone; @Enumerated(EnumType.STRING) @Column(nullable = false) private UserStatus status; @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; // Constructors public User() {} public User(String firstName, String lastName, String email, String phone) { this.userId = UUID.randomUUID().toString(); this.firstName = firstName; this.lastName = lastName; this.email = email; this.phone = phone; this.status = UserStatus.ACTIVE; } // Business methods public void deactivate() { this.status = UserStatus.INACTIVE; this.updatedAt = LocalDateTime.now(); } public void activate() { this.status = UserStatus.ACTIVE; this.updatedAt = LocalDateTime.now(); } // Getters and setters public Long getId() { return id; } public String getUserId() { return userId; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public UserStatus getStatus() { return status; } public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } } enum UserStatus { ACTIVE, INACTIVE, SUSPENDED }

Repository

@Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUserId(String userId); Optional<User> findByEmail(String email); List<User> findByStatus(UserStatus status); boolean existsByEmail(String email); @Query("SELECT u FROM User u WHERE u.firstName LIKE %:name% OR u.lastName LIKE %:name%") List<User> findByNameContaining(@Param("name") String name); }

Service Layer

public interface UserService { UserDTO createUser(CreateUserRequest request); UserDTO getUserById(String userId); UserDTO getUserByEmail(String email); List<UserDTO> getAllUsers(); List<UserDTO> getUsersByStatus(UserStatus status); UserDTO updateUser(String userId, UpdateUserRequest request); void deleteUser(String userId); void deactivateUser(String userId); void activateUser(String userId); } @Service @Transactional public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; public UserServiceImpl(UserRepository userRepository, UserMapper userMapper) { this.userRepository = userRepository; this.userMapper = userMapper; } @Override public UserDTO createUser(CreateUserRequest request) { // Validate email uniqueness if (userRepository.existsByEmail(request.getEmail())) { throw new UserAlreadyExistsException("User with email " + request.getEmail() + " already exists"); } User user = new User( request.getFirstName(), request.getLastName(), request.getEmail(), request.getPhone() ); User savedUser = userRepository.save(user); return userMapper.toDTO(savedUser); } @Override public UserDTO getUserById(String userId) { User user = userRepository.findByUserId(userId) .orElseThrow(() -> new UserNotFoundException("User not found with ID: " + userId)); return userMapper.toDTO(user); } @Override public UserDTO getUserByEmail(String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new UserNotFoundException("User not found with email: " + email)); return userMapper.toDTO(user); } @Override public List<UserDTO> getAllUsers() { return userRepository.findAll().stream() .map(userMapper::toDTO) .collect(Collectors.toList()); } @Override public List<UserDTO> getUsersByStatus(UserStatus status) { return userRepository.findByStatus(status).stream() .map(userMapper::toDTO) .collect(Collectors.toList()); } @Override public UserDTO updateUser(String userId, UpdateUserRequest request) { User user = userRepository.findByUserId(userId) .orElseThrow(() -> new UserNotFoundException("User not found with ID: " + userId)); user.setFirstName(request.getFirstName()); user.setLastName(request.getLastName()); user.setPhone(request.getPhone()); User updatedUser = userRepository.save(user); return userMapper.toDTO(updatedUser); } @Override public void deleteUser(String userId) { User user = userRepository.findByUserId(userId) .orElseThrow(() -> new UserNotFoundException("User not found with ID: " + userId)); userRepository.delete(user); } @Override public void deactivateUser(String userId) { User user = userRepository.findByUserId(userId) .orElseThrow(() -> new UserNotFoundException("User not found with ID: " + userId)); user.deactivate(); userRepository.save(user); } @Override public void activateUser(String userId) { User user = userRepository.findByUserId(userId) .orElseThrow(() -> new UserNotFoundException("User not found with ID: " + userId)); user.activate(); userRepository.save(user); } }

Controller

@RestController @RequestMapping("/api/users") @Validated public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) { UserDTO user = userService.createUser(request); return ResponseEntity.status(HttpStatus.CREATED).body(user); } @GetMapping("/{userId}") public ResponseEntity<UserDTO> getUser(@PathVariable String userId) { UserDTO user = userService.getUserById(userId); return ResponseEntity.ok(user); } @GetMapping public ResponseEntity<List<UserDTO>> getAllUsers( @RequestParam(required = false) UserStatus status) { List<UserDTO> users; if (status != null) { users = userService.getUsersByStatus(status); } else { users = userService.getAllUsers(); } return ResponseEntity.ok(users); } @PutMapping("/{userId}") public ResponseEntity<UserDTO> updateUser( @PathVariable String userId, @Valid @RequestBody UpdateUserRequest request) { UserDTO user = userService.updateUser(userId, request); return ResponseEntity.ok(user); } @DeleteMapping("/{userId}") public ResponseEntity<Void> deleteUser(@PathVariable String userId) { userService.deleteUser(userId); return ResponseEntity.noContent().build(); } @PatchMapping("/{userId}/deactivate") public ResponseEntity<Void> deactivateUser(@PathVariable String userId) { userService.deactivateUser(userId); return ResponseEntity.ok().build(); } @PatchMapping("/{userId}/activate") public ResponseEntity<Void> activateUser(@PathVariable String userId) { userService.activateUser(userId); return ResponseEntity.ok().build(); } }

DTOs and Request/Response Classes

public class UserDTO { private String userId; private String firstName; private String lastName; private String email; private String phone; private UserStatus status; private LocalDateTime createdAt; private LocalDateTime updatedAt; // Constructors, getters, and setters public UserDTO() {} public UserDTO(String userId, String firstName, String lastName, String email, String phone, UserStatus status, LocalDateTime createdAt, LocalDateTime updatedAt) { this.userId = userId; this.firstName = firstName; this.lastName = lastName; this.email = email; this.phone = phone; this.status = status; this.createdAt = createdAt; this.updatedAt = updatedAt; } // Getters and setters... } public class CreateUserRequest { @NotBlank(message = "First name is required") @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters") private String firstName; @NotBlank(message = "Last name is required") @Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters") private String lastName; @NotBlank(message = "Email is required") @Email(message = "Invalid email format") private String email; @NotBlank(message = "Phone number is required") @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number format") private String phone; // Constructors, getters, and setters... } public class UpdateUserRequest { @NotBlank(message = "First name is required") @Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters") private String firstName; @NotBlank(message = "Last name is required") @Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters") private String lastName; @NotBlank(message = "Phone number is required") @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number format") private String phone; // Constructors, getters, and setters... }

Application Configuration

@SpringBootApplication @EnableDiscoveryClient @EnableJpaAuditing public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } @Bean public UserMapper userMapper() { return new UserMapperImpl(); } }

Application Properties (application.yml)

server: port: 8081 spring: application: name: user-service datasource: url: jdbc:postgresql://localhost:5432/userdb username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:password} driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true h2: console: enabled: true eureka: client: service-url: defaultZone: http://admin:admin123@localhost:8761/eureka/ instance: prefer-ip-address: true instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always show-components: always logging: level: com.example.userservice: DEBUG org.hibernate.SQL: DEBUG

4. Order Service with Feign Client

Order Service Implementation

Order Entity and Domain

@Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String orderId; @Column(nullable = false) private String userId; @Enumerated(EnumType.STRING) @Column(nullable = false) private OrderStatus status; @Column(nullable = false) private BigDecimal totalAmount; @Column(nullable = false) private String currency; @Embedded private Address shippingAddress; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<OrderItem> items = new ArrayList<>(); @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; // Constructors and methods public Order() { this.orderId = UUID.randomUUID().toString(); this.status = OrderStatus.PENDING; this.currency = "USD"; } public Order(String userId, Address shippingAddress) { this(); this.userId = userId; this.shippingAddress = shippingAddress; } public void addItem(OrderItem item) { item.setOrder(this); this.items.add(item); calculateTotal(); } public void removeItem(OrderItem item) { this.items.remove(item); item.setOrder(null); calculateTotal(); } private void calculateTotal() { this.totalAmount = items.stream() .map(OrderItem::getItemTotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } public void confirm() { if (this.status != OrderStatus.PENDING) { throw new IllegalOrderStateException("Only pending orders can be confirmed"); } this.status = OrderStatus.CONFIRMED; } public void ship() { if (this.status != OrderStatus.CONFIRMED) { throw new IllegalOrderStateException("Only confirmed orders can be shipped"); } this.status = OrderStatus.SHIPPED; } public void deliver() { if (this.status != OrderStatus.SHIPPED) { throw new IllegalOrderStateException("Only shipped orders can be delivered"); } this.status = OrderStatus.DELIVERED; } public void cancel() { if (this.status == OrderStatus.DELIVERED || this.status == OrderStatus.CANCELLED) { throw new IllegalOrderStateException("Cannot cancel order in current state: " + this.status); } this.status = OrderStatus.CANCELLED; } // Getters and setters... } @Entity @Table(name = "order_items") public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id", nullable = false) private Order order; @Column(nullable = false) private String productId; @Column(nullable = false) private String productName; @Column(nullable = false) private BigDecimal unitPrice; @Column(nullable = false) private Integer quantity; // Constructors and methods public OrderItem() {} public OrderItem(String productId, String productName, BigDecimal unitPrice, Integer quantity) { this.productId = productId; this.productName = productName; this.unitPrice = unitPrice; this.quantity = quantity; } public BigDecimal getItemTotal() { return unitPrice.multiply(BigDecimal.valueOf(quantity)); } // Getters and setters... } @Embeddable public class Address { @Column(nullable = false) private String street; @Column(nullable = false) private String city; @Column(nullable = false) private String state; @Column(nullable = false) private String zipCode; @Column(nullable = false) private String country; // Constructors, getters, and setters... } enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }

Feign Client for User Service

@FeignClient(name = "user-service", path = "/api/users") public interface UserServiceClient { @GetMapping("/{userId}") ResponseEntity<UserDTO> getUserById(@PathVariable("userId") String userId); @GetMapping("/email/{email}") ResponseEntity<UserDTO> getUserByEmail(@PathVariable("email") String email); @GetMapping("/{userId}/exists") ResponseEntity<Boolean> userExists(@PathVariable("userId") String userId); } // Feign configuration @Configuration public class FeignConfig { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } @Bean public ErrorDecoder errorDecoder() { return new CustomErrorDecoder(); } } @Component public class CustomErrorDecoder implements ErrorDecoder { private final ErrorDecoder defaultErrorDecoder = new Default(); @Override public Exception decode(String methodKey, Response response) { if (response.status() == 404) { return new UserNotFoundException("User not found"); } if (response.status() >= 400 && response.status() <= 499) { return new FeignClientException("Client error occurred: " + response.status()); } if (response.status() >= 500) { return new FeignServerException("Server error occurred: " + response.status()); } return defaultErrorDecoder.decode(methodKey, response); } }

Order Service Implementation

@Service @Transactional public class OrderService { private final OrderRepository orderRepository; private final UserServiceClient userServiceClient; public OrderService(OrderRepository orderRepository, UserServiceClient userServiceClient) { this.orderRepository = orderRepository; this.userServiceClient = userServiceClient; } public OrderDTO createOrder(CreateOrderRequest request) { // Validate user exists validateUserExists(request.getUserId()); Order order = new Order(request.getUserId(), request.getShippingAddress()); // Add items to order request.getItems().forEach(itemRequest -> { OrderItem item = new OrderItem( itemRequest.getProductId(), itemRequest.getProductName(), itemRequest.getUnitPrice(), itemRequest.getQuantity() ); order.addItem(item); }); Order savedOrder = orderRepository.save(order); return OrderMapper.toDTO(savedOrder); } public OrderDTO getOrder(String orderId) { Order order = orderRepository.findByOrderId(orderId) .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId)); return OrderMapper.toDTO(order); } public List<OrderDTO> getOrdersByUser(String userId) { validateUserExists(userId); return orderRepository.findByUserId(userId).stream() .map(OrderMapper::toDTO) .collect(Collectors.toList()); } public OrderDTO updateOrderStatus(String orderId, OrderStatus newStatus) { Order order = orderRepository.findByOrderId(orderId) .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId)); switch (newStatus) { case CONFIRMED: order.confirm(); break; case SHIPPED: order.ship(); break; case DELIVERED: order.deliver(); break; case CANCELLED: order.cancel(); break; default: throw new IllegalArgumentException("Invalid status: " + newStatus); } Order updatedOrder = orderRepository.save(order); return OrderMapper.toDTO(updatedOrder); } private void validateUserExists(String userId) { try { ResponseEntity<Boolean> response = userServiceClient.userExists(userId); if (!Boolean.TRUE.equals(response.getBody())) { throw new UserNotFoundException("User not found: " + userId); } } catch (FeignClientException e) { throw new UserNotFoundException("User not found: " + userId); } } }

Order Controller

@RestController @RequestMapping("/api/orders") @Validated public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping public ResponseEntity<OrderDTO> createOrder(@Valid @RequestBody CreateOrderRequest request) { OrderDTO order = orderService.createOrder(request); return ResponseEntity.status(HttpStatus.CREATED).body(order); } @GetMapping("/{orderId}") public ResponseEntity<OrderDTO> getOrder(@PathVariable String orderId) { OrderDTO order = orderService.getOrder(orderId); return ResponseEntity.ok(order); } @GetMapping("/user/{userId}") public ResponseEntity<List<OrderDTO>> getUserOrders(@PathVariable String userId) { List<OrderDTO> orders = orderService.getOrdersByUser(userId); return ResponseEntity.ok(orders); } @PatchMapping("/{orderId}/status") public ResponseEntity<OrderDTO> updateOrderStatus( @PathVariable String orderId, @RequestParam OrderStatus status) { OrderDTO order = orderService.updateOrderStatus(orderId, status); return ResponseEntity.ok(order); } }

5. Configuration Service

Spring Cloud Config Server

Config Server Application

@SpringBootApplication @EnableConfigServer public class ConfigServerApplication { public static void main(String[] args) { SpringApplication.run(ConfigServerApplication.class, args); } }

Configuration (application.yml)

server: port: 8888 spring: application: name: config-server cloud: config: server: git: uri: https://github.com/your-org/config-repo search-paths: '{application}' default-label: main clone-on-start: true force-pull: true management: endpoints: web: exposure: include: health,metrics,bus-refresh endpoint: health: show-details: always encrypt: key: ${CONFIG_SERVER_ENCRYPT_KEY:default-key}

6. Docker Configuration

Docker Compose for Microservices

version: '3.8' services: eureka-server: build: ./eureka-server ports: - "8761:8761" environment: - EUREKA_PASSWORD=admin123 networks: - microservices-network config-server: build: ./config-server ports: - "8888:8888" environment: - CONFIG_SERVER_ENCRYPT_KEY=my-secret-key depends_on: - eureka-server networks: - microservices-network api-gateway: build: ./api-gateway ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=docker - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://admin:admin123@eureka-server:8761/eureka/ depends_on: - eureka-server - config-server networks: - microservices-network user-service: build: ./user-service ports: - "8081:8081" environment: - SPRING_PROFILES_ACTIVE=docker - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://admin:admin123@eureka-server:8761/eureka/ - SPRING_CLOUD_CONFIG_URI=http://config-server:8888 - DB_HOST=postgres-db - DB_USERNAME=postgres - DB_PASSWORD=password depends_on: - eureka-server - config-server - postgres-db networks: - microservices-network order-service: build: ./order-service ports: - "8082:8082" environment: - SPRING_PROFILES_ACTIVE=docker - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://admin:admin123@eureka-server:8761/eureka/ - SPRING_CLOUD_CONFIG_URI=http://config-server:8888 - DB_HOST=postgres-db - DB_USERNAME=postgres - DB_PASSWORD=password depends_on: - eureka-server - config-server - postgres-db networks: - microservices-network postgres-db: image: postgres:14 environment: - POSTGRES_DB=microservices - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data networks: - microservices-network redis: image: redis:7-alpine ports: - "6379:6379" networks: - microservices-network volumes: postgres_data: networks: microservices-network: driver: bridge

Dockerfile for Services

# Dockerfile for Spring Boot microservices FROM eclipse-temurin:17-jre-alpine RUN addgroup -S spring && adduser -S spring -G spring USER spring:spring ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java", "-jar", "/app.jar"]

7. Testing Microservices

Integration Tests

@SpringBootTest @ActiveProfiles("test") class UserServiceIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Autowired private UserRepository userRepository; @Test void shouldCreateUser() { // Given CreateUserRequest request = new CreateUserRequest( "John", "Doe", "[email protected]", "+1234567890" ); // When ResponseEntity<UserDTO> response = restTemplate.postForEntity( "/api/users", request, UserDTO.class); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().getEmail()).isEqualTo("[email protected]"); } @Test void shouldGetUserById() { // Given User user = new User("Jane", "Smith", "[email protected]", "+0987654321"); userRepository.save(user); // When ResponseEntity<UserDTO> response = restTemplate.getForEntity( "/api/users/" + user.getUserId(), UserDTO.class); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().getUserId()).isEqualTo(user.getUserId()); } } @SpringBootTest @ActiveProfiles("test") class OrderServiceIntegrationTest { @MockBean private UserServiceClient userServiceClient; @Test void shouldCreateOrderWhenUserExists() { // Given when(userServiceClient.userExists(anyString())).thenReturn(ResponseEntity.ok(true)); CreateOrderRequest request = new CreateOrderRequest( "user-123", new Address("123 Main St", "New York", "NY", "10001", "USA"), List.of(new OrderItemRequest("prod-1", "Laptop", new BigDecimal("999.99"), 1)) ); // When ResponseEntity<OrderDTO> response = restTemplate.postForEntity( "/api/orders", request, OrderDTO.class); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().getTotalAmount()).isEqualByComparingTo("999.99"); } }

This comprehensive microservices architecture with Spring Boot provides a solid foundation for building scalable, resilient, and maintainable distributed systems. Each service is independently deployable and follows microservices best practices.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper