DEV Community

Cover image for Spring Cloud Microservices Architecture: Complete Service Discovery and Configuration Management Guide
Aarav Joshi
Aarav Joshi

Posted on

Spring Cloud Microservices Architecture: Complete Service Discovery and Configuration Management Guide

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Building microservices means thinking about your application as a collection of independent services. Each service runs its own process and communicates over a network. This approach brings flexibility and scalability, but it also introduces complexity. You need to manage service discovery, configuration, and communication in a way that keeps the system reliable. I have found that using Spring Boot with Spring Cloud provides a powerful toolkit for handling these distributed system challenges.

Service discovery is fundamental. In a dynamic environment, services come and go. Their network locations change. Hard-coding IP addresses or hostnames is a recipe for failure. Instead, services should register themselves with a discovery server when they start up. Other services can then query this registry to find available instances.

Spring Cloud Netflix Eureka offers a simple solution. You run a Eureka server as a separate application. Your microservices act as Eureka clients. They register with the server and send heartbeats to show they are still alive. When one service needs to call another, it asks Eureka for the current list of healthy instances.

Setting up a Eureka server is straightforward. You create a new Spring Boot application and add the @EnableEurekaServer annotation. The server maintains a registry of client applications and their status.

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

Your microservices then become Eureka clients. They need the @EnableEurekaClient annotation and some configuration pointing to the server's location. This allows them to register upon startup.

@SpringBootApplication
@EnableEurekaClient
public class InventoryServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(InventoryServiceApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

Client configuration typically lives in the application.yml file. You specify the Eureka server's URL and how the client should identify itself.

eureka:
  client:
    serviceUrl:
      defaultZone: http://eureka-server:8761/eureka/
  instance:
    instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}
Enter fullscreen mode Exit fullscreen mode

When you need to call another service, you use the discovery client. Spring's RestTemplate or WebClient can be configured to use service names instead of hardcoded URLs. The framework handles the lookup and load balancing between available instances.

@Service
public class OrderServiceClient {

    private final RestTemplate restTemplate;

