Article

Spring Boot Microservices Ecommerce: Multivendor Marketplace Guide

Spring Boot microservices ecommerce marketplace architecture
Spring Boot microservices ecommerce — gateway, domain services, storefront, and admin panel

If you have ever searched for a Spring Boot microservices ecommerce example, you already know the pattern: two services, one MySQL schema, and a README that says “the rest is homework.”

This post is about a different kind of sample. It is a multivendor marketplace you can clone and run with Docker. Customers shop. Sellers apply and list products. Admins moderate the platform. Each business area is its own Spring Boot service with its own database.

Source code (this is my repository — dofollow): github.com/needyamin/spring-boot-microservices-ecommerce

You do not need to be a distributed-systems expert to follow along. I will explain each idea in plain language first, then show how this project uses it.

What this article covers

  1. The mall analogy (why not one giant app)
  2. What you open after Docker starts
  3. The technology list
  4. How one click travels through the gateway
  5. REST vs Kafka, outbox, and saga
  6. Who owns which data
  7. Login and roles
  8. Checkout, webhooks, and order states
  9. A full “day in the life of an order”
  10. How a multi-seller cart is split
  11. The admin BFF
  12. API examples you can copy into Postman
  13. Flyway, folders, and how to run it
  14. Gateway route map
  15. How I would test a change
  16. What to build next
  17. Troubleshooting and FAQ

Think of a mall, not a single shop

A normal online store is one shop: one counter, one cash register, one stockroom. A marketplace is a mall.

In a mall, the clothing store does not open the electronics store’s cash drawer. The food court does not share a ledger with the cinema. There is still a front door, security, and a public directory.

That is the mental model:

  • API gateway = the mall entrance and security desk
  • Each microservice = one shop
  • Each MySQL database = that shop’s own cash register
  • Kafka = internal mail between shops (“order packed”, “payment failed”)
  • Next.js storefront = the public website shoppers see
  • Administrator service = the staff office, not a public store

Why bother? Because a marketplace has three kinds of users with three kinds of risk.

  • Shoppers want fast product pages and a checkout that is not charged twice.
  • Sellers want to apply, open a store, publish products, and see their own orders and stock.
  • Admins want to approve sellers, freeze bad accounts, and look at orders without logging into fourteen databases.

Search can be slow while checkout still works. Email can be down while payment still succeeds. Seller approval should not lock the whole catalog. Separate services keep those failures from becoming one giant outage.

What you see after you start Docker

After docker compose up is healthy, open these on your own computer (localhost is not linked on purpose — search engines should not crawl it):

  • Shop website (Next.js) — port 3000
  • Admin panel (AdminLTE 4) — port 3010
  • API gateway — port 8080, path /api/v1
  • Health dashboard — port 8080, path /dashboard
  • phpMyAdmin — port 8181
  • Grafana / Prometheus — ports 3001 and 9091

The first time MySQL is empty, Flyway creates tables and demo users. All three demo accounts use the same local-only password Password123!:

  • customer@demo.com — shopper
  • seller@demo.com — seller
  • admin@demo.com — admin panel

Those accounts are for your laptop. Do not use them on a public server. Put real secrets in .env before you expose the stack.

The technology, in one pass

Every backend module is Java 21 and Spring Boot 3. That keeps the programming style the same even though the processes are separate.

  • Spring Cloud Gateway — one public URL for APIs
  • MySQL 8 + Flyway — one schema per service; schema changes are SQL files, not “edit production by hand”
  • Redis — fast cart data
  • Apache Kafka — events after something important already saved
  • OpenSearch — product search, not the live product tables
  • Next.js — customer UI
  • Thymeleaf + AdminLTE 4 — admin UI
  • Docker Compose — start the mall with one command

Shared Java types (API wrappers, events, JWT helpers) live in common-lib. That library is a toolbox, not a dumping ground for business logic.

Third-party docs (nofollow): Spring Boot, Docker Compose, Apache Kafka.

How a request actually travels

Imagine you click “Add to cart” on the website.

  1. The browser talks only to the gateway (port 8080).
  2. The gateway checks the JWT if the route is protected.
  3. It throws away any fake “I am user X” headers from the client.
  4. It writes the real user id and role from the token.
  5. It forwards the call to cart-service.
  6. Cart-service reads and writes only cart_db (and Redis). It does not open catalog_db.

