A production-grade e-commerce backend built as event-driven microservices (Java / Spring Boot, plus a .NET shipping service), with an Angular admin console. Put any frontend in front of it — React, Next.js, Vue, Angular, Flutter, React Native, or plain HTTP — and you have a complete store.
All client traffic goes through a single API Gateway on port 8080. The gateway verifies the JWT, enforces roles, and forwards the caller's identity to the services. Services register with Eureka, pull configuration from Config Server, and talk to each other asynchronously over Kafka.
- One entry point — every request goes to
localhost:8080(or your domain). - JWT auth with refresh tokens — register, verify email, log in, refresh, log out, reset password.
- Identity is set by the gateway — the gateway derives
X-User-Id/X-User-Rolefrom the token and strips any client-supplied copies, so callers can't impersonate another user. - Role-based access — catalog writes and
/api/v1/admin/**requireADMIN, checked at the gateway and again in each service (security-lib). - Event-driven order flow — order → stock reservation → payment → shipment → notifications, all over Kafka, with idempotent consumers.
- Self-healing workflows — unpaid reservations expire and their orders are cancelled automatically; failed emails are retried with backoff and dead-lettered.
- Real email delivery — SMTP or SendGrid, HTML templates; Mailpit catches every email locally.
- Schema-per-service PostgreSQL with Flyway migrations where the schema is evolving.
- Polyglot — the shipping service is ASP.NET Core 8, wired into the same Eureka, Config Server and Kafka.
- CI on every PR — Java and .NET builds and tests, plus Docker image builds.
Clients (web, mobile) Admin console (Angular) :4200
│ │
└──────────────┬───────────────┘
▼
API Gateway :8080 JWT check · role rules · identity headers
│
┌────────────────────────┼─────────────────────────────────────┐
│ user-service :8082 auth, email verification, password reset
│ product-service :8081 catalog, categories, search
│ cart-service :8083 Redis-backed cart
│ order-service :8084 order lifecycle, admin order API, cancellation
│ inventory-service :8085 stock, reservations, expiry sweeper
│ payment-service :8086 payment simulation
│ notification-service :8087 email delivery, retry, DLQ
│ shipping-service :8088 shipments, tracking numbers (.NET 8)
└────────────────────────┼─────────────────────────────────────┘
│
PostgreSQL :5432 · Redis :6379 · Kafka :9092 (+ ZooKeeper :2181)
Eureka :8761 · Config Server :8888 · Mailpit :8025 (UI) / :1025 (SMTP)
order.created ─► inventory reserves stock
─► payment
├─ payment.successful ─► order marked paid · inventory commits stock
│ ─► shipping creates shipment ─► shipment.dispatched ─► shipment.delivered ─► order updated
└─ payment.failed ─► order marked failed · inventory releases stock
reservation not paid in time ─► inventory.reservation-expired ─► order cancelled ─► order.cancelled
every customer-facing event ─► notification-service ─► email (failures ─► retry ─► notification.dlq)
| Layer | Technology |
|---|---|
| Services | Java 21, Spring Boot 3.3.5 · ASP.NET Core 8 (shipping) |
| Gateway / discovery / config | Spring Cloud Gateway · Eureka · Spring Cloud Config (Steeltoe on .NET) |
| Data | PostgreSQL 16 (schema per service) · Redis 7 · Flyway / EF Core |
| Messaging | Apache Kafka (Confluent 7.6, ZooKeeper mode) |
| Auth | JWT (HS256) + refresh tokens, shared security-lib |
| Thymeleaf templates · SMTP / SendGrid · Mailpit for local dev | |
| Admin UI | Angular 18, served by nginx in Docker |
| Build | Maven 3.9 multi-module · .NET SDK 8 · npm |
| Tests | JUnit 5, Mockito, Testcontainers, embedded Kafka · xUnit · Karma |
| CI | GitHub Actions |
| API docs | SpringDoc OpenAPI 3 / Swagger UI |
.github/workflows/ci.yml CI: Java, .NET and Docker build jobs
shopsphere/
pom.xml Maven parent (all Java modules)
docker-compose.yml full local stack
common-lib/ shared Kafka event classes and topic names
security-lib/ shared JWT verification / role auto-configuration
service-registry/ Eureka
config-service/ Spring Cloud Config (configs in src/main/resources/config)
api-gateway/
user-service/ product-service/ cart-service/ order-service/
inventory-service/ payment-service/ notification-service/
shipping-service/ ASP.NET Core 8 (src/ + tests/)
admin-app/ Angular admin console
| Service | Port | Responsibility |
|---|---|---|
| api-gateway | 8080 | Single entry point; JWT, roles, identity headers |
| product-service | 8081 | Products, categories, search |
| user-service | 8082 | Registration, login, refresh, email verification, password reset |
| cart-service | 8083 | Cart (Redis) |
| order-service | 8084 | Orders, admin order management, cancellation |
| inventory-service | 8085 | Stock levels, reservations, reservation expiry |
| payment-service | 8086 | Payment simulation |
| notification-service | 8087 | Email notifications, retry, dead-letter queue |
| shipping-service | 8088 | Shipments and tracking (.NET 8) |
| admin-app | 4200 | Angular admin console |
| service-registry | 8761 | Eureka dashboard |
| config-service | 8888 | Centralized configuration |
| Mailpit | 8025 / 1025 | Local email inbox (web UI) / SMTP |
- Docker Desktop
- Java 21 and Maven 3.9 (to build and test locally)
- .NET SDK 8 (only to work on
shipping-service) - Node.js 20+ (only to work on
admin-app)
cd shopsphere
docker compose up -d --build
docker ps --format '{{.Names}}\t{{.Status}}'The first build takes a few minutes. When every container shows Up:
| What | URL |
|---|---|
| API Gateway | http://localhost:8080 |
| Admin console | http://localhost:4200 |
| Email inbox (Mailpit) | http://localhost:8025 |
| Eureka dashboard | http://localhost:8761 |
Rebuild a single service after a change:
docker compose up -d --build order-serviceNo admin is seeded. Register one, then confirm the email (click the link in Mailpit, or flip the flag directly):
curl -s -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"firstName":"Admin","lastName":"User","email":"admin@shopsphere.local","password":"Admin@12345","role":"ADMIN"}'
docker exec shopsphere-postgres psql -U shopsphere -d shopsphere \
-c "update shopsphere_users.users set email_verified = true where email = 'admin@shopsphere.local';"Then sign in at http://localhost:4200.
Note: public registration currently accepts
"role":"ADMIN". Fine for local development; restrict it before deploying anywhere public.
cd shopsphere
mvn clean install # all Java modules, with tests
mvn -pl order-service -am test # one service (plus the modules it depends on)dotnet test shopsphere/shipping-service/tests/ShippingService.Tests/ShippingService.Tests.csprojIntegration tests use Testcontainers and need Docker running. If they are reported as skipped on a recent Docker Desktop, add api.version=1.44 to ~/.docker-java.properties.
cd shopsphere/admin-app
npm install
npm start # http://localhost:4200, proxies /api to the gateway on :8080Stop the admin-app container first (docker compose stop admin-app) — both use port 4200.
Start the infrastructure and platform services in Docker, stop the one you're working on, and run it locally:
docker compose stop order-service
mvn -pl order-service spring-boot:run1. POST /api/v1/auth/register → account created, verification email sent
2. POST /api/v1/auth/email-verification/confirm { token } (link in the email)
3. POST /api/v1/auth/login → { token, refreshToken, expiresIn, userId, role }
4. Send Authorization: Bearer <token> on every protected request
5. POST /api/v1/auth/refresh { refreshToken } → new token pair when the access token expires
6. POST /api/v1/auth/logout { refreshToken }
You only send the Authorization header. The gateway works out who the caller is and adds X-User-Id / X-User-Role itself; any values a client sends for these headers are discarded.
const BASE = 'http://localhost:8080'; // Android emulator: http://10.0.2.2:8080
let token = '';
export async function login(email, password) {
const res = await fetch(`${BASE}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error(`Login failed: ${res.status}`);
const data = await res.json();
token = data.token; // keep data.refreshToken to renew it
return data;
}
const auth = () => ({ 'Content-Type': 'application/json', Authorization: `Bearer ${token}` });
export const getProducts = () => fetch(`${BASE}/api/v1/products`).then(r => r.json());
export const addToCart = (productId, quantity) =>
fetch(`${BASE}/api/v1/carts/items`, { method: 'POST', headers: auth(), body: JSON.stringify({ productId, quantity }) });
export const placeOrder = body =>
fetch(`${BASE}/api/v1/orders`, { method: 'POST', headers: auth(), body: JSON.stringify(body) }).then(r => r.json());The same pattern works in any stack (Angular HttpClient, Axios, Flutter http, …). From a browser on another origin, configure CORS on the gateway for your frontend's domain.
All paths are called through the gateway (http://localhost:8080).
POST /api/v1/auth/register { firstName, lastName, email, password }
POST /api/v1/auth/login { email, password }
POST /api/v1/auth/refresh { refreshToken }
POST /api/v1/auth/logout { refreshToken }
POST /api/v1/auth/email-verification/confirm
POST /api/v1/auth/email-verification/resend
POST /api/v1/auth/password-reset/request
POST /api/v1/auth/password-reset/confirm
GET /api/v1/users/me
GET /api/v1/products?name=&categoryId=&minPrice=&maxPrice=&inStock=&page=&size=&sort=
GET /api/v1/products/{id}
POST /api/v1/products PUT /api/v1/products/{id} DELETE /api/v1/products/{id}
GET /api/v1/categories GET /api/v1/categories/{id}
POST /api/v1/categories PUT /api/v1/categories/{id} DELETE /api/v1/categories/{id}
GET /api/v1/carts
POST /api/v1/carts/items { productId, quantity }
PUT /api/v1/carts/items/{productId} { quantity }
DELETE /api/v1/carts/items/{productId}
DELETE /api/v1/carts/clear
POST /api/v1/orders { items, shippingAddress }
GET /api/v1/orders/my-orders
GET /api/v1/orders/{orderId}
GET /api/v1/admin/orders
GET /api/v1/admin/orders/{orderId}
POST /api/v1/admin/orders/{orderId}/cancel
POST /api/v1/payments/simulate
GET /api/v1/payments
GET /api/v1/payments/{paymentReference}
POST /api/v1/inventory { productId, quantity }
GET /api/v1/inventory/{productId}
PUT /api/v1/inventory/{productId} { quantity }
GET /api/v1/inventory/low-stock
POST /api/v1/inventory/check-availability [{ productId, quantity }, ...]
GET /api/v1/shipments/{shipmentId}
GET /api/v1/shipments/order/{orderId}
POST /api/v1/shipments/{shipmentId}/ship
POST /api/v1/shipments/{shipmentId}/deliver
POST /api/v1/shipments/{shipmentId}/cancel
GET /api/v1/notifications/user/{userId}
Every service also exposes a public GET /api/v1/<service>/health.
Each Java service serves Swagger UI at /swagger-ui.html and the raw spec at /v3/api-docs; the shipping service serves /swagger.
| Service | Swagger UI |
|---|---|
| Product | http://localhost:8081/swagger-ui.html |
| User | http://localhost:8082/swagger-ui.html |
| Cart | http://localhost:8083/swagger-ui.html |
| Order | http://localhost:8084/swagger-ui.html |
| Inventory | http://localhost:8085/swagger-ui.html |
| Shipping | http://localhost:8088/swagger |
To call protected endpoints: log in, copy token, click Authorize, paste it.
| Topic | Published by | Consumed by |
|---|---|---|
order.created |
order | inventory, payment, notification |
order.cancelled |
order | notification |
payment.successful |
payment | order, inventory, shipping, notification |
payment.failed |
payment | order, inventory, notification |
inventory.low-stock |
inventory | notification |
inventory.reservation-expired |
inventory | order |
shipment.dispatched / shipment.delivered |
shipping | order, notification |
user.email-verification-requested / user.password-reset-requested |
user | notification |
notification.dlq |
notification | notification (marks permanently failed) |
Topic names live in common-lib (Topics.java); the .NET service mirrors them in Messaging/Topics.cs.
One PostgreSQL database (shopsphere), one schema per service:
| Service | Schema | Schema management |
|---|---|---|
| user-service | shopsphere_users |
Hibernate |
| product-service | public |
Hibernate |
| order-service | shopsphere_orders |
Flyway (db/migration) |
| inventory-service | shopsphere_inventory |
Hibernate |
| payment-service | shopsphere_payments |
Hibernate |
| notification-service | shopsphere_notifications |
Flyway (db/migration) |
| shipping-service | shopsphere_shipping |
EF Core |
When you change an entity in a Flyway-managed service, add a new V<n>__description.sql migration — never edit one that has already run.
Shared settings live in shopsphere/config-service/src/main/resources/config/<service>.yml. Environment variables used by Docker Compose (see shopsphere/.env.example):
SPRING_DATASOURCE_URL=jdbc:postgresql://<host>:5432/shopsphere
SPRING_DATASOURCE_USERNAME=<user>
SPRING_DATASOURCE_PASSWORD=<pass>
REDIS_HOST=<redis-host>
KAFKA_BOOTSTRAP=<kafka-host>:29092
EUREKA_URL=http://<eureka-host>:8761/eureka/
CONFIG_SERVER_URL=http://<config-host>:8888
JWT_SECRET=<at least 256 bits of randomness>
INTERNAL_API_TOKEN=<shared secret for service-to-service calls>
FRONTEND_BASE_URL=http://localhost:4200 # used in email links
AUTH_REQUIRE_VERIFIED_EMAIL=true.github/workflows/ci.yml runs on pushes and pull requests to main:
- Java —
mvn clean verifyacross all modules (unit and integration tests). - .NET — restore, build and test
shipping-service. - Docker — builds every service image (after both test jobs pass).
New services don't have to be Java — shipping-service (.NET) is the reference for a non-Java service. To plug one in:
- Put it in its own folder under
shopsphere/with its own Dockerfile and tests. - Give it its own PostgreSQL schema (
shopsphere_<name>) and its own migrations. - Communicate through Kafka topics (add the names to
common-lib/Topics.java) or REST through the gateway — never read another service's tables. - Add a gateway route in
config-service/.../config/api-gateway.ymland decide which paths are public, JWT-only or ADMIN. - Add it to
docker-compose.ymland add a build/test job to.github/workflows/ci.yml.
| Symptom | Fix |
|---|---|
dependency kafka failed to start / NodeExistsException |
Kafka restarted before ZooKeeper dropped its old registration. The kafka service's startup command waits for it — make sure that block is still in docker-compose.yml, then docker compose up -d again. |
column ... does not exist |
The service never started, so its migration didn't run. Check docker logs shopsphere-<service>. |
| Integration tests show as skipped | Docker isn't reachable from Testcontainers — see Build and test above. |
Login works on :8082 but not through :8080 |
Check docker logs shopsphere-gateway for exceptions. |
# lines in pasted commands fail in zsh |
Run setopt interactivecomments, or add it to ~/.zshrc. |
The backend is environment-agnostic — point clients at your deployed gateway instead of localhost:8080.
| Platform | Notes |
|---|---|
| AWS ECS / Fargate | One task per service; RDS (Postgres), ElastiCache (Redis), MSK (Kafka) |
| Kubernetes (GKE / EKS / AKS) | One Deployment per service; Eureka optional with Kubernetes DNS |
| Railway / Render | Push Docker images; set connection strings as env vars |
| DigitalOcean App Platform | Multi-container app with managed Postgres and Redis |
Before going live: set a real JWT_SECRET and INTERNAL_API_TOKEN, configure SendGrid or SMTP for email, restrict ADMIN self-registration, and configure CORS for your frontend's domain.