DEV Community

lou
lou

Posted on

Part 1: Create a complete Microservices Architecture with Spring Boot, Spring Cloud, Eureka, Gateway, and OpenFeign

Reminder of Microservices Request Flow:

  1. All HTTP requests go through the Gateway.
  2. From the URL path, the Gateway determines the target microservice name.
  3. The Gateway queries the Registration (Discovery) Service using this name, which returns a list of healthy service instances.
  4. The Gateway performs load balancing by selecting one healthy instance.
  5. The Gateway forwards the request to the selected microservice.
  6. The microservice returns a response to the Gateway, which then returns the response to the client.

Let's create our app:

Create a maven parent project with Java 25.
This project will contain all microservices as modules.


Customer service

We start by creating the Customer Service, which will be responsible for managing customer data.
Using Spring Initializr, generate a new Spring Boot module and include the following dependencies:

  1. Spring web
  2. Lombok
  3. Spring data JPA
  4. Spring data REST (rest repositories)
  5. H2 database
  6. Spring Cloud config client
  7. Spring Boot Actuator
  8. Eureka Discovery client

Models:

We will use JPA to map our Java classes to database tables.
For example, the Customer entity can be defined as follows:

package net.lou.customerservice.entities;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;

@Entity
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @Builder
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
}
Enter fullscreen mode Exit fullscreen mode

JPA Annotations Explained:

@entity
Marks the class as a JPA entity and maps it to a database table.

@id
Indicates the primary key of the entity. Every JPA entity must define exactly one identifier.

@GeneratedValue
Specifies how the primary key is generated.
Using GenerationType.IDENTITY delegates ID generation to the database, which automatically increments the value for each new record.

Lombok Annotations:

Lombok is used to reduce boilerplate code.

@NoArgsConstructor and @AllArgsConstructor generate constructors.

@Getter and @setter generate accessor methods.

@builder enables the builder pattern, making object creation more readable and flexible.

Example usage with the builder pattern:

 @Bean
    CommandLineRunner commandLineRunner(CustomerRepository customerRepository){
        return args ->{
            customerRepository.save(Customer.builder().firstName("lou").lastName("test").email("test@email.com").build());
        };

    }
Enter fullscreen mode Exit fullscreen mode

This allows us to preload the database with sample data when the application starts.

Service Configuration:

The Customer Service is designed to integrate with both:

  1. Discovery Service (Eureka)
  2. Configuration Service (Spring Cloud Config)

To enable service discovery, add the following property

spring.cloud.discovery.enabled=true
Enter fullscreen mode Exit fullscreen mode

For now, this will be set to false until the Discovery Service is implemented.

Similarly, to enable the connection to the configuration server:

spring.cloud.config.enabled=true

Enter fullscreen mode Exit fullscreen mode

This allows the Customer Service to retrieve its configuration from the Config Server once it is available. We will set it to false.


Inventory Service

To create our Inventory Service, we will follow the same steps above.
Our Product entity will be defined as follows:

@Entity
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @Builder @ToString
public class Product {
    @Id
    private String id;
    private String name;
    private double price;
    private int quantity;
}

Enter fullscreen mode Exit fullscreen mode

Gateway Service

To route client requests to our microservices, we introduce an API Gateway.
This gateway will act as a single entry point, handling request routing, filtering, and cross-cutting concerns.

Create a new module named gateway-service using Spring Initializr, and add the following dependencies:

  1. Gateway
  2. Spring Cloud Actuator
  3. Config Client
  4. Eureka Discovery Client

Static Routing Configuration:

In the gateway, routes can be defined using a YAML configuration file.
Each route specifies:

  1. URI (target service),

  2. predicates (conditions to match incoming requests),

  3. filters (Used to modify requests and responses, for example: add, remove, or modify request headers)

Example configuration:

