Spring Microservices

1. Service Discovery (Eureka)

Eureka Server

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

// application.yml
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
  server:
    waitTimeInMsWhenSyncEmpty: 0

Eureka Client

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

// application.yml
spring:
  application:
    name: user-service
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    preferIpAddress: true

2. API Gateway (Spring Cloud Gateway)

Gateway Configuration

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user_service", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .rewritePath("/api/users/(?<segment>.*)", "/${segment}")
                    .addRequestHeader("X-Gateway-Request", "true")
                    .circuitBreaker(config -> config
                        .setName("userServiceCircuitBreaker")
                        .setFallbackUri("forward:/fallback/users")))
                .uri("lb://user-service"))
            .route("order_service", r -> r
                .path("/api/orders/**")
                .filters(f -> f
                    .rewritePath("/api/orders/(?<segment>.*)", "/${segment}")
                    .retry(config -> config
                        .setRetries(3)
                        .setMethods(HttpMethod.GET)))
                .uri("lb://order-service"))
            .build();
    }

    @Bean
    public CircuitBreakerFactory circuitBreakerFactory() {
        Customizer<Resilience4JCircuitBreakerFactory> customizer = 
            factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .circuitBreakerConfig(CircuitBreakerConfig.custom()
                    .slidingWindowSize(10)
                    .failureRateThreshold(50)
                    .waitDurationInOpenState(Duration.ofSeconds(10))
                    .build())
                .timeLimiterConfig(TimeLimiterConfig.custom()
                    .timeoutDuration(Duration.ofSeconds(2))
                    .build())
                .build());

        return new Resilience4JCircuitBreakerFactory(customizer);
    }
}

Fallback Controller

@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @GetMapping("/users")
    public ResponseEntity<Map<String, String>> userServiceFallback() {
        Map<String, String> response = new HashMap<>();
        response.put("message", "User Service is currently unavailable");
        response.put("timestamp", Instant.now().toString());

        return ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(response);
    }
}

3. Config Server

Config Server Setup

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

// application.yml
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/your-org/config-repo
          searchPaths: '{application}'
          default-label: main
        encrypt:
          enabled: true

Config Client

@SpringBootApplication
@EnableConfigurationProperties
public class ServiceApplication {

    @Value("${custom.property}")
    private String customProperty;

    @RefreshScope
    @RestController
    class ConfigController {

        @GetMapping("/config")
        public Map<String, String> getConfig() {
            return Map.of("customProperty", customProperty);
        }
    }
}

// bootstrap.yml
spring:
  application:
    name: user-service
  cloud:
    config:
      uri: http://localhost:8888
      fail-fast: true
      retry:
        max-attempts: 6
        initial-interval: 1000
        max-interval: 2000
        multiplier: 1.1

4. Circuit Breaker (Resilience4j)

Circuit Breaker Configuration

@Configuration
public class Resilience4jConfig {

    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)
            .waitDurationInOpenState(Duration.ofMillis(1000))
            .slidingWindowSize(2)
            .build();

        return CircuitBreakerRegistry.of(circuitBreakerConfig);
    }

    @Bean
    public TimeLimiterRegistry timeLimiterRegistry() {
        TimeLimiterConfig config = TimeLimiterConfig.custom()
            .timeoutDuration(Duration.ofSeconds(2))
            .build();

        return TimeLimiterRegistry.of(config);
    }

    @Bean
    public Retry retry() {
        RetryConfig config = RetryConfig.custom()
            .maxAttempts(3)
            .waitDuration(Duration.ofMillis(100))
            .retryExceptions(Exception.class)
            .build();

        return RetryRegistry.of(config).retry("default");
    }
}

Service Implementation

@Service
public class UserService {

    private final CircuitBreaker circuitBreaker;
    private final TimeLimiter timeLimiter;
    private final Retry retry;

    public UserService(CircuitBreakerRegistry circuitBreakerRegistry,
                      TimeLimiterRegistry timeLimiterRegistry,
                      Retry retry) {
        this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("userService");
        this.timeLimiter = timeLimiterRegistry.timeLimiter("userService");
        this.retry = retry;
    }

    public User getUser(Long id) {
        return CircuitBreaker.decorateSupplier(circuitBreaker,
            () -> retry.executeSupplier(
                () -> userRepository.findById(id)
                    .orElseThrow(() -> new UserNotFoundException(id))
            )
        ).get();
    }

