Spring Boot Microservices
Quantity Measurement Application
What you will learn
Architecture Principles — Monolith vs Microservices
Spring Boot 3.x & Spring Cloud 2023.x foundations
Service Registry using Netflix Eureka
API Gateway & Routing with Spring Cloud Gateway
Interservice Communication using OpenFeign
Prerequisites: Java 17+, Spring Boot basics, Maven, REST APIs
Tech stack: Java 17 • Spring Boot 3.2 • Spring Cloud 2023.x • Eureka • OpenFeign • H2 • Maven
CHAPTER 01
Architecture Principles
Monolith vs Microservices — when and why to split
What is the Quantity Measurement App?
The Quantity Measurement App converts and computes units of measurement: length (km ↔
miles), weight (kg ↔ lbs), temperature (°C ↔ °F), and volume (L ↔ gallons). We will build this
as a microservices system — where each concern lives in its own independently deployable
service.
The Monolith Approach
A monolith puts everything — controllers, services, repositories, UI — into a single Spring Boot
application packaged as one JAR. This is perfectly valid for small apps, but it creates problems
as the team and feature set grow.
Monolith — Challenges Microservices — Benefits
One deploy for every Deploy only what changed.
change. Even fixing a typo in Update measurement-service
length service means without touching user-service.
redeploying the entire app. Scale independently. Run 5
Scaling is all-or-nothing. If instances of
conversion requests spike, measurement-service, 1 of
you must scale the whole app, user-service.
not just the busy part. Technology freedom. Each
Technology lock-in. The service can use the tech that
whole codebase must use the suits it best.
same Java version, Spring Isolated failures. If
version, and libraries. user-service crashes,
Failure cascades. A crash in conversions still work.
one module can bring down Small, focused codebases.
the entire application. Each service is easy to
Large, complex codebase. understand and maintain.
Hard to onboard new
developers as the app grows.
Our Microservices Architecture
Here is how the Quantity Measurement App is split into four independent services:
Service Port Responsibility
eureka-serve :8761 Service registry — all services register and discover each other here
r
api-gateway :8080 Single entry point — routes client requests to the correct service
measurement- :8081 Core logic — handles all unit conversions (length, weight, temp,
service volume)
user-service :8082 User profiles — stores user data and conversion history
Design Principles in Play
Single Responsibility — Each service has one job. measurement-service only
knows about conversions.
Loose Coupling — Services communicate via HTTP/REST. Changing one
service's internals never breaks others.
High Cohesion — Everything related to measurement (model, controller,
service, repository) lives in one place.
Database per Service — Each service has its own database. They never share
a DB directly — only APIs.
Resilience — If user-service is down, measurement-service continues to serve
conversions.
Key insight — Database per Service
Microservices each own their data. measurement-service has its own H2 database; user-service has its
own. They never query each other's database directly. If user-service needs data from
measurement-service, it calls the REST API. This is the most important pattern to internalise.
Startup Order
Always start services in this order to avoid connection errors:
eureka-server — must be running first so others can register
measurement-service and user-service — register with Eureka on startup
api-gateway — registers with Eureka and begins routing traffic
CHAPTER 02
Spring Boot & Spring Cloud
The foundational framework and cloud-native extensions
Spring Boot in a Nutshell
Spring Boot eliminates boilerplate configuration. You get an embedded Tomcat server,
auto-configuration, and production-ready defaults out of the box. Your service is a single
runnable JAR — no separate server installation required.
Spring Cloud — What it Adds
Spring Cloud builds on Spring Boot to solve distributed system problems: service discovery,
load balancing, config management, circuit breaking, and API routing.
Spring Cloud Netflix Spring Cloud Gateway OpenFeign Spring Cloud Config
Eureka (registry), Ribbon Reactive API gateway with Declarative HTTP client — Centralised config from a
(load balancer) routing and filters write interfaces, not code Git-backed server
Maven Setup — measurement-service
[Link] — measurement-service
<parent>
<groupId>[Link]</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<properties>
<[Link]>2023.0.0</[Link]>
</properties>
<dependencies>
<!-- Core web: REST controllers + embedded Tomcat -->
<dependency>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Register with Eureka service registry -->
<dependency>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Feign: declarative HTTP client for inter-service calls -->
<dependency>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- H2 in-memory database (dev/test) -->
<dependency>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Project Structure — measurement-service
Folder structure
measurement-service/
├── src/main/java/com/qm/measurement/
│ ├── [Link] ← @SpringBootApplication
│ ├── controller/
│ │ └── [Link] ← REST endpoints
│ ├── service/
│ │ └── [Link] ← Business logic
│ ├── model/
│ │ └── [Link] ← Response model
│ └── client/
│ └── [Link] ← Feign client
└── src/main/resources/
└── [Link] ← Port, Eureka, app name
Main Application Class
[Link]
@SpringBootApplication
@EnableFeignClients // Enables Feign for interservice calls
public class MeasurementServiceApplication {
public static void main(String[] args) {
[Link]([Link], args);
}
}
REST Controller
[Link]
@RestController
@RequestMapping("/api/convert")
public class ConversionController {
@Autowired
private ConversionService conversionService;
// GET /api/convert/length?from=km&to=miles&value=10
@GetMapping("/length")
public ConversionResult convertLength(
@RequestParam String from,
@RequestParam String to,
@RequestParam double value) {
return [Link](from, to, value);
}
// GET /api/convert/temperature?from=C&to=F&value=100
@GetMapping("/temperature")
public ConversionResult convertTemperature(
@RequestParam String from,
@RequestParam String to,
@RequestParam double value) {
return [Link](from, to, value);
}
}
[Link]
measurement-service/src/main/resources/[Link]
server:
port: 8081
spring:
application:
name: measurement-service # Used as service ID in Eureka!
datasource:
url: jdbc:h2:mem:measurementdb
driver-class-name: [Link]
h2:
console:
enabled: true
eureka:
client:
service-url:
defaultZone: [Link]
instance:
prefer-ip-address: true
[Link] is critical
This name becomes the service ID in Eureka. Other services and the API gateway use this exact name to look
up and communicate with your service. Think of it as your service's phone number in the registry.
CHAPTER 03
Service Registry with Eureka
Dynamic service discovery — the phone book of microservices
Why Do We Need a Service Registry?
In a monolith, a method call is just a function invocation. In microservices, services run as
separate OS processes — possibly on different machines — and their IP addresses change as
they are restarted or scaled. We cannot hardcode addresses.
Eureka solves this: every service announces its presence on startup, and any service that
needs to call another service asks Eureka for the current address at runtime.
The Registration Flow
1. Service starts 2. Registers 3. Heartbeat 4. Discovery
measurement-service Sends POST to Eureka with Sends keep-alive every 30 Other services query Eureka
boots up on :8081 name, host, port seconds to get the address
Setting Up the Eureka Server
Create a new Spring Boot project called eureka-server with the following dependency:
[Link] — eureka-server
<dependency>
<groupId>[Link]</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
[Link]
@SpringBootApplication
@EnableEurekaServer // This single annotation activates the registry
public class EurekaServerApplication {
public static void main(String[] args) {
[Link]([Link], args);
}
}
eureka-server/[Link]
server:
port: 8761 # Standard Eureka port (convention)
spring:
application:
name: eureka-server
eureka:
client:
register-with-eureka: false # Server doesn't register with itself
fetch-registry: false # Server doesn't fetch its own registry
server:
wait-time-in-ms-when-sync-empty: 0
Eureka Dashboard
After starting eureka-server, open [Link] in your browser. You will see the Eureka dashboard
showing all registered instances. Bookmark this — you will use it constantly during development to verify that
services have registered correctly.
Client Configuration (measurement-service and user-service)
Adding the eureka-client dependency is sufficient — Spring Boot auto-configuration handles
registration automatically on startup. Add this to [Link]:
Eureka client config in [Link]
eureka:
client:
service-url:
defaultZone: [Link]
fetch-registry: true # Download the registry (to discover others)
register-with-eureka: true # Announce ourselves
instance:
lease-renewal-interval-in-seconds: 10 # Heartbeat every 10s
lease-expiration-duration-in-seconds: 30 # Deregister after 30s of silence
Heartbeat and Expiry
After registration, each service sends a heartbeat to Eureka every 30 seconds. If Eureka does
not receive a heartbeat for 90 seconds, it removes that instance from the registry. This ensures
the registry always reflects which services are actually alive.
CHAPTER 04
API Gateway & Routing
One entry point — intelligent routing, filtering, and load balancing
Why an API Gateway?
Without a gateway, clients must know the address and port of every service (:8081, :8082, ...).
As services are scaled, restarted, or moved, clients would break. The API Gateway provides
one stable address (:8080) for all clients. It then routes requests internally.
Routing Load Balancing Filters Rate Limiting
Maps URL paths to Round-robin across multiple Auth checks, logging, Protect backends from traffic
backend services instances via Eureka headers before/after spikes
requests
Setting Up api-gateway
[Link] — api-gateway
<!-- Spring Cloud Gateway (reactive, WebFlux-based) -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Eureka client so the gateway can discover services -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Route Configuration
api-gateway/[Link]
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# Route 1: /api/convert/** → measurement-service
- id: measurement-route
uri: lb://measurement-service # lb:// = load-balanced via Eureka
predicates:
- Path=/api/convert/**
filters:
- StripPrefix=0
# Route 2: /api/users/** → user-service
- id: user-route
uri: lb://user-service
predicates:
- Path=/api/users/**
eureka:
client:
service-url:
defaultZone: [Link]
lb:// prefix explained
The lb:// prefix tells Spring Cloud Gateway to use load-balanced service discovery. Instead of a hardcoded URL
like [Link] the gateway asks Eureka for all instances running under the name
'measurement-service' and distributes requests across them using round-robin.
How a Request is Routed — Step by Step
Client sends: GET
[Link]
Gateway receives the request and checks all defined routes in order.
Predicate check: Does the path /api/convert/length match the pattern
/api/convert/**? YES.
Eureka lookup: lb://measurement-service → query Eureka → returns
[Link]
Gateway forwards: GET
[Link]
measurement-service processes the request and returns the response to the
client via the gateway.
Adding a Global Logging Filter
Filters intercept every request passing through the gateway. A global filter applies to all routes:
[Link] — inside api-gateway
@Component
public class LoggingFilter implements GlobalFilter {
private static final Logger log =
[Link]([Link]);
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
ServerHttpRequest req = [Link]();
[Link]("Incoming: {} {}",
[Link](), [Link]());
// Record the timestamp before forwarding
long start = [Link]();
return [Link](exchange).then([Link](() -> {
long duration = [Link]() - start;
[Link]("Completed in {}ms | status: {}",
duration,
[Link]().getStatusCode());
}));
}
}
CHAPTER 05
Interservice Communication
Feign clients, resilience patterns, and graceful degradation
The Scenario
When a user requests a unit conversion, measurement-service should also save a record to the
user's conversion history. That history lives in user-service. measurement-service must call
user-service over HTTP.
RestTemplate vs Feign
RestTemplate — verbose OpenFeign — recommended
Manual URL construction Just write an interface. Spring
required generates the HTTP client.
Explicit response type Uses Eureka service names. No
handling hardcoded URLs.
No integration with Eureka Looks like a local method call.
service names Clean, readable code.
More code, more error Built-in load balancing.
surface Automatically uses lb:// routing.
Still works — used in Easy fallbacks. Integrate with
legacy Spring apps Resilience4j circuit breaker.
Step 1 — Declare the Feign Client
Inside measurement-service, create an interface that mirrors user-service's endpoints:
[Link] — inside measurement-service/client/
// 'name' must match the [Link] of user-service exactly
@FeignClient(name = "user-service")
public interface UserServiceClient {
// Maps to POST /api/users/{userId}/history in user-service
@PostMapping("/api/users/{userId}/history")
ConversionHistoryResponse saveHistory(
@PathVariable("userId") Long userId,
@RequestBody ConversionHistoryRequest request
);
// Maps to GET /api/users/{userId}/history in user-service
@GetMapping("/api/users/{userId}/history")
List<ConversionHistoryResponse> getHistory(
@PathVariable("userId") Long userId
);
}
You never implement this interface
Spring generates a full HTTP client at runtime. Feign looks up user-service in Eureka, constructs the URL,
serialises the request body to JSON, makes the call, and deserialises the response — all from the interface
declaration. You write zero HTTP code.
Step 2 — Use the Feign Client in a Service
[Link] — measurement-service
@Service
public class ConversionService {
@Autowired
private UserServiceClient userServiceClient; // Injected like any Spring bean
public ConversionResult convertLength(
String from, String to, double value, Long userId) {
// Step 1: Perform the actual unit conversion
double result = performConversion(from, to, value);
// Step 2: Save the record to user's history via user-service
ConversionHistoryRequest histReq = new ConversionHistoryRequest(
"LENGTH", from, to, value, result
);
[Link](userId, histReq);
// ^ This looks like a local call, but it's actually an HTTP POST
// to user-service :8082 via Eureka load balancing
return new ConversionResult(from, to, value, result);
}
private double performConversion(String from, String to, double value) {
// km to miles: multiply by 0.621371
if ([Link]("km") && [Link]("miles")) return value * 0.621371;
if ([Link]("miles") && [Link]("km")) return value / 0.621371;
// ... other conversions
throw new IllegalArgumentException("Unsupported conversion: " + from + " -> " + to);
}
}
Step 3 — Endpoint in user-service
[Link] — user-service
@RestController
@RequestMapping("/api/users")
public class HistoryController {
@Autowired
private HistoryRepository historyRepository;
@PostMapping("/{userId}/history")
@ResponseStatus([Link])
public ConversionHistory saveHistory(
@PathVariable Long userId,
@RequestBody ConversionHistoryRequest request) {
ConversionHistory history = new ConversionHistory();
[Link](userId);
[Link]([Link]());
[Link]([Link]());
[Link]([Link]());
[Link]([Link]());
[Link]([Link]());
[Link]([Link]());
return [Link](history);
}
}
Step 4 — Fallback for Resilience
Network calls can fail. If user-service is unavailable, we should still return the conversion result
— we simply skip saving the history. This is called graceful degradation.
[Link] — with fallback
@FeignClient(
name = "user-service",
fallback = [Link] // Used when user-service is unreachable
)
public interface UserServiceClient {
@PostMapping("/api/users/{userId}/history")
ConversionHistoryResponse saveHistory(
@PathVariable Long userId,
@RequestBody ConversionHistoryRequest request
);
}
@Component
class UserServiceClientFallback implements UserServiceClient {
private static final Logger log =
[Link]([Link]);
@Override
public ConversionHistoryResponse saveHistory(Long userId,
ConversionHistoryRequest request) {
[Link]("user-service unavailable — history not saved for user {}", userId);
return new ConversionHistoryResponse(); // Return empty response, not an error
}
}
Resilience principle
The conversion still succeeds even when user-service is down. The user gets their answer; the history is silently
skipped. This is intentional design. Never allow one service's failure to cascade and break unrelated functionality.
Complete End-to-End Request Flow
Client sends: GET
1 [Link]
API Gateway matches predicate Path=/api/convert/** → routes to
2 lb://measurement-service
Eureka lookup: lb://measurement-service → [Link]
3
measurement-service ConversionController → [Link]()
4
ConversionService calculates result: 10 km = 6.21371 miles
5
Feign call: [Link](42, {LENGTH, km, miles, 10, 6.21}) →
6 POST :8082
user-service HistoryController saves record to its H2 database
7
Response flows back: {"from":"km","to":"miles","input":10,"result":6.21}
8
Tutorial Complete
You now have everything needed to build and run the full Quantity Measurement microservices stack.
What You Have Learned
Architecture: When microservices make sense, the Database per Service
pattern, and design principles (SRP, loose coupling, cohesion, resilience).
Spring Boot + Spring Cloud: Maven setup, @SpringBootApplication, REST
controllers, auto-configuration, and the Spring Cloud ecosystem.
Eureka Service Registry: @EnableEurekaServer, client registration, heartbeats,
and service discovery at runtime.
API Gateway: Route configuration, lb:// load-balanced URIs, predicates, and
global filters.
Interservice Communication: @FeignClient, interface-based HTTP clients,
fallback methods, and graceful degradation.
Suggested Next Steps
Run the full stack: Start eureka-server → measurement-service → user-service
→ api-gateway and test with Postman or curl.
Add Circuit Breaker: Integrate Resilience4j CircuitBreaker with Feign for
automatic retries and open-circuit protection.
Spring Cloud Config Server: Centralise all [Link] files in a Git repo
and serve them from a Config Server.
Distributed Tracing: Add Micrometer + Zipkin to trace requests as they flow
across services.
Docker Compose: Containerise all four services and define them in a
[Link] for one-command startup.
Kubernetes: Deploy to a local k8s cluster (minikube). Kubernetes has its own
service discovery, so Eureka becomes optional.
Quick Reference — Annotations
Annotation Purpose
@SpringBootApplication Marks the main class; enables auto-configuration + component scan
@EnableEurekaServer Turns the app into a Eureka service registry
@EnableFeignClients Scans for @FeignClient interfaces and generates HTTP clients
@FeignClient(name=...) Declares an interface as a Feign HTTP client targeting the named service
@RestController Combines @Controller + @ResponseBody; returns JSON responses
@GetMapping / Maps HTTP GET/POST requests to handler methods
@PostMapping
@RequestParam Binds query string parameters (e.g. ?from=km)
@PathVariable Binds URL path variables (e.g. /{userId})
@RequestBody Deserialises the HTTP request body to a Java object