Advanced Java & Spring Boot · Lesson 12 of 16 · Updated ·
Microservices with Spring Cloud — Monolith vs Microservices, Eureka and API Gateway
Our Course API is a monolith: one app does everything. That's the right start. But as teams and traffic grow, companies split systems into microservices. Interviews for product companies almost always ask about them.
1. Monolith vs microservices
| Monolith | Microservices | |
|---|---|---|
| Deploy | one unit | each service on its own |
| Scale | everything together | only the busy service |
| Team | one codebase | each team owns a service |
| Data | one shared database | each service owns its data |
| Failure | one bug can take down all | failures can be contained (with care) |
| Complexity | simple | network calls, discovery, tracing, eventual consistency |
Start with a well-structured monolith; split when a part needs independent scaling, deployment or ownership. Microservices are a solution to an organisational problem, not a goal.
2. Our system
Four Spring Boot apps, all built and tested:
| Service | Port | Job |
|---|---|---|
discovery-server | 8761 | Eureka registry: who is running where |
api-gateway | 8080 | single entry point; routes by path |
course-api | 8081 | the API from lessons 4–11 |
order-service | 8082 | places orders (next two lessons) |
Versions: Spring Boot 4.1.2 with Spring Cloud 2025.1.4 (the release train is imported as a BOM in each pom.xml).
3. Service discovery with Eureka
Services start and stop, scale to many instances and get new IP addresses. Hard-coding http://10.0.0.7:8081 breaks. Instead every service registers its address with a discovery server, and callers ask for it by name.
// File: discovery-server/src/main/java/com/skills101/discovery/DiscoveryServerApplication.java
package com.skills101.discovery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer // services register here and look each other up by name
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
# File: discovery-server/src/main/resources/application.properties
spring.application.name=discovery-server
server.port=8761
# the registry itself doesn't register with anyone
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
Clients just add spring-cloud-starter-netflix-eureka-client and say where Eureka is — course-api registers itself as course-api (its spring.application.name):
# File: course-api/src/main/resources/application.properties (excerpt)
spring.application.name=course-api
server.port=8081
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
Our test starts the registry on a random port and checks that GET /eureka/apps answers. Open http://localhost:8761 to see the dashboard of registered instances.
4. API Gateway
Clients shouldn't need to know every service. The gateway is the one door: it routes requests, and is the natural place for cross-cutting work — authentication, rate limiting, CORS, logging.
# File: api-gateway/src/main/resources/application.properties
spring.application.name=api-gateway
server.port=8080
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
# Route 1: /api/courses/** → any healthy instance of course-api ("lb://" = load-balanced via Eureka;
# ${course-api.uri:...} lets a test or another environment point it elsewhere)
spring.cloud.gateway.server.webmvc.routes[0].id=courses
spring.cloud.gateway.server.webmvc.routes[0].uri=${course-api.uri:lb://course-api}
spring.cloud.gateway.server.webmvc.routes[0].predicates[0]=Path=/api/courses/**
spring.cloud.gateway.server.webmvc.routes[0].filters[0]=AddResponseHeader=X-Served-By, api-gateway
# Route 2: /api/orders/** → order-service
spring.cloud.gateway.server.webmvc.routes[1].id=orders
spring.cloud.gateway.server.webmvc.routes[1].uri=lb://order-service
spring.cloud.gateway.server.webmvc.routes[1].predicates[0]=Path=/api/orders/**
lb://course-api— "load-balanced": ask Eureka for the healthy instances ofcourse-apiand spread requests across them.- Predicates decide which route matches (
Path=/api/courses/**); filters change requests or responses (AddResponseHeader). - This is Spring Cloud Gateway's server-webmvc flavour (servlet-based, like the rest of our apps); there's also a reactive WebFlux version.
Our gateway test starts the gateway plus a fake course-api, then checks that GET /api/courses/1 is forwarded, the body comes back unchanged, the X-Served-By: api-gateway header is added, and unknown paths get 404:
// File: api-gateway/src/test/java/com/skills101/gateway/ApiGatewayApplicationTests.java (excerpt)
void forwardsCoursesAndAddsHeader() throws Exception {
HttpResponse<String> res = HttpClient.newHttpClient().send(
HttpRequest.newBuilder(URI.create("http://localhost:" + port + "/api/courses/1")).build(),
HttpResponse.BodyHandlers.ofString());
assertThat(res.statusCode()).isEqualTo(200);
assertThat(res.body()).isEqualTo("{\"id\":1,\"title\":\"Java Full Stack\"}");
assertThat(res.headers().firstValue("X-Served-By")).contains("api-gateway");
5. Other building blocks (know the names)
- Config Server — central configuration for all services (Spring Cloud Config).
- Distributed tracing — follow one request across services (Micrometer Tracing + Zipkin/Jaeger).
- Database per service and Saga pattern for multi-service transactions (no shared transactions across services).
- Kubernetes often replaces Eureka for discovery in production (its Services do the same job).
Common mistakes
- Splitting too early or too small ("nano-services") — all the cost, none of the benefit.
- Services sharing one database — they're then coupled, just over the network.
- Hard-coded host names instead of discovery or DNS.
- No timeouts or fallbacks on calls between services (next lesson).
Quick recap
- Monolith first; microservices when parts need independent scaling, deployment or ownership.
- Eureka: services register by name; callers look them up.
- The API gateway is the single entry point: routes (
lb://name), predicates, filters. - Each service owns its data.