Loading repository data…
Loading repository data…
tahaberkamcadev / repository
Scalable distributed e-commerce platform showcasing modern microservice architecture with Spring Boot, Kafka, CQRS, Saga choreography, Outbox Pattern, Debezium CDC, and full observability.
Portfolio-grade event-driven microservices reference implementation: distributed saga, transactional outbox, CQRS read models, JWT-secured API gateway, and a full observability stack — runnable locally with a single
docker compose up.
Frontend (separate repo): ecom-client — web UI for browsing the catalog, checkout, and purchase flows against this backend. The fastest way to exercise the stack without curl or scripts.
This repository demonstrates how to design and operate a cloud-native e-commerce backend without hiding complexity behind a monolith. It is built to showcase skills that matter in modern backend and platform teams:
| Area | What you can evaluate |
|---|---|
| Distributed systems | Choreography-based saga, compensation, at-least-once delivery handling |
| Data consistency | Transactional Outbox + Debezium CDC instead of dual-write anti-patterns |
| Architecture styles | CQRS, database-per-service, API Gateway, event-driven integration, Redis cache-aside |
| Operational maturity | Prometheus, Grafana, Loki, health probes, structured dashboards |
| Engineering quality | Java 21, Spring Boot 4, unit tests, Dockerized local environment |
The stack is intentionally over-instrumented for a portfolio project so reviewers can trace a purchase end-to-end across services, Kafka topics, databases, and dashboards.
flowchart TB
CLIENT["Clients<br/>ecom-client · curl · demo script"]
subgraph edge["Edge"]
GW["api-gateway :8080<br/>JWT · Redis rate limit · CORS · routing"]
end
subgraph http["HTTP via gateway"]
USER["user-service :8081<br/>auth · JWT issue"]
INV["inventory-service :8082<br/>catalog write · purchase"]
REV["review-service :8086<br/>reviews"]
PRJ["projection-service :8087<br/>CQRS read API"]
end
subgraph saga["Kafka-only saga participants"]
ORD["order-service<br/>order aggregate"]
PAY["payment-service<br/>mock payment"]
end
subgraph readstore["Read-side & edge stores"]
ES["Elasticsearch"]
RD["redis-projection<br/>detail cache"]
RDG["redis-gateway<br/>rate-limit counters"]
end
subgraph data["Data & messaging"]
DB["PostgreSQL ×6<br/>database-per-service"]
DEB["Debezium Connect<br/>outbox → Kafka"]
K["Kafka KRaft"]
end
subgraph obs["Observability"]
PROM["Prometheus"]
LOKI["Loki + Promtail"]
GRAF["Grafana"]
end
CLIENT --> GW
GW --> USER
GW --> INV
GW --> REV
GW --> PRJ
GW --> RDG
USER --> DB
INV --> DB
ORD --> DB
PAY --> DB
REV --> DB
PRJ --> DB
PRJ --> ES
PRJ --> RD
INV --> DEB
ORD --> DEB
PAY --> DEB
REV --> DEB
DEB --> K
K --> ORD
K --> PAY
K --> PRJ
K --> INV
PROM -.-> GRAF
LOKI -.-> GRAF
Gateway exposes auth, products, reviews, and catalog. order-service and payment-service have no public HTTP routes — they only consume/produce saga events on Kafka.
Purchase is a choreography saga (no central orchestrator). The happy path and compensation path are both observable in logs and Grafana.
sequenceDiagram
participant C as Client
participant GW as API Gateway
participant INV as inventory-service
participant DB as PostgreSQL + Outbox
participant K as Kafka
participant ORD as order-service
participant PAY as payment-service
participant PRJ as projection-service
C->>GW: POST /api/products/purchase
GW->>INV: reserve stock (sync, strong consistency)
INV->>DB: UPDATE stock + outbox stock_updated (single TX)
DB-->>K: Debezium → saga.inventory.stock_updated
par consumers of stock_updated
K->>ORD: create order + outbox order_created
K->>PRJ: create order projection (PROCESSING)
end
DB-->>K: Debezium → saga.order.order_created
K->>PAY: mock payment (~2s, ~90% success)
alt payment_completed
DB-->>K: Debezium → saga.payment.payment_completed
K->>ORD: mark DELIVERED
K->>PRJ: mark DELIVERED
else payment_failed
DB-->>K: Debezium → saga.payment.payment_failed
K->>ORD: mark CANCELLED + outbox order_cancelled
K->>PRJ: mark CANCELLED
DB-->>K: Debezium → saga.order.order_cancelled
K->>INV: restore stock (compensation)
end
Notable implementation details:
UPDATE … WHERE stock >= :qty) — no lost updates under concurrency.processed_events (INSERT … ON CONFLICT DO NOTHING).order_cancelled to roll back inventory.| Layer | Technologies |
|---|---|
| Language & runtime | Java 21, Maven |
| Framework | Spring Boot 4, Spring Data JPA, Spring Security, Spring Kafka |
| API edge | Spring Cloud Gateway (WebMVC), JWT (JJWT), Redis-backed rate limiting |
| Messaging | Apache Kafka (KRaft), Debezium Outbox Event Router |
| Databases | PostgreSQL 17 (database-per-service, logical replication enabled) |
| Read model | CQRS — precomputed projection DB, Redis cache-aside, Elasticsearch 9 search |
| Observability | Micrometer, Prometheus, Grafana, Loki, Promtail |
| Packaging | Docker, Docker Compose, multi-stage Dockerfiles, readiness probes |
| Testing | JUnit 5, Mockito, AssertJ, @WebMvcTest, service-layer unit tests |
| Frontend | ecom-client (separate repo) |
| Service | Port | Role | Persistence |
|---|---|---|---|
| api-gateway | 8080 | Single entry point, JWT validation, Redis rate limiting, route proxying | Redis (rate-limit counters) |
| user-service | 8081 | Registration, login, JWT issuance, profile | PostgreSQL |
| inventory-service | 8082 | Product catalog (write), checkout, purchase, stock saga | PostgreSQL + Outbox |
| order-service | 8083 | Order aggregate, saga reactions, compensation | PostgreSQL + Outbox |
| payment-service | 8084 | Mock payment processor, saga participant | PostgreSQL + Outbox |
| review-service | 8086 | Product reviews, review-created events | PostgreSQL + Outbox |
| projection-service | 8087 | Catalog & order read API over a precomputed projection DB, Redis cache, ES search | PostgreSQL + Redis + ES |
Published host ports above are for local inspection; the intended client entry point is still api-gateway :8080. order-service / payment-service are not published to the host.
Supporting infrastructure (Docker Compose): 6× PostgreSQL, Kafka, Kafka UI, Debezium Connect, Prometheus, Grafana, Loki, Promtail, Redis ×2 (projection cache + gateway rate limit), Elasticsearch.
EventRouter.DefaultErrorHandler with DeadLetterPublishingRecoverer and dedicated DLT listeners.redis-gateway, per client IP): stricter on /api/v1/auth/**, higher default for other API traffic; /actuator/** excluded. Counters are shared across gateway instances via a Lua INCR + EXPIRE fixed window — separate from redis-projection (product detail cache-aside).X-Gateway-Secret) — inventory, review, and projection reject requests without it; user-service requires it on /api/v1/internal/**. Clients should use the gateway (:8080); host-mapped backend ports are for local debugging, not a production exposure model.projection-service's PostgreSQL. Browse/list (GET /api/catalog/products) reads that Postgres model (paginated; optional category). Product detail uses Redis cache-aside then DB. Full-text search goes to Elasticsearch. Checkout still hits the inventory write model for an authoritative quote. The trade-off is eventual consistency until projections catch up.GET /api/catalog/products/{id} looks up productById in redis-projection first; on miss it loads Postgres and populates the cache. Entries use a TTL (default 30 minutes) and are @CacheEvicted when projection handlers update that product (stock, price, reviews, etc.). The full catalog is not kept in Redis — list/search stay on Postgres/ES so cache keys stay bounded and invalidation st