Building an E-Commerce Microservices Backend with Spring Boot
A technical case study on building a 10-service e-commerce backend in Spring Boot — Backend-for-Frontend, Eureka service discovery, and an order-service that acts as a saga orchestrator for the entire checkout flow, distributed-systems trade-offs included.

Building an E-Commerce Microservices Backend with Spring Boot
Most microservices tutorials stop at two or three services waving hello to each other over HTTP. The interesting problems only show up once you go further: when ten independent services genuinely depend on each other to do one concrete thing, and none of them share a database or a transaction.
That's the problem this project sits on top of: how do you keep a checkout consistent — stock, payment, shipping, order status — when every one of those steps lives in a different process, with its own database, that can fail independently of the others?
Why 10 microservices?
Not because microservices are trendy — because each domain here has a different rate of change and a different responsibility:
- user-service owns identity and authentication — changes rarely, but everything depends on it.
- product-service owns the catalog and stock — read-heavy, needs to scale independently.
- order-service owns orchestration — the most complex logic, and the one that changes most often.
- payment-service, expedition-service, transaction-service — supporting domains that should be replaceable without touching anything else.
On top of that, two infrastructure layers: an api-gateway as the single entry point, and a eureka-server so no service ever hardcodes another service's host or port.
Architecture
One deliberate choiceabsorbed at the adaptation layer instead of leaking into the domain services as an ever-growing set of ?fields= query parameters.
Responsibility breakdown per service
| Service | Port | Responsibility |
|---|---|---|
| eureka-server | 8761 | Service registry — every other service registers here and discovers peers by name. |
| api-gateway | 99 | Single entry point; routes web traffic to Website API and mobile traffic to Mobile API. |
| website-api | 72 | Web BFF — aggregates and reshapes responses for the web client. |
| mobile-api | 71 | Mobile BFF — leaner payloads, same underlying domain services. |
| user-service | 8081 | Registration, login, JWT issuance, user profile and addresses. |
| product-service | 8082 | Catalog, categories, pricing, stock confirm/release. |
| order-service | 8080 | Order lifecycle and orchestration across five other services. |
| payment-service | 8084 | Records payments (cash / transfer) against an order. |
| expedition-service | 8083 | Shipping cost quotes and shipment creation, priced by distance. |
| transaction-service | 8085 | Final transaction record once an order is delivered. |
Checkout flow
The interesting part of this system lives in order-service. Instead of every service listening for events from every other service (choreography), it uses orchestration-based saga: order-service explicitly calls the other services in sequence, and it's the one place that knows the full order in which things happen — and w
- Create order — validates the user against
user-service, pulls price and stock fromproduct-service, computes a 10% tax. For ONLINE orders, shipping cost is quoted fromexpedition-servicebefore stock is confirmed. - Process payment —
payment-servicerecords the payment. For ONLINE orders, this step also triggers shipment creation immediately afterward. - Mark as delivered —
transaction-servicerecords the final transaction, closing the order. - Cancel — allowed from
PENDINGthroughSHIPPED; any linked payment and shipment are cancelled, and confirmed stock is released back.
Order state machine
Worth being precise about: for an ONLINE order, SHIPPED is set the moment the call to expedition-service to create a shipment succeeds — not the moment a package physically leaves a warehouse. The status reflects a successful API call between two services, not a real-world dispatch event. Nothing in the system currently feedtransition from PAYMENT_CONFIRMED → PROCESSING → SHIPPED: for ONLINE orders, payment confirmation and shipment creation happen back-to-back in the same code path, skipping PROCESSING entirely. The manual/admin path (updateOrderStatus()) still walks through PROCESSING explicitly. It's an inconsistency between the two paths, not a diagram simplification — and it's left in because it's true, and because generic microservices tutorials rarely have detail like this: it only shows up once you've actually built the thing.
Why WebClient
Every inter-service call in this system goes through Spring WebClient — reactive, non-blocking — instead of RestTemplate. This matters most at the BFF layer: a single incoming request from a client often has to fan out to two or three domain services at once. If those calls block a thread each, the thread pool runs out fast the moment traffic increases. Non-blocking I/O keeps that fan-out cheap.
Distributed system challenges
Three things that werboundaries. Consistency is maintained by hand — a specific call order plus explicit compensating actions in cancelOrder() (release stock, cancel payment, cancel shipment) — which is best-effort, not ACID.
- order-service is the highest-coupling node in the system. It's the only service that knows about five others, which also makes it the easiest place for a single downstream failure to cascade.
- Two API contracts, one domain. Keeping
website-apiandmobile-apiin sync with domain service changes takes discipline — a BFF's DTO can quietly drift out of date the moment the domain service underneath it changes shape.
Trade-offs & technical debt
A few things I'd flag explicitly rather than let a reader assume they're already handled:
- order-service has no circuit breaker. A slow or down
payment-serviceorexpedition-servicecurrently just makesorder-servicehang or throw — there's no fallback, timeout policy, or bulkhead in place yet. - The database isn't actually isolated per service. Most services (
user,product,order,payment,transaction) point at the same shared MongoDB Atlas database, separated only by collection name; onlyexpedition-serviceuses a database of its own. It works, but it undercuts the data-ownership guarantee microservices are supposed to give you. - Trust between services is implicit.
user-serviceissues and validates JWTs at the edge, but the internal domain services don't appear to re-validate a token on incoming calls — they trust that a request arriving at all is legitimate, rather th None of these break the system as it stands. They're the first three things I'd point to if someone asked "what would you fix before this touches real traffic."
Stack
| Category | Tools |
|---|---|
| Language & framework | Java 17, Spring Boot 3.2–3.5, Spring Cloud 2025.0.0 |
| Discovery & gateway | Netflix Eureka, Spring Cloud Gateway (WebFlux) |
| Inter-service communication | Spring WebClient (reactive), REST/JSON |
| Data | MongoDB Atlas, Spring Data MongoDB |
| Security | Spring Security, JWT (jjwt, HS256) |
If this project kept going, in rough priority order:
- Circuit breakers (Resilience4j) around every outbound call in
order-service. - Per-service database isolation — give each service its own database, not just its own collection.
- Service-to-service auth — propagate and re-validate the JWT (or move to mTLS) at each domain service boundary, instead of trusting network reachability.
- Distributed tracing — a correlation ID threaded through the five-hop checkout flow would make debugging a failed order far less painful than reading five separate log files.
- Selective choreography — some of the orchestrated calls (like recording a transaction after delivery) are natural candidates for an event instead of a synchronous call, reducing how much
order-serviceneeds to know about.
Closing
The value of this project wasn't in reaching ten services — it was in running into the problems that only exist once a system is actually distributed: keeping data consistent without a shared transaction, designing one domain around two different client contracts, and letting one service orchestrate five others without quietly becoming the single point of failure for all of them. The gaps listed above aren't oversights to hide; they're the honest state of the system, and the clearest map of what building the next version would actually require.