Spring Boot microservice suite for a simple e-commerce workflow with separate services for catalog items, inventory reservation, order placement, order lifecycle tracking, and service discovery.
CNKart started as a monolithic REST API and is now organized as a small microservice system. The split gives each business concern its own lifecycle, which makes the project easier to reason about and a better fit for distributed-systems learning.
The services work together like this:
itemmanages catalog items.inventorychecks stock availability and reserves stock for an order before confirmation.ordercreates traceable order references, tracks order status, handles duplicate order submissions with an idempotency key, and confirms orders only after inventory reservation succeeds.discovery-serveracts as the Eureka registry for the service suite.
# Clone and start everything
git clone https://github.com/byte2code/cnkart.git
cd cnkart
docker compose up --build
# Run the smoke test (requires curl + jq)
./scripts/smoke-test.shThe docker-compose.override.yml ships sensible defaults — no .env file is needed for a first run.
All services extract their configuration to environment variables. You must set these if running manually:
SPRING_DATASOURCE_URL,SPRING_DATASOURCE_USERNAME,SPRING_DATASOURCE_PASSWORDSPRING_KAFKA_BOOTSTRAP_SERVERSEUREKA_CLIENT_SERVICEURL_DEFAULTZONESERVER_PORT
- Spring Boot REST APIs
- Spring Data JPA persistence
- MySQL-backed service databases
- Eureka service discovery
- OpenFeign-based service-to-service communication
- Resilience4j circuit breaker fallback handling in the order flow
- Order lifecycle states:
PENDING,CONFIRMED,REJECTED, andFAILED - Idempotency key handling to avoid duplicate order creation on retries
- Inventory reservation before order confirmation
- Pessimistic locking during stock reservation to reduce overselling risk
- Kafka domain events for order creation, inventory reservation, inventory rejection, and order confirmation
- Kafka consumer stub in order service — closes the event loop by listening to inventory events
- Generated order references for easier order tracing
- Swagger / OpenAPI documentation for order and inventory service endpoints
@ControllerAdviceglobal error handling with structuredApiErrorresponses across all business servicesdocker-compose.override.ymlwith env-var defaults for clone-and-run setup- Curl-based smoke test script to verify the full system
- Service-local configuration files for each module
- Independent service startup and runtime lifecycle
- Java 8+
- Spring Boot 2.7.13
- Spring Cloud 2021.0.8
- Spring Web
- Spring Data JPA
- Spring Cloud Netflix Eureka
- Spring Cloud OpenFeign
- Spring Cloud Circuit Breaker Resilience4j
- Spring Kafka
- springdoc-openapi (Swagger UI)
- MySQL
- Docker & Docker Compose
- Lombok
| Service | Port | Responsibility |
|---|---|---|
discovery-server |
8761 |
Eureka registry for service registration |
item |
8081 |
Create and list catalog items |
order |
8082 |
Create idempotent orders and track status after inventory reservation |
inventory |
8083 |
Check stock and reserve available quantity for orders |
| Service | Swagger UI | OpenAPI JSON |
|---|---|---|
item |
http://localhost:8081/swagger-ui.html | http://localhost:8081/v3/api-docs |
order |
http://localhost:8082/swagger-ui.html | http://localhost:8082/v3/api-docs |
inventory |
http://localhost:8083/swagger-ui.html | http://localhost:8083/v3/api-docs |
flowchart TB
Client["🖥️ Client / Postman / curl"]
subgraph Discovery["Service Discovery"]
Eureka["discovery-server :8761"]
end
subgraph Services["Business Services"]
Item["item-service :8081"]
Order["order-service :8082"]
Inventory["inventory-service :8083"]
end
subgraph Infra["Infrastructure"]
MySQL[("MySQL :3306")]
Kafka[("Kafka :9092")]
end
Client --> Item
Client --> Order
Item -.->|registers| Eureka
Order -.->|registers| Eureka
Inventory -.->|registers| Eureka
Order -->|"OpenFeign + Resilience4j"| Inventory
Item --> MySQL
Order --> MySQL
Inventory --> MySQL
Order -->|publishes| Kafka
Inventory -->|publishes| Kafka
Order -->|"consumes (stub)"| Kafka
sequenceDiagram
participant C as Client
participant O as order-service
participant K as Kafka
participant I as inventory-service
C->>O: POST /api/order (skuCode, qty, idempotencyKey)
alt Duplicate idempotencyKey
O-->>C: 201 — existing order (status + reference)
else New order
O->>O: Save order as PENDING
O->>K: OrderCreated event
O->>I: POST /api/inventory/reservations
alt Reservation succeeds
I->>I: Deduct stock (pessimistic lock)
I->>K: InventoryReserved event
I-->>O: reserved = true
O->>O: Update order to CONFIRMED
O->>K: OrderConfirmed event
O-->>C: 201 — CONFIRMED
else Insufficient stock
I->>K: InventoryRejected event
I-->>O: reserved = false
O->>O: Update order to REJECTED
O-->>C: 201 — REJECTED
else Service unavailable
O->>O: Circuit breaker fallback
O->>O: Update order to FAILED
O-->>C: 201 — FAILED
end
end
| Event | Topic | Producer | Consumer | Meaning |
|---|---|---|---|---|
OrderCreated |
cnkart.order.events |
order |
— | An order was accepted and stored in PENDING state |
OrderConfirmed |
cnkart.order.events |
order |
— | The order was confirmed after inventory reservation succeeded |
InventoryReserved/Rejected |
cnkart.inventory.events |
inventory |
order |
Order service logs reservation outcome for observability |
OrderCreated
{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"idempotencyKey": "checkout-1-user-42",
"skuCode": "1",
"quantity": 2,
"status": "PENDING",
"message": "Order created and stored in PENDING state"
}InventoryReserved
{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"skuCode": "1",
"requestedQuantity": 2,
"availableQuantity": 8,
"eventType": "InventoryReserved",
"message": "Inventory reserved successfully"
}InventoryRejected
{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"skuCode": "1",
"requestedQuantity": 200,
"availableQuantity": 10,
"eventType": "InventoryRejected",
"message": "Insufficient stock available for reservation"
}OrderConfirmed
{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"idempotencyKey": "checkout-1-user-42",
"skuCode": "1",
"quantity": 2,
"status": "CONFIRMED",
"message": "Order confirmed after inventory reservation"
}| Status | Meaning |
|---|---|
PENDING |
Order command has been accepted and stored before inventory validation |
CONFIRMED |
Inventory reservation succeeded and stock was deducted |
REJECTED |
Inventory reservation was declined because stock was unavailable or invalid |
FAILED |
Inventory reservation failed because of an unavailable dependency or invalid order data |
curl -X POST http://localhost:8081/api/item \
-H "Content-Type: application/json" \
-d '{
"name": "Wireless Mouse",
"description": "2.4 GHz ergonomic mouse",
"price": 799.00
}'Expected result:
201 Created
curl http://localhost:8081/api/itemSample response:
[
{
"id": 1,
"name": "Wireless Mouse",
"description": "2.4 GHz ergonomic mouse",
"price": 799.00
}
]curl "http://localhost:8083/api/inventory?skuCode=1&qty=2"Sample response:
truecurl -X POST http://localhost:8083/api/inventory/reservations \
-H "Content-Type: application/json" \
-d '{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"skuCode": "1",
"quantity": 2
}'Sample response:
{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"skuCode": "1",
"requestedQuantity": 2,
"availableQuantity": 8,
"reserved": true,
"message": "Inventory reserved successfully"
}curl -X POST http://localhost:8082/api/order \
-H "Content-Type: application/json" \
-d '{
"skuCode": "1",
"price": 799.00,
"quantity": 2,
"idempotencyKey": "checkout-1-user-42"
}'Success response:
{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"idempotencyKey": "checkout-1-user-42",
"status": "CONFIRMED",
"message": "Order confirmed after inventory reservation"
}Rejected response:
{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"idempotencyKey": "checkout-1-user-42",
"status": "REJECTED",
"message": "Insufficient stock available for reservation"
}Duplicate request response:
{
"orderReference": "ORD-6d6f7b78-1a7d-42de-a2df-4ccdc7f72cc5",
"idempotencyKey": "checkout-1-user-42",
"status": "CONFIRMED",
"message": "Duplicate order request detected, returning existing order status"
}Use the same idempotencyKey when retrying the same checkout request. The order service returns the existing order instead of creating a duplicate record.
When an endpoint returns an error, the response follows a consistent structure across all services:
{
"timestamp": "2026-06-29T00:10:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "Something went wrong",
"path": "/api/order"
}The repo includes a curl-based smoke test script at scripts/smoke-test.sh that proves the system runs end-to-end:
# Default (localhost)
./scripts/smoke-test.sh
# Custom host
./scripts/smoke-test.sh http://192.168.1.100The script exercises:
- Item service — create + list items
- Inventory service — stock check + reservation
- Order service — place order + idempotency retry
- Swagger / OpenAPI — verify docs endpoints
- Discovery server — Eureka dashboard reachability
Requires curl and jq.
cnkart/
├── discovery-server/ # Eureka registry
├── item/ # Catalog item service
├── inventory/ # Stock + reservation service
├── order/ # Order placement service
├── docker/
│ └── mysql/
│ └── init/ # DB init scripts (creates schemas)
├── scripts/
│ └── smoke-test.sh # Curl-based end-to-end smoke test
├── docker-compose.yml # Full stack definition
├── docker-compose.override.yml # Env var defaults for clone-and-run
├── SERVICE_STARTUP.md # Startup guide
├── CHANGELOG.md
├── README.md
└── .gitignore
Each service keeps its own pom.xml, mvnw, source tree, and configuration file so it can be built and run independently.
git clone https://github.com/byte2code/cnkart.git
cd cnkart
docker compose up --buildThe docker-compose.override.yml supplies default values for all environment variables, so no .env file is required. To customise any setting, either export the variable or create a .env file at the project root.
See SERVICE_STARTUP.md for the full startup guide.
- Start MySQL and create the databases
item_service,inventory_service, andorder_service. - Start Kafka and make sure it is reachable on
localhost:9092. - Start
discovery-serveron port8761. - Start
inventoryon port8083. - Start
itemon port8081. - Start
orderon port8082. - Call the endpoints above from Postman, curl, or any REST client.
The local configuration files currently point to localhost MySQL settings, so update the database username and password if your environment differs.
The repo includes Dockerfiles for each service and a root docker-compose.yml that starts:
discovery-serveriteminventoryorder- MySQL
- Kafka + Zookeeper
The compose setup uses environment variables so the same services can run in containers or directly from your IDE without code changes.
- Converting a monolith into a microservice suite
- Using Eureka to register and discover services
- Calling one service from another with OpenFeign
- Keeping order placement resilient with Resilience4j fallback
- Modeling order state transitions instead of only saving successful orders
- Using idempotency keys to make retry behavior safe for checkout APIs
- Reserving inventory before confirmation instead of only checking stock availability
- Applying pessimistic locking during reservation to protect stock updates
- Returning traceable order references to support debugging and future event workflows
- Emitting Kafka events from the order and inventory services for downstream workflows
- Closing the event loop with a Kafka consumer stub in the order service
- Documenting REST APIs with Swagger / OpenAPI
- Standardising error responses with
@ControllerAdviceand structured DTOs - Separating service data, config, and startup responsibility
- Practicing REST, JPA, and distributed-system wiring in one repo
- The legacy monolithic CNKart codebase has been replaced by the microservice suite in this version.
- Configuration files are retained in each service folder for local setup.
- Build artifacts and IDE-specific files are intentionally excluded from the repository.