Case study · Team project

Marketplace

An e-commerce platform on Spring Boot microservices: sellers list products, buyers search, rate and pay with Stripe, admins moderate. The interesting part is what happens between the services: how they stay consistent without a shared database, and how a purchase never sells the same unit twice.

Role
Main contributor, with teammates
Stack
Java, Spring Boot, Spring Cloud, Kafka, MongoDB, Angular
Status
Source code on GitHub

What it does

Architecture

The browser talks to one HTTPS edge. Behind it, a Spring Cloud Gateway checks the JWT and routes each path to a service found in Eureka. Each service owns its own MongoDB database; Kafka events keep their copies of shared data in step. Select a component to see its role, what it talks to and the events it uses, or follow a purchase step by step.

The interactive diagram needs JavaScript. In short: Angular → Gateway (JWT, Redis rate limit, Eureka) → users, products, media and payments services, each with its own MongoDB; Kafka between them; OpenSearch for search; Stripe for payments.

Decisions worth explaining

Reserve stock before payment, in one atomic update

If stock only went down after payment, two buyers could both pay for the last unit. So the payments service reserves first. The products service runs one MongoDB findAndModify whose filter requires quantity ≥ requested and that this reservation id isn't recorded yet, and whose update lowers the quantity and records the reservation. Because the check and the change happen in a single document operation, only one of two racing buyers matches; the other gets 409 Conflict, and the quantity can't go below zero.

Criteria.where("_id").is(id)
    .and("quantity").gte(quantity)
    .and("stockReservations.reservationId").ne(reservationId)
// update: quantity -= n, push { reservationId, n }

Stripe's signed webhook decides, not the redirect

The success page a buyer lands on can be visited without paying, so it changes nothing. Orders move only when a webhook passes Stripe's signature check: completed publishes a sale event and marks the order paid; expired releases the exact reservation and cancels the order. Each move is conditional on the order still being PENDING, so a webhook delivered twice has no second effect.

Wait for Kafka before saying "paid"

The sale event is sent with kafkaTemplate.send(...).join(), which waits for the broker's acknowledgement. Only then does the order become PAID. If Kafka is down, the webhook fails, the order stays pending, and Stripe retries later.

The server owns the price and the buyer

The checkout request carries only a product id and a quantity. The price comes from the products service, and the buyer id from the gateway, which takes it from the verified token. A modified browser can't pay less or buy as someone else.

One database per service, events in between

Users, products, media and payments each own a MongoDB. When a user is created or removed, or an image is confirmed or replaced, an event tells the services that keep a copy or a file. Unused uploads are cleaned up after an hour, so images from abandoned forms don't pile up.

Security

Deployment and operations

Each part runs in Docker, started with Docker Compose on a cloud VM, on one shared Docker network.

  1. CheckoutThe commit to deploy
  2. Build and testMaven / Gradle builds and tests, only for services that changed
  3. AnalysisSonarCloud code analysis
  4. DeploySecrets injected, services restarted in order
  5. RollbackOn failure, back to the last successful commit

The monitoring dashboards and Jenkins are private and aren't linked here.

Testing