Careeroza
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,infoNote: 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/99Example 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.