A multi vendor clothing marketplace built as 10 independently deployable Spring Boot services behind an API gateway, with a Next.js storefront and a checkout that runs as an asynchronous saga over Kafka. When a customer places an order, stock gets reserved, payment gets charged through Stripe, and a confirmation email goes out all coordinated across separate services that only talk to each other through events, and if anything fails partway through, the system rolls itself back cleanly: stock gets released, the order gets cancelled.
Checkout saga coordinated through Kafka, correlated by a business reference key across three services, with compensating transactions on failure
API gateway as the single point of JWT validation downstream services trust forwarded, spoof proof identity headers instead of each managing their own auth
Idempotency keys to prevent duplicate charges on retry, optimistic locking on stock, and payments that fail closed rather than silently succeed
Prometheus metrics tracking real business events (stock reserved, payment charged), not just request latency
Seller onboarding through Stripe Connect, with a signature verified webhook as the source of truth for payout status
Problem
Wanted to solve checkout for a multi vendor marketplace not just save an order to a database, but handle the fact that it touches inventory, payment, and multiple sellers at once, and any of those can fail halfway through.
Solution
Split the app into 10 separate services instead of one big monolith, and built checkout as a real event driven process over Kafka order placed, stock reserved, payment charged, confirmed where a failure at any step automatically rolls back instead of leaving things half done.
Architecture
Made the API gateway the only place a login token ever gets checked, so every other service just trusts the identity info the gateway passes along. Each service owns its own data, and I tied the checkout steps together with a reference code instead of a database ID, since IDs can be faked by the client.
FE
Service
Next.js 16
GW
Service
Spring Cloud Gateway
KC
Service
Keycloak (OIDC)
CUST
Service
Spring Boot
PROD
Service
Spring Boot
SELL
Service
Spring Boot
CART
Service
Spring Boot
ORD
Service
Spring Boot
PAY
Service
Spring Boot
NOTIF
Service
Spring Boot
CDB
Database
MongoDB
PDB
Database
PostgreSQL
SDB
Database
PostgreSQL
RDS
Database
Redis
ODB
Database
PostgreSQL
PYDB
Database
PostgreSQL
NDB
Database
MongoDB
MIN
Service
MinIO (S3-compatible)
KFK
Queue
Apache Kafka
KFK
Service
Stripe API
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
Reliability
Security
Used Keycloak for login, with the gateway as the single checkpoint for every request. Beyond that, I made each service double check that you actually own whatever you're trying to change. I verify Stripe's webhook by signature instead of a login token, since Stripe calls it directly.
Observability
Wired every service into Prometheus and Grafana, and added two custom metrics specifically to watch the checkout flow itself not just server health, but whether orders are actually completing. I also traced requests end to end with Zipkin.
Testing
Wrote unit tests throughout, plus real integration tests using actual Postgres and Kafka for the checkout flow, and browser based end to end tests for the main user journeys. I haven't done load testing yet.
Results
Ended up with 10 independently running services and a checkout flow I verified live through Grafana, watching the stock and payment counters actually move during a real order. I also got the order service's test suite to 35 out of 35 passing, including real integration tests against actual Postgres and Kafka, not mocks.
Lessons learned
Learned you have to design for failure from day one how things roll back isn't something you bolt on later. I also learned that safety features like circuit breakers can introduce their own bugs if you don't think through what happens on a retry.