The admin panel is the same idea with a twist. It is a BFF (backend for frontend): a small Spring app that serves HTML and calls the gateway for you. It has no admin database. Staff log in, the JWT sits in an HttpOnly cookie, and the panel calls the same /api/v1/admin/... APIs Postman can call.

A simple map:

Shop website :3000          Admin office :3010
        \                      /
         \                    /
          \                  /
           \                /
           API gateway :8080
                    |
                    |
                    |
     auth, catalog, seller, order, payment, cart, ...
                    |
                    |
                    |
          MySQL (14 separate databases)
          Kafka mail  |  Redis  |  OpenSearch

Notice the important split: one MySQL process on your laptop, but fourteen databases inside it. That is cheaper to run locally. The rule is still: catalog-service never runs SQL against order_db.

When to call, when to send mail

Two shops in a mall can either phone each other (wait for an answer) or send a note (keep working).

Phone = REST (Feign). Use it when you cannot continue without the answer. Example: checkout must know “is this coupon valid?” and “is this SKU reservable?” before it creates an order.

Mail = Kafka. Use it when the sender should not wait. Example: the order is already saved. Email, search index, and shipping can catch up. If email is down, the customer still has an order.

Outbox is the boring-but-important trick. If you save the order and then try to send Kafka in the next line of Java, the process can die in between. You get an order with no event. Outbox means: in the same database transaction, write the order and a row that says “please publish this event.” A small poller sends Kafka later and ticks the row done.

Checkout is still a distributed transaction (stock + coupon + payment + order). This project does not use two-phase commit. Each service commits locally. If payment fails, a compensation event tells inventory to release the reservation. That pattern is a saga, done as choreography (events), not a central boss process.

Who owns what

You do not need to memorize every port. Remember the owner of the data:

  • auth-service — login, register, refresh token, user roles
  • user-service — profile, addresses, wishlist
  • seller-service — seller application, store, approve / reject / suspend
  • catalog-service — categories, brands, products, SKUs
  • inventory-service — warehouses, quantity, reservations
  • cart-service — guest and logged-in carts
  • order-service — orders and per-seller sub-orders
  • payment-service — payment intents, webhooks, refunds
  • shipping-service — shipments and tracking
  • notification-service — email / in-app messages
  • review-service — ratings and reviews
  • promotion-service — coupons and discount math
  • search-service — search index
  • recommendation-service — “you may also like”

Plus api-gateway (door), frontend (shop window), administrator-service (staff office, no database).

One extra rule that saves you later: at checkout, order-service copies the price and address onto the order. If the seller changes the price tomorrow, yesterday’s order does not magically change.

Login, in human terms

Early in the project, APIs were left open so the wiring could be finished. That is not how it ships now.

A shopper logs in like this:

  1. Website sends email and password to /api/v1/auth/login.
  2. auth-service returns a short-lived access JWT and a longer refresh token stored on the server.
  3. The website sends Authorization: Bearer … on later calls.
  4. The gateway checks the signature, then stamps the real user id and role on the request.
  5. The target service checks the JWT again (belt and suspenders) and checks the path (customer vs seller vs admin).

Roles in this marketplace:

  • CUSTOMER — own profile, cart, orders, reviews, payments
  • SELLER / SELLER_STAFF — own store, products, stock, seller orders
  • SUPPORT — read-only admin APIs
  • ADMIN — full admin APIs and the AdminLTE panel

Anyone can hit login, register, health, public catalog, search, public reviews, and a guest cart without a token. Almost everything else needs a token.

Honest limitation: seller id is still often a header, not a field inside the JWT. Seller routes require a seller role, but the cleaner design is “look up the shop from the logged-in user.” That is a good future patch, not a mystery.

Checkout without trusting the browser

The dangerous idea in ecommerce is letting the browser say “I paid” or “this costs $1.” This project does not do that.

A typical path:

  1. Cart is loaded from cart-service.
  2. Coupon is checked with promotion-service (if any).
  3. Inventory reserves stock.
  4. Order is created in PENDING.
  5. payment-service creates a payment intent.
  6. The payment provider calls a webhook.
  7. payment-service verifies the signature, ignores duplicate event ids, then publishes paid or failed.
  8. If paid, the order can move to CONFIRMED. If failed, stock is released.

