Skills101

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.

A monolith is one deployable with courses, orders, payments and notifications sharing one database; microservices are separate small services that each own their database

1. Monolith vs microservices

MonolithMicroservices
Deployone uniteach service on its own
Scaleeverything togetheronly the busy service
Teamone codebaseeach team owns a service
Dataone shared databaseeach service owns its data
Failureone bug can take down allfailures can be contained (with care)
Complexitysimplenetwork 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

api-gateway on port 8080 routes /api/courses to course-api and /api/orders to order-service; both register with the Eureka discovery-server; order-service calls course-api through Feign with a circuit breaker and publishes OrderPlacedEvent to Kafka

Four Spring Boot apps, all built and tested:

ServicePortJob
discovery-server8761Eureka registry: who is running where
api-gateway8080single entry point; routes by path
course-api8081the API from lessons 4–11
order-service8082places 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 of course-api and 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.

Practice what you learned

📝 Quiz: Microservices Basics5 questions · instant answers & explanations · +10 XP · counts towards your free certificateTake the quiz →