A production-grade Java 21 / Spring Boot 3.4.x microservices project demonstrating synchronous (Feign + Resilience4j) and asynchronous (Kafka) inter-service communication, Redis caching, distributed tracing, and API Gateway routing.
┌─────────────────────┐
│ API Gateway │ :8080
│ (Spring Cloud GW) │
└──────────┬──────────┘
┌─────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Order Service│ │ Inventory │ │Payment / Notif. │
│ :8081 │ │ Service │ │ Service :8083 │
│ │ │ :8082 │ │ Service :8084 │
└──────┬───────┘ └──────┬───────┘ └────────┬─────────┘
│ Feign(sync) │ │
└────────────────►│ │
│ │ Kafka consumers
│ Kafka(async) │ ORDER_PLACED topic
└─────────────────────────────────────►│
│
┌────────────────────────────────────────────────────────┐
│ Infrastructure (Docker) │
│ PostgreSQL×3 │ Redis │ Kafka+ZK │ Zipkin │
└────────────────────────────────────────────────────────┘
| Flow | Protocol | Detail |
|---|---|---|
| Client → Services | HTTP | Via API Gateway routes |
| Order → Inventory | Feign (REST) | Synchronous stock check with Resilience4j circuit breaker |
| Order → Payment/Notification | Kafka | Async ORDER_PLACED event, fire-and-forget |
| Payment DLQ | Kafka | @RetryableTopic (4 attempts) + @DltHandler |
| Notification | Java async | CompletableFuture.allOf(email, sms).join() |
| Layer | Technology |
|---|---|
| Language | Java 21 + Virtual Threads (spring.threads.virtual.enabled=true) |
| Framework | Spring Boot 3.4.3, Spring Cloud 2024.0.1 |
| Sync comms | Spring Cloud OpenFeign + Resilience4j Circuit Breaker |
| Async comms | Apache Kafka (ORDER_PLACED topic) |
| Persistence | PostgreSQL 16 via Spring Data JPA (3 separate DBs) |
| Caching | Redis 7.2 via Spring Cache (GenericJackson2JsonRedisSerializer) |
| Tracing | Micrometer Tracing (Brave) + Zipkin |
| Concurrency | @Version optimistic locking, @Retryable, CompletableFuture |
| Gateway | Spring Cloud Gateway (Netty, reactive — no servlet web starter) |
| Build | Maven Multi-Module (parent + 5 child modules) |
| Infra | Docker Compose |
java_microservice/
├── pom.xml ← parent (Spring Boot 3.4.3, Cloud 2024.0.1, Java 21)
├── docker-compose.yml ← all infrastructure services
│
├── order-service/ :8081
│ └── src/main/java/com/ecommerce/orderservice/
│ ├── config/ FeignConfig, KafkaProducerConfig, RedisConfig
│ ├── controller/ OrderController
│ ├── dto/ OrderRequest, OrderResponse, InventoryResponse
│ ├── entity/ Order
│ ├── event/ OrderPlacedEvent
│ ├── exception/ InsufficientStockException, GlobalExceptionHandler
│ ├── feign/ InventoryFeignClient, InventoryFeignFallback
│ ├── repository/ OrderRepository
│ └── service/ OrderService (@CircuitBreaker)
│
├── inventory-service/ :8082
│ └── src/main/java/com/ecommerce/inventoryservice/
│ ├── config/ RedisConfig
│ ├── controller/ InventoryController
│ ├── dto/ InventoryResponse, StockCheckRequest
│ ├── entity/ Inventory (@Version optimistic lock)
│ ├── exception/ OutOfStockException, GlobalExceptionHandler
│ ├── repository/ InventoryRepository
│ └── service/ InventoryService (@Cacheable, @Retryable)
│
├── payment-service/ :8083
│ └── src/main/java/com/ecommerce/paymentservice/
│ ├── config/ KafkaConsumerConfig
│ ├── dto/ PaymentRecord
│ ├── entity/ Payment
│ ├── event/ OrderPlacedEvent
│ ├── exception/ PaymentProcessingException
│ ├── listener/ OrderEventListener (@RetryableTopic, @DltHandler)
│ ├── repository/ PaymentRepository
│ └── service/ PaymentService
│
├── notification-service/ :8084
│ └── src/main/java/com/ecommerce/notificationservice/
│ ├── config/ AsyncConfig (ThreadPoolTaskExecutor)
│ ├── dto/ NotificationPayload
│ ├── event/ OrderPlacedEvent
│ ├── listener/ NotificationListener
│ └── service/ NotificationService (CompletableFuture.allOf)
│
└── api-gateway/ :8080
└── src/main/java/com/ecommerce/apigateway/
└── ApiGatewayApplication.java
| Tool | Version | Install |
|---|---|---|
| Java | 21 | brew install openjdk@21 |
| Maven | 3.9+ | brew install maven |
| Docker Desktop | 4.x+ | docker.com |
Important: Lombok is incompatible with Java 25. Always use Java 21 to build.
Add to
~/.zshrc:export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home" export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"
cd java_microservice
docker compose up -d
docker compose ps # all should show (healthy)Infrastructure ports:
| Service | Host Port |
|---|---|
| order-db (PostgreSQL) | 5435 |
| inventory-db (PostgreSQL) | 5433 |
| payment-db (PostgreSQL) | 5434 |
| Redis | 6379 |
| Kafka | 9092 |
| Zipkin | 9411 |
Note: order-db uses port 5435 (not 5432) to avoid conflict with any locally-installed PostgreSQL.
mvn clean install -DskipTestsOpen 5 separate terminals, or run them in the background:
# Terminal 1 — inventory first (order-service calls it via Feign)
java -jar inventory-service/target/inventory-service-1.0.0-SNAPSHOT.jar
# Terminal 2
java -jar order-service/target/order-service-1.0.0-SNAPSHOT.jar
# Terminal 3
java -jar payment-service/target/payment-service-1.0.0-SNAPSHOT.jar
# Terminal 4
java -jar notification-service/target/notification-service-1.0.0-SNAPSHOT.jar
# Terminal 5
java -jar api-gateway/target/api-gateway-1.0.0-SNAPSHOT.jardocker exec inventory-db psql -U postgres -d inventory_db -c "
INSERT INTO inventory (sku_code, quantity, version)
VALUES ('SKU-LAPTOP-001', 50, 0),
('SKU-PHONE-002', 5, 0),
('SKU-EMPTY-003', 0, 0)
ON CONFLICT (sku_code) DO NOTHING;
"curl http://localhost:8080/actuator/health # gateway
curl http://localhost:8081/actuator/health # order
curl http://localhost:8082/actuator/health # inventory
curl http://localhost:8083/actuator/health # payment
curl http://localhost:8084/actuator/health # notificationAll requests go through the gateway on port 8080.
| Route | Target | Method |
|---|---|---|
/api/orders/** |
order-service :8081 | POST |
/api/inventory/** |
inventory-service :8082 | GET |
/api/payments/** |
payment-service :8083 | — |
/api/notifications/** |
notification-service :8084 | — |
POST /api/orders
Content-Type: application/json
{
"skuCode": "SKU-LAPTOP-001",
"quantity": 2,
"price": 999.99,
"customerEmail": "alice@example.com",
"customerPhone": "+91-9876543210"
}201 Created:
{
"id": 1,
"orderNumber": "5a4af749-0e5a-49e5-bc7a-34dc52e540ec",
"skuCode": "SKU-LAPTOP-001",
"quantity": 2,
"price": 999.99,
"status": "PLACED",
"createdAt": "2026-02-21T12:27:51.410159"
}409 Conflict (out of stock):
{
"title": "Insufficient Stock",
"status": 409,
"detail": "Insufficient stock for SKU: SKU-EMPTY-003"
}GET /api/inventory/check?skuCode=SKU-LAPTOP-001&quantity=2
{
"skuCode": "SKU-LAPTOP-001",
"inStock": true,
"availableQuantity": 50
}Open http://localhost:9411 in a browser. Traces are sampled at 100% (probability: 1.0).
All service logs include trace/span IDs:
INFO [order-service,69995818979df9c820a3bc44718b825f,28359cae5ffcffcc] ...
Each service exposes:
/actuator/health/actuator/info/actuator/metrics/actuator/prometheus
| Issue | Root Cause | Fix Applied |
|---|---|---|
| Lombok fails on Java 25 | TypeTag::UNKNOWN — Lombok incompatible |
Use Java 21 (openjdk@21) |
Feign fallback= ignored |
FeignCachingInvocationHandlerFactory bypasses CB in Spring Cloud 2024 |
Switched to @CircuitBreaker on OrderService.placeOrder() |
| order-db port 5435 | Local PostgreSQL occupies 5432 | docker-compose.yml maps 5435→5432 for order-db |
| Redis 500 error | InventoryResponse not Serializable |
Added RedisConfig (Jackson serializer) to inventory-service |