After that, the seller pack-and-ship path is a simple state machine. Cancel and refund are real states, not “delete the row.”

                    payment webhook OK
                           |
                           v
  +---------+    +-----------+    +------------+    +---------------+
  | PENDING |--->| CONFIRMED |--->| PROCESSING |--->| READY_TO_SHIP |
  +---------+    +-----------+    +------------+    +-------+-------+
       |               |                |                    |
       |               |                |                    v
       |               |                |              +---------+
       |               |                |              | SHIPPED |
       |               |                |              +----+----+
       |               |                |                   |
       |               |                |                   v
       |               |                |              +-----------+
       |               |                |              | DELIVERED |
       |               |                |              +-----------+
       |               |                |
       +---------------+----------------+-----> CANCELLED
                       |                            |
                       +------------------------> REFUNDED  (after delivery, if approved)

  Who moves the status?
    PENDING     ->  payment-service event "PaymentCompleted"
    CONFIRMED   ->  seller accepts the sub-order
    PROCESSING  ->  seller packed the box
    READY_TO_SHIP -> carrier pickup
    SHIPPED     ->  tracking says out for delivery / in transit
    DELIVERED   ->  delivery confirmation
    CANCELLED   ->  customer / admin / timeout before ship
    REFUNDED    ->  payment-service refund webhook

Keep history. Webhooks and Kafka consumers must be safe to run twice (idempotent).

A full day in the life of one order

This is the story of one checkout, written as a sequence so you can see which service talks and which only listens.

Shopper "Amina"  (CUSTOMER)
  has SKU-BLUE-M from Seller S1
  and SKU-CABLE  from Seller S2
  coupon WELCOME10

T0  Browser
      GET  /api/v1/cart
      -> gateway -> cart-service
      cart_db + Redis return 2 lines

T1  Browser
      POST /api/v1/orders/checkout
      body: address snapshot, couponCode, cart token
      -> gateway -> order-service

T2  order-service  (synchronous phones)
      |-- Feign GET  promotion-service  "is WELCOME10 valid for this total?"
      |-- Feign POST inventory-service "reserve SKU-BLUE-M qty 1"
      |-- Feign POST inventory-service "reserve SKU-CABLE  qty 1"
      |-- INSERT order           status = PENDING
      |-- INSERT sub_order S1    status = PENDING
      |-- INSERT sub_order S2    status = PENDING
      |-- INSERT order_item x2   unit_price copied from catalog at this moment
      |-- INSERT outbox row      type = OrderCreated
      COMMIT order_db           <-- if this fails, no Kafka, no payment

T3  Outbox poller (order-service)
      reads unpublished outbox
      produces Kafka topic "order.created"
      {
        "eventId": "e-100",
        "orderId": "ord-55",
        "buyerId": "user-Amina",
        "subOrders": [
          { "sellerId": "S1", "items": ["SKU-BLUE-M"] },
          { "sellerId": "S2", "items": ["SKU-CABLE"] }
        ]
      }

T4  Kafka consumers (async, they do not block checkout)
      notification-service  "Your order was placed"
      search-service          no-op (order is not a product)
      recommendation-service record interaction

T5  order-service
      Feign POST payment-service  create PaymentIntent
      payment_db: status = REQUIRES_ACTION

T6  Payment provider (Stripe or test double)
      Shopper pays
      HTTP POST /api/v1/payments/webhooks
      Header: Stripe-Signature: t=...,v1=...

T7  payment-service
      verify signature
      if eventId already processed -> return 200 (duplicate, ignore)
      UPDATE payment PAID
      INSERT outbox PaymentCompleted
      COMMIT

T8  Kafka topic "payment.completed"
      order-service    PENDING -> CONFIRMED  (both sub-orders)
      inventory-service convert reservation -> sold (or keep reserved until ship)
      notification     "Payment received"

T9  Seller S1 dashboard
      POST /api/v1/sellers/me/orders/{id}/accept
      sub-order S1 -> PROCESSING

T10 Seller S1
      POST .../pack     -> READY_TO_SHIP
      shipping-service  create shipment, tracking MP123

