The Product Service

Configuration

server:
  port: 8081

spring:
  application:
    name: product-service
  datasource:
    url: jdbc:h2:mem:product_db
  jpa:
    hibernate:
      ddl-auto: update

management:
  endpoints:
    web:
      exposure:
        include: health,info

Note: spring.application.name is the service's identity. Discovery, config server, logs and tracing all use it, so set it in every service from day one.

Entity, Repository and Controller

package com.shopease.product;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "products")
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    private BigDecimal price;
    private int stock;
    private String category;

    protected Product() {}
    public Product(String name, String description, BigDecimal price, int stock, String category) {
        this.name = name; this.description = description;
        this.price = price; this.stock = stock; this.category = category;
    }
    // getters ...
    public void reduceStock(int qty) {
        if (qty > stock) throw new IllegalStateException("Only " + stock + " left");
        stock -= qty;
    }
}

// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {}

// ProductResponse.java - the public contract (never expose the entity directly)
public record ProductResponse(Long id, String name, BigDecimal price, int stock, String category) {
    static ProductResponse from(Product p) {
        return new ProductResponse(p.getId(), p.getName(), p.getPrice(), p.getStock(), p.getCategory());
    }
}

REST Endpoints

package com.shopease.product;

import org.springframework.http.*;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductRepository repo;

    public ProductController(ProductRepository repo) { this.repo = repo; }

    @GetMapping
    public List<ProductResponse> all() {
        return repo.findAll().stream().map(ProductResponse::from).toList();
    }

    @GetMapping("/{id}")
    public ProductResponse one(@PathVariable Long id) {
        return repo.findById(id).map(ProductResponse::from)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Product " + id + " not found"));
    }

    public record StockRequest(int quantity) {}

    /** Called by order-service when an order is placed. */
    @PostMapping("/{id}/reserve")
    @Transactional
    public ProductResponse reserve(@PathVariable Long id, @RequestBody StockRequest req) {
        Product p = repo.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Product " + id + " not found"));
        try {
            p.reduceStock(req.quantity());
        } catch (IllegalStateException e) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, e.getMessage());
        }
        return ProductResponse.from(p);
    }
}

Try It

Example

Start the service and call it (seed a few products with a CommandLineRunner or data.sql):

./mvnw spring-boot:run
curl localhost:8081/api/products/1
curl -i localhost:8081/api/products/99

Example output

INFO  o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8081 (http)

{"id":1,"name":"boAt Rockerz 450","price":1499.00,"stock":120,"category":"Electronics"}

HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{"type":"about:blank","title":"Not Found","status":404,"detail":"Product 99 not found","instance":"/api/products/99"}

Note: The 404 body is in the standard ProblemDetail format when spring.mvc.problemdetails.enabled=true. A consistent error format across services makes life easier for every caller.