From Monolith to Microservices: Scaling with Spring Boot and Spring Cloud

Written by amirtha | Published 2025/11/27
Tech Story Tags: java-programming | spring-boot | spring-cloud | microservices | microservices-from-scratch | monolith-to-microservices | microservices-ecosystem | monolith-ecosystem

TLDRIn this step-by-step tutorial, we’ll build a fully working microservices ecosystem from scratch.via the TL;DR App

In today’s world, monolithic applications are rapidly being replaced by microservice architectures — where applications are divided into small, independent services that can scale, deploy, and evolve individually.

In this step-by-step tutorial, we’ll build a fully working microservices ecosystem from scratch using:

  • Spring Boot 3.2.x
  • Spring Cloud Netflix Eureka (Service Discovery)
  • Spring Cloud Gateway (API Gateway)
  • Spring Cloud OpenFeign (Inter-Service Communication)
  • Maven (Build Tool)

By the end, you’ll have four independent Spring Boot microservices running and talking to each other — no Docker, no complex infra, just clean Java + Spring setup.

Why I Used Microservices Architecture in My Project

In one of my recent projects, our monolithic application started facing several challenges as the business grew.

We had multiple teams working on every small change or release caused deployment dependencies, performance bottlenecks, and testing delays.

To overcome these challenges, we decided to migrate to a Microservices-based architecture using Spring Boot and Spring Cloud.

Key Pain Points in the Monolith:

  • Every new feature deployment required full application redeployment.
  • The codebase became large and tightly coupled.
  • Database queries were shared between multiple modules.
  • Downtime affected the entire system, even for small bugs.

Architecture Overview

Our ecosystem will have four Spring Boot projects:

Service

Port

Role

Eureka Server

8761

Service registry — all services register here

Department Service

8081

Provides department data

Employee Service

8082

Provides employee data, calls department service

API Gateway

8080

Single entry point, routes requests to services

Architecture Diagram

** **



How It Works in Real Time

Scenario 1 — Fetching an Employee’s Department Info

When an HR manager opens an employee profile in the web app:

  1. Frontend calls
  2. GET https://api.mycompany.com/employees/1001
  3. Request hits the API Gateway → routes to Employee Service
  4. Employee Service fetches employee details (from DB) and calls Department Service (via Feign + Eureka) to get department info.
  5. Employee Service aggregates both results and sends the response back to the Gateway → client.
  6. The client sees:
{
  "employee": {
    "id": 1001,
    "name": "Alice Johnson",
    "departmentId": 2
  },
  "department": {
    "id": 2,
    "name": "Engineering"
  }
}

Scenario 2 — Scaling Up

  • During appraisal season, Employee Service traffic spikes.
  • Since each microservice is independent, you can deploy multiple Employee Service instances (on Kubernetes, ECS, etc.).
  • Eureka automatically registers them, and the API Gateway load-balances requests across all.

No config change needed
No downtime
Seamless scaling

Step 1 — Create the Parent Folder

Create one folder to hold all projects:

mkdir spring-microservices-ecosystem

We’ll create four sub-modules:

  • eureka-server
  • department-service
  • employee-service
  • api-gateway

Each module will be an independent Spring Boot Maven project.

Step 2 — Setup Eureka Server (Service Registry)

Folder: eureka-server

1️ Create pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.6</version>
  <relativePath/>
 </parent>
 <groupId>com.example</groupId>
 <artifactId>eureka-server</artifactId>
 <version>1.0.0</version>
 <properties>
  <java.version>17</java.version>
  <spring-cloud.version>2023.0.1</spring-cloud.version>
 </properties>
 <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-web</artifactId>
  </dependency>
 </dependencies>
 <dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
 </dependencyManagement>
 <build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
 </build>
</project>

2️ Create main class

package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }}

3️ application.yml

server:
  port: 8761
spring:
  application:
    name: eureka-server
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false

Run & Verify

Run EurekaServerApplication. Then open http://localhost:8761 you should see the Eureka Dashboard.

Step 3 — Create Department Service

Folder: department-service

1️ pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.6</version>
  <relativePath/>
 </parent>
 <groupId>com.example</groupId>
 <artifactId>department-service</artifactId>
 <version>1.0.0</version>
 <properties>
  <java.version>17</java.version>
  <spring-cloud.version>2023.0.1</spring-cloud.version>
 </properties>
 <dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>
 </dependencies>
 <dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
 </dependencyManagement>
 <build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
 </build>
</project>

2️ Main class

package com.example.departmentservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class DepartmentServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(DepartmentServiceApplication.class, args);}}

3️ Model

package com.example.departmentservice.model;
public class Department {
    private Long id;
    private String name;
    public Department() {}
    public Department(Long id, String name) {
        this.id = id; this.name = name;
    }
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

4️ Controller

package com.example.departmentservice.controller;
import com.example.departmentservice.model.Department;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/departments")
public class DepartmentController {
    private static final List<Department> departments = Arrays.asList(
            new Department(1L, "Engineering"),
            new Department(2L, "HR"),
            new Department(3L, "Finance")
    );
    @GetMapping
    public List<Department> getAll() {
        return departments;
    }
    @GetMapping("/{id}")
    public Department getById(@PathVariable Long id) {
        return departments.stream()
                .filter(dep -> dep.getId().equals(id))
                .findFirst()
                .orElse(null);
    }
}

5️ application.yml

server:
  port: 8081
spring:
  application:
    name: department-service
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

Step 4 — Create Employee Service (Feign Client + Eureka)

Folder: employee-service

1️pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.6</version>
  <relativePath/>
 </parent>
 <groupId>com.example</groupId>
 <artifactId>employee-service</artifactId>
 <version>1.0.0</version>
 <properties>
  <java.version>17</java.version>
  <spring-cloud.version>2023.0.1</spring-cloud.version>
 </properties>
 <dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>
 </dependencies>
 <dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
 </dependencyManagement>
 <build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
 </build>
</project>

2️ Main class

package com.example.employeeservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class EmployeeServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(EmployeeServiceApplication.class, args);
    }
}