T11 Kafka "shipment.created"
      notification  "Your blue shirt is on the way"
      order-service  sub-order S1 -> SHIPPED

T12 Seller S2 is slower
      same path, later
      Amina sees one parent order with two timelines

If T6 had failed (card declined):
      Kafka "payment.failed"
      inventory  RELEASE reservation SKU-BLUE-M and SKU-CABLE
      order      PENDING -> CANCELLED
      notification "Payment failed, stock released"

How a cart with two sellers is split

Amazon-style marketplaces do not create one box for two independent shops. They create one parent order and one sub-order per seller.

Cart (cart_db)
  line 1  SKU-BLUE-M   seller S1   qty 1   (display price only)
  line 2  SKU-CABLE    seller S2   qty 1

After checkout, order_db looks like this:

  orders
    id            ord-55
    buyer_id      user-Amina
    status        CONFIRMED
    currency      USD
    grand_total   42.90          <-- after WELCOME10
    ship_to_json  { "line1": "...", "city": "Dhaka" }   snapshot

  sub_orders
    id so-1   order_id ord-55   seller_id S1   status PROCESSING
    id so-2   order_id ord-55   seller_id S2   status PENDING

  order_items
    sku_code     SKU-BLUE-M   unit_price 29.99   qty 1   sub_order so-1
    sku_code     SKU-CABLE    unit_price 19.99   qty 1   sub_order so-2

Seller S1 can pack so-1 without waiting for S2.
Refunds can hit one sub-order only.
Inventory reservations are per SKU, so S1 stock is independent of S2.

What a JWT looks like (conceptually)

The access token is not magic. It is three Base64 parts: header, claims, signature. After the gateway verifies the signature, it trusts the claims — not the headers the browser sent.

HTTP request from the storefront:

  POST /api/v1/cart/items HTTP/1.1
  Host: localhost:8080
  Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLUFtaW5hIiwiZW1haWwiOiJjdXN0b21lckBkZW1vLmNvbSIsInJvbGUiOiJDVVNUT01FUiIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAwOTAwfQ.signature
  Content-Type: application/json

  { "skuId": "...", "quantity": 1 }

Decoded payload (example, not a real secret):

  {
    "sub":   "user-Amina",
    "email": "customer@demo.com",
    "role":  "CUSTOMER",
    "iat":   1700000000,
    "exp":   1700000900
  }

Gateway then forwards to cart-service as:

  POST /api/v1/cart/items
  Authorization: Bearer (same)
  X-User-Id:    user-Amina          <-- overwritten from "sub"
  X-User-Role:  CUSTOMER
  X-User-Email: customer@demo.com

If the browser had sent X-User-Id: user-Hacker, that header is discarded.
Refresh token is NOT in this JWT. It is an opaque string stored in auth_db
and rotated when POST /api/v1/auth/refresh is called.

The admin panel (staff office)

Admins should not live in phpMyAdmin. The administrator app is Spring Boot + Thymeleaf + AdminLTE 4 on port 3010.

  • No administrator_db.
  • Login goes through the gateway. Only ADMIN may continue.
  • The JWT is an HttpOnly cookie (JavaScript on the page cannot steal it as easily as localStorage).
  • Pages other than login and static CSS/JS require that cookie.

Screens you can actually use: dashboard (which services are up), products, sellers, users, orders. Product photos are URL fields in this version — no file upload yet — so the admin app stays stateless.

How to run it

You need Docker. You need JDK 21 and Maven only if you change Java and rebuild JARs.

# 1) Get the code
git clone https://github.com/needyamin/spring-boot-microservices-ecommerce.git
cd spring-boot-microservices-ecommerce

# 2) Local secrets (never commit the real .env)
cp .env.example .env
# edit JWT_SECRET, DB passwords, Stripe keys if you have them

# 3) First Java package so Dockerfiles have JARs to copy
mvn -DskipTests package

# 4) Start the mall
docker compose up -d --build

# 5) Watch health
docker compose ps
docker compose logs -f api-gateway
docker compose logs -f administrator-service

# 6) Stop without deleting shop data
docker compose down

# 7) Nuclear reset (MySQL + Kafka + Redis volumes)
docker compose down -v
docker compose up -d --build

