A production-style microservices e-commerce backend built with Spring Boot 3, Java 17, and PostgreSQL. Seven independently deployable services communicate through a central Spring Cloud Gateway. The checkout flow is orchestrated as a saga: synchronous payment processing followed by asynchronous stock decrements and Kafka-driven notifications.
Project status: Core service development is in progress. Full observability (distributed tracing, metrics, structured logging) is a planned phase and will be layered in once all services are complete — hence the project name.
- System Architecture
- Services Overview
- Key Design Decisions
- Kafka Topics
- API Reference
- Security Architecture
- Database Schema
- Tech Stack
- Running Locally
- Environment Variables
- Production Considerations
- Roadmap
+------------------------------------------+
Client Requests | API Gateway :8080 |
───────────────► | Spring Cloud Gateway · JWT Filter |
| Redis-backed rate limiting · CORS |
+--------------------+---------------------+
│ Routes by path prefix
+──────────────────────────────────┼──────────────────────────────+
│ │ │
▼ ▼ ▼
+──────────────────+ +──────────────────+ +──────────────────────+
│ User Service │ │ Product Service │ │ Cart Service │
│ :8081 │ │ :8082 │ │ :8083 │
│ Register/Login │ │ Catalog │ │ Add / Remove items │
│ JWT issuance │ Redis ◄──┤ Categories │──► Redis │ Qty update │
│ Profile CRUD │ │ Inventory │ │ Auto-total │
+──────────────────+ +──────────────────+ +──────────────────────+
│ own DB ▲ WebClient │ WebClient
▼ │ ▼
[user_db] +─────────────────────────────────────────────────────+
│ Order Service :8084 │
│ Checkout saga orchestrator │
│ 1. Batch fetch products │
│ 2. Persist PENDING order │
│ 3. Synchronous payment call │
│ 4. Async stock decrements (WebClient) │
│ 5. Publish Kafka event (fire-and-forget) │
+───────────────┬─────────────────────────────────────+
│
┌────────────────────┴─────────────────────┐
▼ ▼
+──────────────────────+ +─────────────────────────────+
│ Payment Service │ │ Notification Service │
│ :8085 │ │ :8086 │
│ Mock charge/refund │ │ Kafka consumer │
│ 90 % success rate │ │ Mock email/SMS dispatch │
+──────────────────────+ │ Audit log of sends │
+─────────────────────────────+
┌────────────────────────┐
│ Shared Middleware │
│ Redis 7 (cache/rate) │
│ Kafka + Zookeeper 7.6 │
└────────────────────────┘
| Service | Port | Database | Responsibility |
|---|---|---|---|
| API Gateway | 8080 | — | Route all requests, JWT validation, CORS, rate limit |
| User Service | 8081 | user_db |
Register, Login (JWT), Profile CRUD |
| Product Service | 8082 | product_db |
Catalog, Categories, Inventory management |
| Cart Service | 8083 | cart_db |
Per-user cart, denormalised product snapshots |
| Order Service | 8084 | order_db |
Checkout saga (product → payment → Kafka notify) |
| Payment Service | 8085 | payment_db |
Mock payment processing and refunds |
| Notification Service | 8086 | notification_db |
Kafka-driven email/SMS dispatch and audit log |
JWT tokens (HS256, 24-hour expiry) are issued by the User Service and validated once at the API Gateway. The gateway injects X-User-Email and X-Auth-Token headers into all downstream requests. Downstream services never handle raw JWTs — they rely solely on these trusted headers.
Each service owns its schema exclusively. There are no cross-service JOINs or shared tables. This enforces bounded contexts and allows each service to evolve its schema and scale independently.
Cart items and order items capture a snapshot of product data (name, SKU, price) at the time of the add or checkout event. This preserves historical accuracy — a subsequent price change on a product does not alter past carts or completed orders.
The Order Service orchestrates the checkout saga in the following sequence:
- Batch-fetch all product details in a single WebClient call (eliminates N+1 remote calls)
- Validate stock availability for every item
- Persist the order with status
PENDING - Call the Payment Service synchronously — the result determines order status
- On
SUCCESS: set status toCONFIRMED, asynchronously decrement each product's stock (WebClient, fire-and-forget) - Publish a Kafka event (
order.confirmedororder.payment-failed) — the Notification Service consumes it asynchronously
Why hybrid? Payment is synchronous because the API response must reflect success or failure. Stock decrements and notifications are asynchronous because they are non-fatal background side-effects that should not block the client.
The Notification Service is a pure Kafka consumer. It listens on order.confirmed and order.payment-failed topics, constructs and dispatches the appropriate notification, and persists an audit record — all without being coupled to the Order Service via HTTP.
Redis is shared across the API Gateway (rate limiting) and the product/user/cart services (response caching). Each service connects to the same Redis instance via Lettuce with a bounded connection pool.
Each service declares its own local DTOs for inter-service calls. This avoids shared-library coupling, keeps each service independently deployable, and allows DTOs to evolve at different rates.
| Topic | Producer | Consumer | Payload |
|---|---|---|---|
order.confirmed |
Order Service | Notification Service | orderId, userId, transactionId, totalAmount |
order.payment-failed |
Order Service | Notification Service | orderId, userId, reason |
Consumer group: notification-service-group (ensures exactly-once processing per message when scaled horizontally).
The full API is documented via springdoc-openapi. After starting all containers, access the centralised Swagger UI at:
http://localhost:8080/swagger-ui.html
Use the "Select a definition" dropdown (top-right) to switch between the APIs of each microservice without changing ports.
POST /api/v1/auth/register Register a new user account and receive a JWT
POST /api/v1/auth/login Authenticate with credentials and receive a JWT
GET /api/v1/users/me Retrieve the authenticated user's profile
PATCH /api/v1/users/me Update the authenticated user's profile
GET /api/v1/users [ADMIN] List all users
GET /api/v1/users/{id} [ADMIN] Get user by ID
DELETE /api/v1/users/{id} [ADMIN] Delete user by ID
GET /api/v1/products Paginated list of active products
GET /api/v1/products/{id} Get product by ID
GET /api/v1/products/sku/{sku} Get product by SKU
GET /api/v1/products/category/{catId} Filter products by category
GET /api/v1/products/search?keyword= Full-text keyword search
POST /api/v1/products Create a product
POST /api/v1/products/batch [Internal] Batch-fetch products by ID list (used by order-service)
PATCH /api/v1/products/{id} Partial update of a product
PATCH /api/v1/products/{id}/inventory Update stock (ADD / SUBTRACT / SET)
DELETE /api/v1/products/{id} Soft-delete a product
GET /api/v1/categories List all active categories
GET /api/v1/categories/{id} Get category by ID
POST /api/v1/categories Create a category
PUT /api/v1/categories/{id} Update a category
DELETE /api/v1/categories/{id} Soft-delete a category
GET /api/v1/cart Retrieve the authenticated user's cart
POST /api/v1/cart/items Add an item (merges quantity if duplicate)
PATCH /api/v1/cart/items/{itemId} Update item quantity (quantity = 0 removes it)
DELETE /api/v1/cart/items/{itemId} Remove a specific item
DELETE /api/v1/cart Clear the entire cart
POST /api/v1/orders Initiate checkout (triggers full saga)
GET /api/v1/orders/{id} Get order by ID
GET /api/v1/orders/user/{userId} Paginated order history for a user
POST /api/v1/orders/{id}/cancel Cancel an order (PENDING or CONFIRMED only)
PATCH /api/v1/orders/{id}/status [ADMIN] Manually update order status
POST /api/v1/payments/charge Process a payment charge
POST /api/v1/payments/refund Refund a successful payment
GET /api/v1/payments/{id} Get payment by ID
GET /api/v1/payments/order/{id} Get all payments for an order
GET /api/v1/payments/user/{id} Get payment history for a user
POST /api/v1/notifications/send Send a notification manually
GET /api/v1/notifications/reference/{id} Get notifications by reference ID
GET /api/v1/notifications/recipient?email= Get notifications by recipient email
JWT issued by user-service (HS256)
Expiry: configurable via JWT_EXPIRATION (default 30 min; .env template sets 24 h)
│
└─► Validated by API Gateway on every inbound request
│
└─► Two trusted headers injected into downstream requests:
X-User-Email — used by user-service and order-service
X-User-Id — used by cart-service
│
└─► Downstream services trust these headers (no re-validation)
| Mechanism | Implementation |
|---|---|
| Password hashing | BCrypt (strength 10) |
| JWT algorithm | HS256, signed with JWT_SECRET environment variable |
| JWT expiry | Configurable via JWT_EXPIRATION (ms); default 30 min |
| Session management | Stateless (no HttpSession) |
| Roles | CUSTOMER (default) and ADMIN |
| Role authorisation | @PreAuthorize("hasRole('ADMIN')") on admin endpoints |
| Input validation | Jakarta Bean Validation on all request DTOs |
| Type coercion | Rejected — JSON field types are strictly enforced |
| Service | Key Tables |
|---|---|
| user-service | users (id, email, password_hash, role, enabled) |
| product-service | products (id, sku, price, stock_quantity, category_id, active), categories |
| cart-service | carts (id, user_id, total_price), cart_items (product snapshot per line) |
| order-service | orders (id, user_id, status, total_amount, payment_txn_id), order_items |
| payment-service | payments (id, transaction_id, order_id, amount, status, method) |
| notification-service | notifications (id, recipient, subject, type, status, reference_id, sent_at) |
Schema management: Hibernate DDL auto = update — tables are created or migrated on startup.
| Concern | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.2.3 |
| Gateway | Spring Cloud Gateway 2023.0.0 |
| Security | Spring Security 6 + JWT (jjwt 0.11.5) |
| Persistence | Spring Data JPA + Hibernate |
| Database | PostgreSQL 16 (one instance per service) |
| Caching | Redis 7 (Lettuce client, connection pooled) |
| Messaging | Apache Kafka 3.6 + Zookeeper 7.6 |
| HTTP Client | Spring WebFlux WebClient |
| Validation | Jakarta Bean Validation |
| Mapping | MapStruct 1.5.5 |
| API Docs | springdoc-openapi 2.3.0 (Swagger UI) |
| Boilerplate | Lombok 1.18.30 |
| Containerisation | Docker + Docker Compose |
| Build | Maven (multi-module parent POM) |
- Docker Desktop with Compose V2 (
docker compose— notdocker-compose) - Java 17 and Maven 3.9+ to build JARs before Docker image creation
- ~4 GB of free RAM — each of the 7 application containers is capped at 512 MB; add the 6 PostgreSQL instances, Redis, and Kafka/Zookeeper
Create a .env file in the project root (it is listed in .gitignore and must not be committed):
POSTGRES_USER=postgres
POSTGRES_PASSWORD=YourSecurePassword!
JWT_SECRET=<64-char-hex-string>
JWT_EXPIRATION=86400000
COMPOSE_PARALLEL_LIMIT=3Tip: Generate a strong JWT secret with
openssl rand -hex 32.
A reference template is shown above. The COMPOSE_PARALLEL_LIMIT=3 cap prevents a BuildKit pipe crash that occurs when all 7 services try to build simultaneously against the same large build context.
Each Dockerfile performs a self-contained multi-stage Maven build inside Docker, so a local Maven build is not required before docker compose up --build. However, running it locally first caches dependencies and speeds up the Docker build significantly:
# Optional — run from project root; builds all modules via the parent POM
mvn clean package -DskipTestsdocker compose up --buildDocker Compose will start (in dependency order): 6 × PostgreSQL → Redis → Zookeeper → Kafka → 7 × microservices.
# Check all container states
docker compose ps
# Verify the API Gateway is healthy
curl http://localhost:8080/actuator/health# Register a new user
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@example.com",
"password": "Password1!",
"phone": "+919876543210"
}'
# Login to obtain a JWT
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com", "password": "Password1!"}'
# Access a protected endpoint
curl http://localhost:8080/api/v1/users/me \
-H "Authorization: Bearer <YOUR_TOKEN>"TOKEN="<YOUR_JWT_TOKEN>"
# 1. Create a category
curl -X POST http://localhost:8080/api/v1/categories \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Electronics", "description": "Electronics and Gadgets"}'
# 2. Create a product (use the categoryId returned above)
curl -X POST http://localhost:8080/api/v1/products \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Laptop", "sku": "LAP-001", "price": 59999.0, "stockQuantity": 50, "categoryId": 1}'
# 3. Place an order — triggers the full checkout saga
curl -X POST http://localhost:8080/api/v1/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"shippingAddress": "123 Main Street, Hyderabad, Telangana",
"paymentMethod": "CREDIT_CARD",
"items": [{"productId": 1, "quantity": 2}]
}'# Stop and remove all containers; -v also removes named volumes (clears all database data)
docker compose down -v| Variable | Description | Example |
|---|---|---|
POSTGRES_USER |
PostgreSQL username used by all database containers | postgres |
POSTGRES_PASSWORD |
PostgreSQL password used by all database containers | SecurePass! |
JWT_SECRET |
HS256 signing secret (min 64 hex characters) | 404E635266556A58... |
JWT_EXPIRATION |
Token expiry in milliseconds | 86400000 (24 hours) |
COMPOSE_PARALLEL_LIMIT |
Max concurrent Docker Compose service builds | 3 |
The following enhancements are recommended before promoting this system to a production environment:
| Area | Recommendation |
|---|---|
| Kafka Resilience | Add dead-letter topics for failed Notification consumer messages |
| Kafka Reliability | Increase replication factor to ≥ 2 in production (currently 1 — dev default) |
| Resilience | Add retry and circuit-breaker policies via Resilience4j on WebClient calls |
| Service Discovery | Integrate Spring Cloud Eureka or switch to Kubernetes DNS |
| Configuration | Centralise configuration with Spring Cloud Config Server |
| Distributed Tracing | Add end-to-end tracing with Micrometer Tracing + Zipkin or Tempo |
| Metrics & Alerting | Expose Actuator metrics to Prometheus; alert via Grafana |
| Payments | Replace the mock payment processor with Stripe or Razorpay |
| Notifications | Integrate JavaMailSender for real email; Twilio for SMS |
| Database Migrations | Replace ddl-auto: update with Flyway or Liquibase for controlled migrations |
| Secrets Management | Store JWT_SECRET and DB credentials in Vault or a cloud secrets manager |
| API Versioning | Enforce versioning via URL path (/api/v2/) or Accept request headers |
The following capabilities are planned for upcoming development phases:
| Capability | Planned Implementation |
|---|---|
| Distributed Tracing | Micrometer Tracing + Zipkin (or Grafana Tempo) |
| Metrics Collection | Micrometer → Prometheus scrape endpoint on every service |
| Dashboards & Alerting | Grafana dashboards with RED metrics (Rate, Errors, Duration) |
| Structured Logging | Logback JSON encoder; correlation IDs propagated via MDC |
| Health Aggregation | Spring Boot Admin or custom Gateway health dashboard |
| Capability | Planned Implementation |
|---|---|
| Resilience | Resilience4j circuit-breakers and retries on all WebClient calls |
| Kafka Reliability | Increase replication factor and add dead-letter topics (DLT) |
| Database Migrations | Flyway replacing ddl-auto: update |
| Secrets Management | Environment-agnostic secrets via HashiCorp Vault or cloud KMS |
| Real Payments | Stripe or Razorpay integration replacing the mock processor |
| Real Notifications | JavaMailSender (email) + Twilio (SMS) replacing mock dispatch |