Building a 13-Microservice E-Commerce Clone for the n11 Bootcamp
A while back I applied for the Patika.dev x n11 Spring Boot bootcamp, and to my surprise I got accepted. It was a roughly two-week program: a stretch of lectures and exercises on Spring Boot, microservices, and the patterns that hold distributed systems together, and then a final case to prove you actually absorbed it.
The brief for that final case was open-ended in the best way: build something real. I decided to clone n11.com — a Turkish e-commerce marketplace — not as a single app, but as a proper microservice ecosystem. What started as "a few services" snowballed into thirteen. I built it in structured phases (with a fair bit of AI assistance along the way), each phase with its own success criteria and a verification gate before moving on, which is the only reason a project this size stayed manageable for one person in the time I had.

Tools
This was a Java-heavy build, leaning on the modern Spring Cloud stack:
Language: Java 21 (LTS)
Framework: Spring Boot 3.5
Service mesh: Spring Cloud 2025.0 — Eureka for discovery, a Config Server, and an API Gateway
Database: PostgreSQL 16 (with pgvector)
Messaging: RabbitMQ — the backbone of the whole order flow
AI: Google Gemini (chat + embeddings) via a provider-agnostic abstraction
Agents: a Model Context Protocol (MCP) server for external AI agents
Frontend: React 19 + Vite + Tailwind 4 (a fully Turkish storefront)
Build & deploy: Gradle + Jib, all running locally on docker-compose
The Big Picture
Thirteen services sounds like a lot — and it is — but they fall into clean groups once you step back.
Three of them are pure infrastructure: a Eureka server so services can find each other, a Config Server so configuration lives in one place, and an API Gateway that sits at the edge. The gateway is the only thing exposed to the outside world; it validates the JWT, strips the Authorization header, and injects a trusted X-User-Id downstream so no internal service ever has to re-parse a token.
Then there are the business services, each owning one slice of the domain and its own database schema: identity (auth and JWT issuance), product (the catalog), inventory (stock), cart, order, payment, and notification. On top of those sit the AI services: an ai-service that powers an in-app shopping assistant, a search-service, and an mcp-server that exposes the same capabilities to outside agents.
The rule I held to throughout: a service owns its data, and nobody reaches into anybody else's schema. If you want something, you ask over the network or you listen for an event.

The Order Saga
This was the part I'm most proud of, and the part that taught me the most.
In a single-database app, placing an order is one transaction: reserve stock, take payment, confirm the order, all-or-nothing. But the moment those steps live in separate services with separate databases, that neat transaction is gone. You can't roll back across service boundaries. So you need a different shape entirely — a saga.
I went with a choreography saga over RabbitMQ. There's no central coordinator barking orders; each service simply listens for the events that concern it and emits its own. Placing an order kicks off a chain of events:
OrderCreated -> StockReserved -> PaymentCompleted -> OrderConfirmed
The order service announces OrderCreated. Inventory hears it, reserves stock, and announces StockReserved. Payment hears that, runs the charge, and announces PaymentCompleted. The order service finally flips the order to confirmed. End to end, the happy path settles in about three seconds.
The interesting half is what happens when something goes wrong. If a payment fails, you can't just leave reserved stock dangling forever. So every forward step has a compensating step that unwinds it:
PaymentFailed -> StockReleased -> OrderCancelled

Two patterns made this trustworthy. First, a transactional outbox: a service never writes to its database and publishes a message as two separate actions (the classic dual-write trap, where one can succeed and the other fail). Instead it writes the event into an outbox table inside the same transaction, and a separate poller publishes it afterwards. Second, an idempotency inbox: because messaging is at-least-once, the same event can arrive twice, so every consumer records which events it has already processed and quietly ignores duplicates. Together they mean the saga is correct even when the network misbehaves — which, eventually, it always does.

Paying for Real
A fake "payment succeeded" button would have been easy. I wanted the real thing, so I integrated Iyzico (a Turkish payment provider) using its sandbox Checkout Form, complete with 3D Secure.
The tricky bit with real payment providers is the callback: after the user finishes 3DS on the bank's page, the provider needs to call your server back to report the result — and it can't call localhost. So I ran a Cloudflare Tunnel to give my local machine a public HTTPS URL that Iyzico could reach. I also added a scheduled job to sweep up orders that get stuck in a pending state (say, the user closes the tab mid-payment), so nothing sits in limbo and reserved stock eventually gets released.

Teaching It to Shop
The last big piece was an AI shopping assistant — a floating chat bubble where you can type, in Turkish, "find me a MacBook" and get back streamed responses with real product cards you can add to your cart.
Under the hood it's Gemini with function calling: I gave the model a set of tools (search_products, add_to_cart, view_cart, create_order, and so on), and it decides which to call to satisfy a request. Responses stream token-by-token over SSE so it feels alive, and conversations persist across page reloads.
The design detail I like most is what I called one toolset, two surfaces. Rather than defining those tools twice, I put them in a single shared module. The in-app ai-service imports it — and so does a separate MCP server, which exposes the exact same capabilities to external agents like Claude Desktop. Zero duplicated tool definitions. The same code that lets my storefront's chat bubble search products also lets an outside AI agent do it. Sitting underneath all of it is a provider-agnostic port: the rest of the system talks to a ChatProvider interface, never to Gemini directly, so swapping the model out is a one-adapter change.

What I Have Learnt
The biggest lesson is that microservices don't make things simpler — they trade one kind of complexity for another. A monolith would have shipped this in a fraction of the time. What you buy with the extra effort is independent services, clear ownership boundaries, and the ability to reason about one piece without holding the whole system in your head. Whether that trade is worth it depends entirely on the problem; for a learning project whose whole point was distributed systems, it absolutely was.
The saga work in particular reshaped how I think about correctness. Distributed correctness isn't free — you pay for it with outboxes, idempotency keys, and compensation logic — and a huge amount of real-world backend engineering is just carefully accounting for the moment a message arrives twice or a service dies mid-flow.
Two weeks of bootcamp turned into a project that touched auth, event-driven messaging, real payments, AI tool-calling, and a full frontend. I came out of it far more comfortable with Spring Cloud and, more importantly, with the patterns that keep distributed systems honest.