Important detail: Java images copy JARs from each module’s target folder. After you change Java:

# Example: you changed order-service checkout
mvn -pl order-service -am -DskipTests package
docker compose up -d --build order-service api-gateway

# Frontend only
docker compose up -d --build frontend

# Admin BFF only (gateway must already be up)
mvn -pl administrator-service -am spring-boot:run
# then open port 3010

If Kafka complains about a cluster id after you wiped volumes, delete the Kafka volumes and start Kafka again. Do not delete the MySQL volume unless you want an empty shop.

Rebuild only the shop UI: docker compose up -d --build frontend. Do not run npm run dev at the same time as that container — both want port 3000.

API examples you can paste into Postman

Base URL is the gateway: http://localhost:8080. Import marketplace-api.postman_collection.json from the repo, or start with these.

1. Login (customer)

POST /api/v1/auth/login
Content-Type: application/json

{
  "email": "customer@demo.com",
  "password": "Password123!"
}

Example success body (shape):

{
  "success": true,
  "message": "Login successful",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "refreshToken": "opaque-refresh-string",
    "tokenType": "Bearer",
    "expiresIn": 900,
    "user": {
      "id": "a1111111-1111-4111-8111-111111111101",
      "email": "customer@demo.com",
      "role": "CUSTOMER"
    }
  }
}

2. Public product list (no token)

GET /api/v1/products?page=0&size=20&sort=createdAt,desc

{
  "success": true,
  "data": {
    "content": [
      {
        "id": "...",
        "name": "Wireless headphones",
        "slug": "wireless-headphones",
        "minPrice": 29.99,
        "status": "PUBLISHED"
      }
    ],
    "page": 0,
    "size": 20,
    "totalElements": 8
  }
}

3. Admin: list users (ADMIN token required)

GET /api/v1/admin/users?page=0&size=20
Authorization: Bearer eyJhbGciOi...

PUT /api/v1/admin/users/{userId}/status
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
  "status": "SUSPENDED"
}

POST /api/v1/admin/sellers/{sellerId}/approve
Authorization: Bearer eyJhbGciOi...

Typical HTTP codes: 200 OK, 201 created, 400 validation, 401 missing/bad token, 403 wrong role, 404 unknown id, 409 conflict (duplicate email, coupon already used).

Kafka topics (the internal mail room)

Names vary slightly in code, but the conversations look like this:

Producer                Topic / event              Consumers
---------               --------------             ---------
order-service           OrderCreated               notification, recommendation
payment-service         PaymentCompleted           order (confirm), inventory
payment-service         PaymentFailed              order (cancel), inventory (release)
catalog-service         ProductPublished           search-service (index), recommendation
shipping-service        ShipmentCreated           notification, order (SHIPPED)
shipping-service        ShipmentDelivered          order (DELIVERED), notification
seller-service          SellerApproved             notification (seller email)

Each event JSON should include:
  eventId     (uuid — consumer stores it so retries are no-ops)
  occurredAt  (UTC)
  payload     (orderId, sellerId, skuId, amounts...)

Consumer rule:
  if (already_processed(eventId)) return;
  do work;
  save eventId;

Flyway: how a table is born

You never “create table” by hand in phpMyAdmin for this project. You add a file:

auth-service/src/main/resources/db/migration/

  V1__init_auth_schema.sql
  V2__demo_data.sql
  V3__admin_user.sql
  V4__fix_admin_password.sql

Example (simplified):

-- V1__init_auth_schema.sql
CREATE TABLE users (
  id            CHAR(36)     NOT NULL,
  email         VARCHAR(255) NOT NULL,
  password_hash VARCHAR(100) NOT NULL,
  role          VARCHAR(32)  NOT NULL,
  status        VARCHAR(32)  NOT NULL,
  created_at    TIMESTAMP(6)  NOT NULL,
  updated_at    TIMESTAMP(6)  NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uk_users_email (email)
);

Rules:
  * File name: V{number}__short_description.sql
  * Never edit V1 after it has run on a machine (add V5 instead)
  * Demo users belong in V2/V3, not in application.yml

Folder map