spring:
  cloud:
    gateway:
      routes:
        - id: example-route
          uri: http://example-service
          predicates:
            - Method=GET
            - Path=/api/example/**
          filters:
            - YourCustomFilter

Enter fullscreen mode Exit fullscreen mode

This approach assumes that the service URI is known ahead of time.
However, in a real microservices architecture, services are dynamic: instances can start, stop, or change addresses.

In practice, we only know the microservice name, not its physical location.


Discovery Service

To solve this problem, we introduce a Discovery Service.
Its role is to:

  1. allow microservices to register themselves,

  2. enable the Gateway to discover services dynamically by name,

  3. decouple service consumers from hardcoded URIs.

Create a new module named discovery-service and add a single dependency:

Enabling the Eureka Server

In the DiscoveryServiceApplication class, enable Eureka using:

@EnableEurekaServer
Enter fullscreen mode Exit fullscreen mode

Discovery Service Configuration:

Add the following properties to the Discovery Service configuration to:
1.Prevent the server from registering itself as a client.
2.Prevent it from fetching its own registry.
3.Define the Eureka server address.


spring.application.name=gateway-service
server.port=8888
spring.cloud.config.enabled=false
spring.cloud.discovery.enabled=true
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
eureka.instance.prefer-ip-address=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true

Enter fullscreen mode Exit fullscreen mode

Once the service is running, you should be able to access the Eureka dashboard at:

http://localhost:8761
Enter fullscreen mode Exit fullscreen mode

You will see a web interface listing all registered microservices:

This was the static way. Let’s now configure it the dynamic way.
In the GatewayServiceApplication, create a bean named locator that returns an object of type DiscoveryClientRouteDefinitionLocator:


  @Bean
    DiscoveryClientRouteDefinitionLocator locator(
            ReactiveDiscoveryClient rdc, DiscoveryLocatorProperties dlp){
        return new DiscoveryClientRouteDefinitionLocator(rdc,dlp);
    }

Enter fullscreen mode Exit fullscreen mode

If you run the service and launch

http://localhost:8888/product-service/api/products,

Enter fullscreen mode Exit fullscreen mode

this should return the product list.


Billing Service

Now that we have our Product and Customer services, we can create the Billing Service that connects them.

Before creating the module, let’s clarify the relationships:

  1. A Customer can have multiple Bills.
  2. Each Bill belongs to one Customer.
  3. A Bill can contain one or more Product items.
  4. Each Product item references exactly one Product.
  5. The same Product can appear in multiple Product items across different Bills.

Create your billing-service module and add the following dependencies:

  1. Spring Web
  2. Spring Data JPA
  3. H2 Database
  4. Eureka Discovery Client
  5. Lombok
  6. Rest Repositories
  7. Config Client
  8. OpenFeign
  9. Spring HATEOAS

Create 2 Repositories, entities and model.
In your entity repository create 2 Classes:

Bill:

@Entity
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @Builder
public class Bill {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Date billingDate;
    private long customerId;
    @OneToMany(mappedBy = "bill")
    private List<ProductItem> productItems = new ArrayList<>();
}

Enter fullscreen mode Exit fullscreen mode

ProductItem:

@Entity
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class ProductItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productId;
    @ManyToOne
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private Bill bill;
    private int quantity;
    private double unitPrice;
}
Enter fullscreen mode Exit fullscreen mode

In your model repository, create DTO classes for the Customer and Product:

@Getter @Setter
public class Customer {
    private Long id;
    private String name;
    private String email;
}

Enter fullscreen mode Exit fullscreen mode
@Getter @Setter
public class Product {
    private String id;
    private String name;
    private double price;
    private int quantity;
}

Enter fullscreen mode Exit fullscreen mode

To manage the entities, create JpaRepository interfaces for the two entities Bill and ProductItem:

public interface BillRepository  extends JpaRepository<Bill, Long> {
}
Enter fullscreen mode Exit fullscreen mode
public interface ProductItemRepository extends JpaRepository<ProductItem, Long> {
}

Enter fullscreen mode Exit fullscreen mode

To manage customers and products across microservices, we use OpenFeign.

And in your Bill class, add a field called customer of type Customer. The annotation @Transient will serve as an indicator that the field does not exist in the database:

@Transient private Customer customer;

Then go to your ProductItem class and add the following field:

@Transient private Product product;

Create a repository for Feign clients, and add interfaces for customer-service and inventory-service:

@FeignClient(name = "customer-service")
public interface CustomerRestClient {
    @GetMapping("/api/customers/{id}")
    Customer getCustomerById(@PathVariable Long id);

    @GetMapping("/api/customers")
    PagedModel<Customer> getAllCustomers();

}

Enter fullscreen mode Exit fullscreen mode
@FeignClient(name = "inventory-service")
public interface ProductRestClient {
    @GetMapping("/api/products/{id}")
    Product getProductById(@PathVariable String id);
    @GetMapping("/api/products")
    PagedModel<Product> getAllProducts();
}

Enter fullscreen mode Exit fullscreen mode

For example, calling getCustomerById(@PathVariable Long id) sends an HTTP request to the customer-service.
The Discovery Service resolves the microservice’s address. The request is sent to /api/customers/{id} (the id comes from the @PathVariable).
The Customer Service returns a JSON with the Customer data, which OpenFeign maps to the Customer object.

To sum it up: To look up data from your database, you use Spring Data JPA. To retrieve data from another microservice, you use OpenFeign.


Configuration Service (Spring Cloud Config)

The Configuration Service allows microservices to externalize their configuration instead of hardcoding it inside each service.
This makes configuration centralized, consistent, and easy to change without rebuilding services.

Each microservice retrieves its configuration at startup from the Config Server using its application name.

Enabling Config Client in a Microservice

To enable configuration retrieval from the Config Server, add the following dependency:

  1. Spring Cloud Config Client

This allows the microservice to fetch its configuration remotely at startup.

Microservice Configuration:

In the microservice application.properties make sure the spring cloud config is enabled and specify the config server address:

spring.cloud.config.enabled=true
spring.config.import=optional:configserver:http://localhost:9999
Enter fullscreen mode Exit fullscreen mode

** Testing Configuration Retrieval:**

To verify that the configuration service is working, create a REST controller and inject configuration values using @Value annotation

Example configuration stored in the Config Server:

config.params.param1=80
config.params.param2=20
Enter fullscreen mode Exit fullscreen mode

Injecting the values:

@Value("${config.params.param1}")
private String param1;

@Value("${config.params.param2}")
private String param2;
Enter fullscreen mode Exit fullscreen mode

Expose an endpoint to test:

@GetMapping("/testConfig1")
public Map<String, String> configTest() {
    return Map.of("param1", param1);
}
Enter fullscreen mode Exit fullscreen mode

Calling this endpoint confirms whether the configuration was successfully loaded from the Config Server

Option 2:

For structured and scalable configuration, it is better to group related properties into a configuration class.

Example configuration class:

@ConfigurationProperties(prefix = "config.params")
public record CustomerConfigParams(String param1, String param2) {}
Enter fullscreen mode Exit fullscreen mode

The prefix config.params automatically maps:

  • config.params.param1 → param1
  • config.params.param2 → param2

Enabling Configuration Binding:

In the main application class, enable configuration properties:

@EnableConfigurationProperties(CustomerConfigParams.class)
@SpringBootApplication
public class CustomerServiceApplication {
}
Enter fullscreen mode Exit fullscreen mode

This tells Spring to create a bean of CustomerConfigParams and make it injectable.

Injecting and Exposing Configuration:

Inject the configuration bean into a REST controller:

@Autowired
private CustomerConfigParams customerConfigParams;
Enter fullscreen mode Exit fullscreen mode

Expose it through an endpoint:

@GetMapping("/testConfig2")
public CustomerConfigParams configTest2() {
    return customerConfigParams;
}
Enter fullscreen mode Exit fullscreen mode

This endpoint returns all configuration values bound from the Config Server:

Each time we update the config, we need to restart the microservice.

Refreshing Configuration at Runtime:

To ensure that configuration values stay up to date without restarting the microservice, Spring Cloud provides a refresh mechanism.

In your REST controller, add the annotation@RefreshScope This tells Spring that the bean should be recreated when a configuration refresh is triggered.

When a refresh occurs, Spring:

  1. Reinstantiates the controller

  2. Re-injects configuration values (@Value and @ConfigurationProperties)

  3. Applies the updated configuration at runtime

Example:

@RestController
@RefreshScope
public class ConfigTestRestController {
}
Enter fullscreen mode Exit fullscreen mode

Building and Starting the Microservices:

Once all modules are implemented, start the services in the following order:

  1. Configuration Service
  2. Discovery Service
  3. Customer Service
  4. Inventory Service
  5. Billing Service
  6. Gateway Service

This order ensures that:

  • The Configuration Service is available before other services attempt to load their external configuration.
  • The Discovery Service is running before microservices try to register themselves.
  • The Gateway Service starts after all backend services are registered and discoverable.

Testing the API:
Once all services are running successfully, you can test the system through the Gateway Service, which acts as the single entry point for all client requests.


Source Code

You can find the complete project source code here:
https://github.com/leriaetnasta/my-mini-souq-micro-services

Top comments (1)

Collapse
 
mel_wang_bd07bd519fc32037 profile image
Mel Wang

This tutorial is awesome!