Add spring-boot-microservices-project: config server, Eureka, gateway and three services on Boot 4.1.1 + Spring Cloud 2025.1.3

Product, order and inventory services behind Spring Cloud Gateway (WebMVC), configuration from a Config Server,
discovery with Eureka, a Resilience4j circuit breaker on order -> inventory and Brave tracing. Run as six plain jars;
output/ holds real transcripts (config, registry, requests, trace propagation, circuit breaker, startup race and the
load-balanced RestClient.Builder trap). Dockerfile and docker-compose.yml are included but were not run.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HhW8LD8r3EigoBaCGrQZx5
This commit is contained in:
Claude
2026-10-21 17:08:42 +00:00
commit def73fa1c4
49 changed files with 1419 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# GET /api/orders/ready through the gateway, every 2 seconds, starting the moment the last service reported healthy.
# 200 means the gateway can route to the order service AND the order service can reach product and inventory.
/api/products/1 t+ 1s HTTP 200 {"id":1,"name":"Mechanical keyboard","price":89.90}
/api/inventory/1 t+ 0s HTTP 200 {"productId":1,"quantity":20,"lowStock":false}
/api/orders/ready t+ 0s HTTP 500 {"timestamp":"2026-10-21T17:07:18.322Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+ 2s HTTP 500 {"timestamp":"2026-10-21T17:07:20.364Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+ 4s HTTP 500 {"timestamp":"2026-10-21T17:07:22.396Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+ 6s HTTP 500 {"timestamp":"2026-10-21T17:07:24.421Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+ 8s HTTP 500 {"timestamp":"2026-10-21T17:07:26.444Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+10s HTTP 500 {"timestamp":"2026-10-21T17:07:28.466Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+12s HTTP 500 {"timestamp":"2026-10-21T17:07:30.490Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+14s HTTP 500 {"timestamp":"2026-10-21T17:07:32.513Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+16s HTTP 500 {"timestamp":"2026-10-21T17:07:34.542Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+18s HTTP 500 {"timestamp":"2026-10-21T17:07:36.570Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+20s HTTP 500 {"timestamp":"2026-10-21T17:07:38.595Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+22s HTTP 500 {"timestamp":"2026-10-21T17:07:40.634Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+24s HTTP 500 {"timestamp":"2026-10-21T17:07:42.658Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+26s HTTP 500 {"timestamp":"2026-10-21T17:07:44.686Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+28s HTTP 500 {"timestamp":"2026-10-21T17:07:46.709Z","status":500,"error":"Internal Server Error","path
/api/orders/ready t+30s HTTP 200 {"ready":true}
+16
View File
@@ -0,0 +1,16 @@
# What the config server hands out (port 8888). The services fetch this at startup.
--- order-service: which files were used and the values ---
name: order-service profiles: ['default']
source: order-service.yml
shop.order.currency = EUR
shop.order.max-quantity = 10
source: application.yml
management.endpoints.web.exposure.include = health,info,circuitbreakers
management.tracing.sampling.probability = 1.0
--- the same value as the order service itself reports it (GET /api/orders/config through the gateway) ---
$ curl -X GET localhost:8080/api/orders/config
HTTP 200
{"currency":"EUR","maxQuantity":10}
+6
View File
@@ -0,0 +1,6 @@
# What is registered in Eureka (port 8761)
GATEWAY instance=gateway:8080 status=UP
INVENTORY-SERVICE instance=inventory-service:8083 status=UP
ORDER-SERVICE instance=order-service:8082 status=UP
PRODUCT-SERVICE instance=product-service:8081 status=UP
+46
View File
@@ -0,0 +1,46 @@
# Requests through the gateway (port 8080). The client only ever talks to 8080.
--- products ---
$ curl -X GET localhost:8080/api/products
HTTP 200
[{"id":1,"name":"Mechanical keyboard","price":89.90},{"id":2,"name":"USB-C dock","price":129.00},{"id":3,"name":"27-inch monitor","price":249.50}]
$ curl -X GET localhost:8080/api/products/2
HTTP 200
{"id":2,"name":"USB-C dock","price":129.00}
$ curl -X GET localhost:8080/api/products/99
HTTP 404
{"timestamp":"2026-10-21T17:07:49.313Z","status":404,"error":"Not Found","path":"/products/99"}
--- stock ---
$ curl -X GET localhost:8080/api/inventory/3
HTTP 200
{"productId":3,"quantity":3,"lowStock":true}
--- orders ---
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":2}'
HTTP 201
{"id":1,"productId":1,"productName":"Mechanical keyboard","quantity":2,"total":179.80,"currency":"EUR","status":"CONFIRMED"}
$ curl -X POST localhost:8080/api/orders -d '{"productId":3,"quantity":4}'
HTTP 409
{"timestamp":"2026-10-21T17:07:49.685Z","status":409,"error":"Conflict","path":"/orders"}
$ curl -X POST localhost:8080/api/orders -d '{"productId":2,"quantity":11}'
HTTP 400
{"timestamp":"2026-10-21T17:07:49.721Z","status":400,"error":"Bad Request","path":"/orders"}
$ curl -X POST localhost:8080/api/orders -d '{"productId":99,"quantity":1}'
HTTP 404
{"timestamp":"2026-10-21T17:07:49.763Z","status":404,"error":"Not Found","path":"/orders"}
$ curl -X GET localhost:8080/api/orders
HTTP 200
[{"id":1,"productId":1,"productName":"Mechanical keyboard","quantity":2,"total":179.80,"currency":"EUR","status":"CONFIRMED"}]
--- stock after the first order ---
$ curl -X GET localhost:8080/api/inventory/1
HTTP 200
{"productId":1,"quantity":18,"lowStock":false}
+12
View File
@@ -0,0 +1,12 @@
# One request, one trace id, four services. Lines are 'service | traceId-spanId | message' from each service's log.
--- POST /api/orders sent to the gateway; trace id 6ad8f16556352b627323a47cb0510c20 ---
gateway | 6ad8f16556352b627323a47cb0510c20-7323a47cb0510c20 | gateway received POST /api/orders
order-service | 6ad8f16556352b627323a47cb0510c20-fe6e6271c03b82ea | placing order for product 2 x1
product-service | 6ad8f16556352b627323a47cb0510c20-a01e14c8e2434acc | looking up product 2
inventory-service | 6ad8f16556352b627323a47cb0510c20-419570d38f840942 | reserved 1 of product 2, 7 left
order-service | 6ad8f16556352b627323a47cb0510c20-fe6e6271c03b82ea | order 2 is CONFIRMED
--- a caller that already has a trace id (W3C traceparent header): the same id comes out the other end ---
gateway | 4bf92f3577b34da6a3ce929d0e0e4736-91a3561198164d6c | gateway received GET /api/products/1
product-service | 4bf92f3577b34da6a3ce929d0e0e4736-bce2498c85b72fc7 | looking up product 1
+70
View File
@@ -0,0 +1,70 @@
# order-service -> inventory-service behind a Resilience4j circuit breaker (4-call window, 50% threshold, 5 s open)
--- healthy: circuit state, then one order ---
$ curl -X GET localhost:8080/api/orders/circuit
HTTP 200
{"state":"CLOSED","failedCalls":0,"notPermittedCalls":0}
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":1}'
HTTP 201
{"id":3,"productId":1,"productName":"Mechanical keyboard","quantity":1,"total":89.90,"currency":"EUR","status":"CONFIRMED"}
--- switch inventory-service to failing (POST /inventory/chaos?down=true), then place 4 orders ---
$ curl -X POST localhost:8080/api/inventory/chaos?down=true
HTTP 200
{"down":true}
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":1}'
HTTP 201
{"id":4,"productId":1,"productName":"Mechanical keyboard","quantity":1,"total":89.90,"currency":"EUR","status":"PENDING_INVENTORY"}
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":1}'
HTTP 201
{"id":5,"productId":1,"productName":"Mechanical keyboard","quantity":1,"total":89.90,"currency":"EUR","status":"PENDING_INVENTORY"}
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":1}'
HTTP 201
{"id":6,"productId":1,"productName":"Mechanical keyboard","quantity":1,"total":89.90,"currency":"EUR","status":"PENDING_INVENTORY"}
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":1}'
HTTP 201
{"id":7,"productId":1,"productName":"Mechanical keyboard","quantity":1,"total":89.90,"currency":"EUR","status":"PENDING_INVENTORY"}
$ curl -X GET localhost:8080/api/orders/circuit
HTTP 200
{"state":"OPEN","failedCalls":2,"notPermittedCalls":2}
--- circuit is OPEN: 2 more orders. inventory reserve calls before and after prove the calls were not even attempted ---
$ curl -X GET localhost:8080/api/inventory/stats
HTTP 200
{"reserveCalls":8}
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":1}'
HTTP 201
{"id":8,"productId":1,"productName":"Mechanical keyboard","quantity":1,"total":89.90,"currency":"EUR","status":"PENDING_INVENTORY"}
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":1}'
HTTP 201
{"id":9,"productId":1,"productName":"Mechanical keyboard","quantity":1,"total":89.90,"currency":"EUR","status":"PENDING_INVENTORY"}
$ curl -X GET localhost:8080/api/inventory/stats
HTTP 200
{"reserveCalls":8}
$ curl -X GET localhost:8080/api/orders/circuit
HTTP 200
{"state":"OPEN","failedCalls":2,"notPermittedCalls":4}
--- inventory-service recovers; wait 6 seconds (> the 5 s open window), then order again ---
$ curl -X POST localhost:8080/api/inventory/chaos?down=false
HTTP 200
{"down":false}
$ curl -X POST localhost:8080/api/orders -d '{"productId":1,"quantity":1}'
HTTP 201
{"id":10,"productId":1,"productName":"Mechanical keyboard","quantity":1,"total":89.90,"currency":"EUR","status":"CONFIRMED"}
$ curl -X GET localhost:8080/api/orders/circuit
HTTP 200
{"state":"CLOSED","failedCalls":0,"notPermittedCalls":0}
+9
View File
@@ -0,0 +1,9 @@
# order-service started with --shop.demo.primary-builder=false (the only RestClient.Builder is the @LoadBalanced one)
--- the log lines that give it away ---
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'scopedTarget.eurekaClient': Requested bean is currently in creation: Is there an unresolvable circular reference or an as
c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://localhost:8761/eureka/}, exception=No instances available for localhost
com.netflix.discovery.DiscoveryClient : DiscoveryClient_ORDER-SERVICE/order-service:8092 - registration failed Cannot execute request on any known server
--- is instance order-service:8092 in Eureka? ---
order-service:8092 registered: False