spring-boot-microservices-ecommerce/
├── pom.xml                          parent Maven build (Java 21)
├── docker-compose.yml              the whole mall
├── .env.example
├── marketplace-api.postman_collection.json
├── ARCHITECTURE.md
├── CONTRIBUTING.md
│
├── common-lib/                     DTOs, events, JWT helpers
├── api-gateway/                    :8080  Spring Cloud Gateway
├── auth-service/                   :8081  auth_db
├── user-service/                    :8082  user_db
├── seller-service/                  :8083  seller_db
├── catalog-service/                 :8084  catalog_db
├── inventory-service/               :8085  inventory_db
├── cart-service/                    :8086  cart_db
├── order-service/                   :8087  order_db
├── payment-service/                 :8088  payment_db
├── shipping-service/                :8089  shipping_db
├── notification-service/           :8090  notification_db
├── review-service/                 :8091  review_db
├── promotion-service/               :8092  promotion_db
├── search-service/                  :8093  search_db
├── recommendation-service/         :8094  recommendation_db
├── administrator-service/        :3010  NO database (BFF)
├── frontend/                       :3000  Next.js
│
├── infrastructure/
│   ├── docker/mysql/               CREATE DATABASE auth_db, catalog_db, ...
│   └── monitoring/              Prometheus + Grafana
└── docs/                           auth, payments, orders, API notes

If you send a pull request: one service at a time, constructor injection, DTOs on APIs, Flyway for schema, tests included. Do not add a database to the admin BFF.

When something is red in docker compose ps

Symptom                         What to try first
-------                         -----------------
api-gateway restarting          mvn -DskipTests package   (missing JAR)
                                docker compose logs api-gateway

auth-service 500 on login       Flyway not applied? check auth_db.flyway_schema_history
                                JSON body must be {"email":"...","password":"..."}

admin panel "Please sign in"    Spring Security default login stole POST /login
                                administrator-service must disable formLogin

admin 3010 already in use      stop leftover java.exe / old container
                                docker compose up -d administrator-service