    public CompletableFuture<User> getUserAsync(Long id) {
        return TimeLimiter.decorateFutureSupplier(timeLimiter,
            () -> CompletableFuture.supplyAsync(
                () -> getUser(id)
            )
        );
    }
}

5. Distributed Tracing (Sleuth & Zipkin)

Tracing Configuration

@Configuration
public class TracingConfig {

    @Bean
    public Sampler defaultSampler() {
        return Sampler.ALWAYS_SAMPLE;
    }

    @Bean
    public SpanCustomizer spanCustomizer(Tracer tracer) {
        return tracer.currentSpan();
    }
}

Service with Tracing

@Service
public class OrderService {

    private final Tracer tracer;
    private final UserClient userClient;

    public OrderService(Tracer tracer, UserClient userClient) {
        this.tracer = tracer;
        this.userClient = userClient;
    }

    public Order createOrder(OrderRequest request) {
        Span span = tracer.nextSpan().name("createOrder");

        try (Tracer.SpanInScope ws = tracer.withSpanInScope(span.start())) {
            span.tag("userId", request.getUserId().toString());

            // Validate user
            User user = userClient.getUser(request.getUserId());
            span.tag("userStatus", user.getStatus().toString());

            // Create order
            Order order = new Order();
            order.setUserId(user.getId());
            order.setItems(request.getItems());
            order.setStatus(OrderStatus.CREATED);

            return orderRepository.save(order);
        } finally {
            span.finish();
        }
    }
}

6. Best Practices

1. Service Communication

@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {

    @GetMapping("/users/{id}")
    User getUser(@PathVariable("id") Long id);

    @PostMapping("/users")
    User createUser(@RequestBody User user);
}

@Component
public class UserClientFallback implements UserClient {

    @Override
    public User getUser(Long id) {
        return new User(); // Default user or throw exception
    }

    @Override
    public User createUser(User user) {
        throw new ServiceUnavailableException("User service is down");
    }
}

2. Service Registry

@Configuration
public class ServiceRegistryConfig {

    @Bean
    public EurekaInstanceConfigBean eurekaInstanceConfig(
            InetUtils inetUtils,
            @Value("${spring.application.name}") String appName) {
        EurekaInstanceConfigBean config = new EurekaInstanceConfigBean(inetUtils);
        AmazonInfo info = AmazonInfo.Builder.newBuilder().autoBuild("eureka");
        config.setDataCenterInfo(info);
        config.setHostname(info.get(AmazonInfo.MetaDataKey.publicHostname));
        config.setIpAddress(info.get(AmazonInfo.MetaDataKey.publicIpv4));
        config.setNonSecurePort(8080);
        config.setAppname(appName);
        config.setInstanceId(info.get(AmazonInfo.MetaDataKey.instanceId));
        return config;
    }
}

3. API Versioning

@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {

    @GetMapping("/{id}")
    public UserV1 getUserV1(@PathVariable Long id) {
        return userService.getUserV1(id);
    }
}

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {

    @GetMapping("/{id}")
    public UserV2 getUserV2(@PathVariable Long id) {
        return userService.getUserV2(id);
    }
}

4. Security

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/actuator/**").hasRole("ADMIN")
                .antMatchers("/api/v1/**").authenticated()
                .anyRequest().permitAll()
            .and()
            .oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthoritiesClaimName("roles");
        converter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
        jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
        return jwtConverter;
    }
}

5. Monitoring

@Configuration
public class MonitoringConfig {

    @Bean
    public MeterRegistry meterRegistry() {
        return new SimpleMeterRegistry();
    }

    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }

    @Bean
    public CountedAspect countedAspect(MeterRegistry registry) {
        return new CountedAspect(registry);
    }
}

@Service
public class MetricsService {

    private final MeterRegistry registry;

    public MetricsService(MeterRegistry registry) {
        this.registry = registry;
    }

    @Timed(value = "service.operation.time", description = "Time taken to execute service operation")
    @Counted(value = "service.operation.count", description = "Number of service operations")
    public void performOperation() {
        // Business logic
    }

    public void recordMetric(String name, double value, String... tags) {
        registry.gauge(name, value);
    }

    public void incrementCounter(String name, String... tags) {
        registry.counter(name, tags).increment();
    }
}