3️ Feign client

package com.example.employeeservice.feign;
import com.example.employeeservice.model.Department;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "department-service")
public interface DepartmentClient {
    @GetMapping("/departments/{id}")
    Department getDepartmentById(@PathVariable("id") Long id);
}

4️ Models & Controller

package com.example.employeeservice.model;
public class Employee {
    private Long id;
    private String name;
    private Long departmentId;
    public Employee() {}
    public Employee(Long id, String name, Long departmentId) {
        this.id = id; this.name = name; this.departmentId = departmentId;
    }
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Long getDepartmentId() { return departmentId; }
    public void setDepartmentId(Long departmentId) { this.departmentId = departmentId; }
}
package com.example.employeeservice.model;
public class Department {
    private Long id;
    private String name;
    public Department() {}
    public Department(Long id, String name) { this.id = id; this.name = name; }
    public Long getId() { return id; } public void setId(Long id) { this.id = id; }
    public String getName() { return name; } public void setName(String name) { this.name = name; }
}
package com.example.employeeservice.controller;
import com.example.employeeservice.feign.DepartmentClient;
import com.example.employeeservice.model.Department;
import com.example.employeeservice.model.Employee;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/employees")
public class EmployeeController {
    private final DepartmentClient departmentClient;
    public EmployeeController(DepartmentClient departmentClient) {
        this.departmentClient = departmentClient;
    }
    private static final List<Employee> employees = Arrays.asList(
            new Employee(1L, "Alice", 1L),
            new Employee(2L, "Bob", 2L),
            new Employee(3L, "Charlie", 1L)
    );
    @GetMapping
    public List<Employee> getAll() { return employees; }
    @GetMapping("/{id}")
    public Map<String, Object> getEmployeeWithDepartment(@PathVariable Long id) {
        Employee emp = employees.stream().filter(e -> e.getId().equals(id)).findFirst().orElse(null);
        if (emp == null) return Map.of("error", "Employee not found");
        Department dept = departmentClient.getDepartmentById(emp.getDepartmentId());
        return Map.of("employee", emp, "department", dept);
    }
}

5️ application.yml

server:
  port: 8082
spring:
  application:
    name: employee-service
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

Step 5 — Create API Gateway

Folder: api-gateway

1️ pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.6</version>
  <relativePath/>
 </parent>
 <groupId>com.example</groupId>
 <artifactId>api-gateway</artifactId>
 <version>1.0.0</version>
 <properties>
  <java.version>17</java.version>
  <spring-cloud.version>2023.0.1</spring-cloud.version>
 </properties>
 <dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
  </dependency>
  <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>
 </dependencies>
 <dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
 </dependencyManagement>
 <build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
 </build>
</project>

2️ Main class

package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

3️ application.yml

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: employee-service
          uri: lb://EMPLOYEE-SERVICE
          predicates:
            - Path=/employees/**
        - id: department-service
          uri: lb://DEPARTMENT-SERVICE
          predicates:
            - Path=/departments/**
eureka:
  client:
    service-url:

Step 6 — Run the Services (Order Matters)

# 1. Start the Eureka server

Run As 🡪 EurekaServerApplication

# 2. Start Department service

Run As 🡪 DepartmentServiceApplication

# 3. Start Employee service

Run As 🡪 EmployeeServiceApplication

# 4. Start API Gateway

Run As 🡪 GatewayApplication

Check the Eureka dashboard: http://localhost:8761
All three clients should show up as REGISTERED.

Step 7 — Test the APIs

Through Gateway

  • GET http://localhost:8080/departments
  • GET http://localhost:8080/employees
  • GET http://localhost:8080/employees/1

Output:

{
  "employee": {
    "id": 1,
    "name": "Alice",
    "departmentId": 1
  },
  "department": {
    "id": 1,
    "name": "Engineering"
  }
}

Step 8 — Troubleshooting

Issue

Cause

Solution

Duplicate Key Exception: found duplicate key spring

Two top-level spring blocks in YAML

Merge them into one

Eureka dashboard empty

Clients not registered

Check eureka.client.service-url.defaultZone and network

Feign 404

Wrong path or service name

Ensure @FeignClient(name="department-service") matches

Port conflict

Already in use

Change server. port

Gateway 404

Wrong route predicates

Check Path and ensure /departments/** is correct

Conclusion

You’ve just built a fully functioning microservices system from scratch using Spring Boot 3.2and Spring Cloud.
Each service isindependent, discoverable, and communicatesvia Feign through Eureka.
The API Gateway provides a single unified entry point.

This same architecture is used by real enterprise systems — what you’ve built here is a production-grade foundation.


Written by amirtha | With over 20 years of experience in software engineering—specializing in Java, Python, Spring Boot, and Microservices.
Published by HackerNoon on 2025/11/27