products 404 via gateway        Path=/api/v1/admin/products AND /products/**
                                rebuild api-gateway + catalog-service

Kafka InconsistentClusterId     docker compose rm -f kafka
                                docker volume rm ..._kafka_data
                                (do not delete mysql_data unless you want empty shop)

frontend build fails            unused imports / missing API helpers in Next.js
                                then docker compose up -d --build frontend

port 3000 busy                  leftover npm run dev  (node.exe)

Rules worth stealing for your own project

  1. Controllers do not contain business rules.
  2. Never return a JPA entity from a REST endpoint.
  3. Public APIs stay under /api/v1.
  4. The browser is not the source of truth for price, stock, or payment status.
  5. Use UTC. Every table needs id, created_at, updated_at.
  6. Webhooks and Kafka consumers must tolerate duplicates.
  7. Secrets stay in .env, not in git.

Gateway route map (what the door allows)

You can think of the gateway as a receptionist with a list. This is the idea, not every line of YAML:

Incoming path                         Who may call              Goes to
-------------                         ------------              -------
POST /api/v1/auth/login                anyone                    auth-service
POST /api/v1/auth/register             anyone                    auth-service
POST /api/v1/auth/refresh             anyone (refresh body)    auth-service

GET  /api/v1/products/**               anyone                    catalog-service
GET  /api/v1/categories/**              anyone                    catalog-service
GET  /api/v1/search/**                 anyone                    search-service

GET  /api/v1/cart/**                   guest or CUSTOMER        cart-service
POST /api/v1/cart/**                  guest or CUSTOMER        cart-service

POST /api/v1/orders/checkout            CUSTOMER                 order-service
GET  /api/v1/orders/me                  CUSTOMER                 order-service

GET  /api/v1/sellers/me/**             SELLER, SELLER_STAFF    seller / catalog / order
POST /api/v1/sellers/me/products        SELLER                   catalog-service

GET  /api/v1/admin/**                  ADMIN, SUPPORT (GET)    matching domain service
POST /api/v1/admin/**                 ADMIN only                matching domain service
PUT  /api/v1/admin/**                  ADMIN only                matching domain service

GET  /api/v1/health/**                 anyone                    gateway aggregator
GET  /dashboard                        anyone                    gateway HTML

Anything else with a missing/invalid JWT  ->  401
Wrong role                                 ->  403
Unknown path                               ->  404

How I would test a change

Example: you changed how coupons apply at checkout. Do not start with Kubernetes. Start small.

1. Unit test in promotion-service
     given coupon WELCOME10 and subtotal 50
     expect discount 5.00
     given expired coupon
     expect 400

2. Unit test in order-service
     mock PromotionClient and InventoryClient
     checkout copies unit prices onto order_items
     outbox row is written in the same transaction

3. Integration (Testcontainers MySQL)
     Flyway V2 demo data loads
     POST /api/v1/auth/login as customer@demo.com
     POST /api/v1/orders/checkout
     GET  /api/v1/orders/{id}  status PENDING or CONFIRMED

4. Compose
     mvn -pl order-service,promotion-service -am test
     mvn -DskipTests package
     docker compose up -d --build order-service promotion-service api-gateway
     use Postman collection folder "Orders"

If the unit tests fail, do not debug Docker yet.
If unit tests pass and Compose fails, look at gateway routes and JWT_SECRET mismatch.

What you could build next

The repo is a teaching mall, not a finished Amazon. Sensible follow-ups if you fork it:

  • Put sellerId in the JWT and stop trusting X-Seller-Id.
  • Replace URL-only product images with S3 uploads.
  • Add real Stripe keys and webhook signature secrets in .env.
  • Per-seller payouts (a new ledger service — do not stuff it into payment-service forever).
  • Rate limits on the gateway (login brute force).
  • OpenTelemetry traces so one checkout shows as one waterfall across services.
Suggested first PR for a newcomer:

  feature/seller-service/seller-id-from-jwt

  1. auth-service   add claim sellerId when role is SELLER
  2. common-lib     gateway/resource-server copy claim to X-Seller-Id
  3. seller-service ignore client-supplied X-Seller-Id if JWT present
  4. tests          CUSTOMER token cannot hit /sellers/me
  5. Postman        update seller folder

FAQ

Is this free?
Yes. MIT License. You run it. You keep your own secrets.

How many services?
Fourteen domain services, plus gateway, Next.js storefront, and the admin BFF.

Is this Shopify?
No. It is source code, not a hosted product.

How does admin login work?
The admin app posts to gateway login, allows ADMIN only, and stores the JWT in an HttpOnly cookie.

Why fourteen MySQL databases on one server?
So your laptop stays simple, while the code still obeys “do not query another service’s tables.” In production you can split the servers without rewriting the apps.

Why does checkout not wait for email?
Email is Kafka. The order is already committed. If SMTP is down, the shopper still has an order.

Can I run only catalog?
You can start MySQL, Kafka, Redis, gateway, auth, and catalog. Other pages will fail. That is expected — the mall is missing shops, not a monolith with optional classes.

Closing

If you want to learn Spring Boot microservices ecommerce by reading real module boundaries — not slides — clone the repo, start Compose, open port 3000, then sign in on port 3010 as the seeded admin.

Download the Spring Boot microservices ecommerce project on GitHub · More from the author on GitHub

This post is an original overview of a public open-source project. It is educational, not legal or payment advice. Demo emails and passwords are for a local Docker database only. Spring, Docker, Kafka, Next.js, and AdminLTE are trademarks of their owners.

Download ANSNEW APP For Ads Free Experiences!
Yamin Hossain Shohan
Software Engineer, Researcher & Digital Creator

I’m a researcher, software engineer and digital creator focused on applying technology and creative problem-solving to build useful tools, explore new ideas and create engaging digital content.

Copyright Disclaimer


All the information is published in good faith and for general information purpose only. We does not make any warranties about the completeness, reliability and accuracy of this information. Any action you take upon the information you find on ansnew.com is strictly at your own risk. We will not be liable for any losses and/or damages in connection with the use of our website. Please read our complete disclaimer. And we do not hold any copyright over the article multimedia materials. All credit goes to the respective owner/creator of the pictures, audios and videos. We also accept no liability for any links to other URLs which appear on our website. If you are a copyright owner or an agent thereof, and you believe that any material available on our services infringes your copyrights, then you may submit a written copyright infringement notification using the contact details

(0) Comments on "Spring Boot Microservices Ecommerce: Multivendor Marketplace Guide"

* Most comments will be posted if that are on-topic and not abusive