    public OrderServiceClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public Order getOrder(String orderId) {
        return restTemplate.getForObject("http://order-service/orders/{id}", Order.class, orderId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuration management is another critical piece. In a distributed system, managing configuration files for each service individually becomes unmanageable. You need a central place to store configuration that all services can access. This allows you to change settings across your entire system without redeploying every service.

Spring Cloud Config Server provides this central configuration service. It can pull configuration from a Git repository, a database, or other sources. Each service connects to the config server at startup to get its configuration.

To create a config server, you start a new Spring Boot application with the @EnableConfigServer annotation. You configure it to point to your desired backend, like a Git repository.

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

The server's configuration specifies the Git repository location and other settings. You can use placeholders for different environments.

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/your-org/configuration-repo
          search-paths: '{application}'
Enter fullscreen mode Exit fullscreen mode

Client services use a bootstrap.yml file instead of application.yml for their initial configuration. This file tells them how to find the config server. The bootstrap context loads before the main application context, ensuring the configuration is available when the application starts.

spring:
  application:
    name: inventory-service
  cloud:
    config:
      uri: http://config-server:8888
      fail-fast: true
Enter fullscreen mode Exit fullscreen mode

The config server serves configuration files based on the application name and active profile. For example, if your service is named inventory-service and running with the production profile, it will look for inventory-service-production.yml in the repository.

I appreciate how this approach keeps configuration separate from code. You can change database URLs, feature flags, or other settings without rebuilding and redeploying your services. The config server can also encrypt sensitive values, adding a layer of security.

Handling failures is inevitable in distributed systems. When services communicate over networks, things can and will go wrong. A service might become slow or completely unavailable. Without proper handling, these failures can cascade through your system, taking down multiple services.

Circuit breakers help prevent this cascade. The pattern is simple: wrap calls to external services in a circuit breaker. If failures reach a certain threshold, the circuit "trips" and all subsequent calls immediately fail without attempting the remote call. This gives the failing service time to recover.

Resilience4j is a excellent library for implementing circuit breakers in Spring applications. It integrates smoothly with Spring Boot and provides several fault tolerance patterns.

To use Resilience4j, you add the dependency to your project. Then you can annotate methods that make external calls with @CircuitBreaker. You specify a fallback method to call when the circuit is open.

@Service
public class PaymentProcessor {

    private final PaymentServiceClient paymentClient;

    public PaymentProcessor(PaymentServiceClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    @CircuitBreaker(name = "paymentService", fallbackMethod = "processPaymentFallback")
    public PaymentResponse processPayment(PaymentRequest request) {
        return paymentClient.process(request);
    }

    private PaymentResponse processPaymentFallback(PaymentRequest request, Exception ex) {
        return PaymentResponse.failed("Payment service temporarily unavailable");
    }
}
Enter fullscreen mode Exit fullscreen mode

You configure the circuit breaker behavior in your application configuration. This includes settings like failure threshold, wait duration, and sliding window size.

resilience4j.circuitbreaker:
  instances:
    paymentService:
      registerHealthIndicator: true
      slidingWindowSize: 10
      minimumNumberOfCalls: 5
      waitDurationInOpenState: 10s
      failureRateThreshold: 50
Enter fullscreen mode Exit fullscreen mode

The circuit breaker has three states: closed, open, and half-open. In the closed state, calls pass through normally. When failures exceed the threshold, it moves to open state and rejects all calls. After the wait duration, it moves to half-open state and allows a few test calls. If they succeed, it returns to closed state.

I have found circuit breakers essential for maintaining system stability. They prevent a single failing service from affecting the entire system. Combined with proper monitoring and alerts, they help you identify and address problems quickly.

API gateways act as a single entry point for all client requests. Instead of clients calling services directly, they call the gateway. The gateway routes requests to the appropriate backend service. This provides several benefits: it simplifies client code, enables cross-cutting concerns like authentication and rate limiting, and hides the internal service structure.

Spring Cloud Gateway is a reactive API gateway built on Spring WebFlux. It offers flexible routing based on various criteria and supports filters for modifying requests and responses.

To create a gateway, you start a new Spring Boot application and add the Spring Cloud Gateway dependency. You then define routes in your configuration.

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

Route configuration can be done in YAML. Each route has an ID, a destination URI, predicates for matching requests, and filters for processing them.

spring:
  cloud:
    gateway:
      routes:
      - id: user_service_route
        uri: lb://user-service
        predicates:
        - Path=/api/users/**
        filters:
        - StripPrefix=1
      - id: product_service_route
        uri: lb://product-service
        predicates:
        - Path=/api/products/**
        filters:
        - StripPrefix=1
Enter fullscreen mode Exit fullscreen mode

The lb:// scheme indicates load balancing through the service discovery client. The gateway will query Eureka (or another discovery service) to find available instances of user-service and product-service.

Filters are powerful for implementing cross-cutting concerns. You can add authentication, logging, rate limiting, or response transformation. Spring Cloud Gateway provides many built-in filters, and you can create custom ones.

spring:
  cloud:
    gateway:
      routes:
      - id: secure_route
        uri: lb://secure-service
        predicates:
        - Path=/api/secure/**
        filters:
        - name: RequestHeader
          args:
            name: Authorization
            value: Bearer {your-token}
        - name: RewritePath
          args:
            regexp: /api/secure/(?<segment>.*)
            replacement: /$\{segment}
Enter fullscreen mode Exit fullscreen mode

I often use the gateway for authentication. Instead of each service handling authentication separately, the gateway can validate tokens and add user information to requests. This keeps security logic in one place.

Distributed tracing gives you visibility into requests as they flow through your system. When a request enters your system, it gets a unique trace ID. As the request moves between services, each service creates spans that represent units of work. These spans are collected and can be visualized to understand performance and troubleshoot issues.

Spring Cloud Sleuth automatically adds trace and span IDs to your logs. It integrates with OpenZipkin or Jaeger for collecting and visualizing traces.

To use Sleuth, you add the dependency to your services. Sleuth automatically instruments common Spring components like REST controllers and Feign clients.

@RestController
public class OrderController {

    private final Tracer tracer;
    private final InventoryService inventoryService;

    public OrderController(Tracer tracer, InventoryService inventoryService) {
        this.tracer = tracer;
        this.inventoryService = inventoryService;
    }

    @PostMapping("/orders")
    public Order createOrder(@RequestBody Order order) {
        Span span = tracer.nextSpan().name("createOrder").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            // Business logic
            inventoryService.checkInventory(order);
            return orderService.create(order);
        } finally {
            span.end();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You configure Sleuth and Zipkin in your application configuration. This includes the sampling rate and Zipkin server location.

spring:
  sleuth:
    sampler:
      probability: 1.0
  zipkin:
    base-url: http://zipkin-server:9411
Enter fullscreen mode Exit fullscreen mode

The sampling rate controls how many requests are traced. Setting it to 1.0 means all requests are traced, which is good for development but might be too much for production. You might set a lower rate in production to reduce overhead.

Zipkin provides a UI for viewing traces. You can see the entire path of a request, how long each service took, and identify bottlenecks. This is invaluable for debugging performance issues in complex systems.

I remember debugging a performance issue where orders were processing slowly. Using Zipkin, I could see that the delay was happening in the payment service. The trace showed that the payment service was calling an external API that was responding slowly. Without distributed tracing, finding this would have taken much longer.

Putting all these techniques together creates a robust microservices architecture. Service discovery allows dynamic location of services. Configuration management centralizes settings. Circuit breakers prevent cascading failures. API gateways simplify client interactions. Distributed tracing provides visibility.

Each technique addresses a specific challenge of distributed systems. Together, they help you build systems that are resilient, scalable, and maintainable. The Spring ecosystem provides excellent support for implementing these patterns with minimal boilerplate code.

I have built several systems using these techniques. They handle traffic spikes gracefully and recover quickly from failures. The learning curve is manageable, and the benefits are significant. Your services remain decoupled and focused on their specific domains.

The key is to start simple and add complexity as needed. You might not need all these techniques for a small system. But as your system grows, having these foundations in place makes scaling much smoother. The patterns and tools are proven to work well in production environments.

Microservices are not a silver bullet. They introduce complexity that monolithic applications don't have. But with the right patterns and tools, you can manage this complexity effectively. Spring Boot and Spring Cloud provide a solid foundation for building distributed systems that meet modern demands for scalability and resilience.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)