Careeroza
Real World Example
One Service, Many Entry Points
At Swiggy or Zomato, "place order" can be triggered by the app (REST), by a reorder button in a notification (another REST endpoint), or by a scheduled subscription (a job). Because the logic lives in OrderService, all of them behave the same.
REST POST /api/orders ----\ Kafka "cart.checkout" -------> OrderService.placeOrder() --> ProductService.reserve() @Scheduled subscription ----/ \--> OrderRepository.save()
ShopEase: Layered Product API
// product/ProductController.java - HTTP only
@RestController
@RequestMapping("/api/products")
class ProductController {
private final ProductService service;
ProductController(ProductService service) { this.service = service; }
@GetMapping List<ProductResponse> list() { return service.list(); }
@GetMapping("/{id}") ProductResponse get(@PathVariable Long id) { return service.get(id); }
@PostMapping
ResponseEntity<ProductResponse> create(@Valid @RequestBody CreateProductRequest req) {
ProductResponse saved = service.create(req);
return ResponseEntity.created(URI.create("/api/products/" + saved.id())).body(saved);
}
@PutMapping("/{id}")
ProductResponse update(@PathVariable Long id, @Valid @RequestBody CreateProductRequest req) {
return service.update(id, req);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void delete(@PathVariable Long id) { service.delete(id); }
}
// product/ProductService.java - rules + mapping
@Service
@Transactional
class ProductService {
private final ProductRepository repo;
private final ProductMapper mapper;
ProductService(ProductRepository repo, ProductMapper mapper) { this.repo = repo; this.mapper = mapper; }
@Transactional(readOnly = true)
List<ProductResponse> list() { return repo.findAll().stream().map(mapper::toResponse).toList(); }
@Transactional(readOnly = true)
ProductResponse get(Long id) { return mapper.toResponse(find(id)); }
ProductResponse create(CreateProductRequest req) {
if (repo.existsBySku(req.sku())) throw new DuplicateSkuException(req.sku());
return mapper.toResponse(repo.save(mapper.toEntity(req)));
}
ProductResponse update(Long id, CreateProductRequest req) {
Product p = find(id);
mapper.update(req, p); // dirty checking saves it at commit
return mapper.toResponse(p);
}
void delete(Long id) { repo.delete(find(id)); }
private Product find(Long id) {
return repo.findById(id).orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
}Example
The API behaves exactly like before; only the inside is better organized:
curl -X DELETE -i localhost:8080/api/products/1
curl -X DELETE localhost:8080/api/products/1Example output
HTTP/1.1 204 No Content
{"type":"https://shopease.in/errors/not-found","title":"Resource not found","status":404,"detail":"Product 1 not found","instance":"/api/products/1","errorCode":"NOT_FOUND"}