Careeroza
Runnable Layered Example
All Three Layers in Plain Java
This program has the same shape as a Spring Boot app, without Spring: a repository interface with an in-memory implementation, a service with business rules, and a controller that turns results and exceptions into status codes. In Spring, the new calls in main are done for you by constructor injection.
Example
import java.math.BigDecimal;
import java.util.*;
public class Main {
public static void main(String[] args) {
ProductRepository repo = new InMemoryProductRepository(); // @Repository
ProductService service = new ProductService(repo); // @Service
ProductController api = new ProductController(service); // @RestController
System.out.println(api.create(new Product(null, "Wireless Mouse", new BigDecimal("799.00"), 120)));
System.out.println(api.create(new Product(null, "Clean Code", new BigDecimal("450.00"), 3)));
System.out.println(api.create(new Product(null, "Clean Code", new BigDecimal("450.00"), 3)));
System.out.println(api.reserve(2L, 2));
System.out.println(api.reserve(2L, 5));
System.out.println(api.get(9L));
System.out.println(api.list());
}
}
record Product(Long id, String name, BigDecimal price, int stock) {}
record Response(int status, Object body) {}
interface ProductRepository {
Product save(Product p);
Optional<Product> findById(Long id);
Optional<Product> findByName(String name);
List<Product> findAll();
}
class InMemoryProductRepository implements ProductRepository {
private final Map<Long, Product> rows = new TreeMap<>();
private long nextId = 1;
public Product save(Product p) {
Product saved = p.id() == null ? new Product(nextId++, p.name(), p.price(), p.stock()) : p;
rows.put(saved.id(), saved);
return saved;
}
public Optional<Product> findById(Long id) { return Optional.ofNullable(rows.get(id)); }
public Optional<Product> findByName(String n) { return rows.values().stream().filter(p -> p.name().equals(n)).findFirst(); }
public List<Product> findAll() { return List.copyOf(rows.values()); }
}
class NotFound extends RuntimeException { NotFound(String m) { super(m); } }
class Conflict extends RuntimeException { Conflict(String m) { super(m); } }
class ProductService {
private final ProductRepository repo;
ProductService(ProductRepository repo) { this.repo = repo; }
Product create(Product p) {
if (repo.findByName(p.name()).isPresent()) throw new Conflict(p.name() + " already exists");
return repo.save(p);
}
Product get(Long id) { return repo.findById(id).orElseThrow(() -> new NotFound("Product " + id + " not found")); }
Product reserve(Long id, int qty) {
Product p = get(id);
if (p.stock() < qty) throw new Conflict("Only " + p.stock() + " left of " + p.name());
return repo.save(new Product(p.id(), p.name(), p.price(), p.stock() - qty));
}
List<Product> list() { return repo.findAll(); }
}
class ProductController {
private final ProductService service;
ProductController(ProductService service) { this.service = service; }
Response create(Product p) { return call(201, () -> service.create(p)); }
Response get(Long id) { return call(200, () -> service.get(id)); }
Response reserve(Long id, int qty) { return call(200, () -> service.reserve(id, qty)); }
Response list() { return call(200, service::list); }
private Response call(int okStatus, java.util.function.Supplier<Object> action) {
try { return new Response(okStatus, action.get()); }
catch (NotFound e) { return new Response(404, e.getMessage()); } // like @ExceptionHandler
catch (Conflict e) { return new Response(409, e.getMessage()); }
}
}Output
Response[status=201, body=Product[id=1, name=Wireless Mouse, price=799.00, stock=120]] Response[status=201, body=Product[id=2, name=Clean Code, price=450.00, stock=3]] Response[status=409, body=Clean Code already exists] Response[status=200, body=Product[id=2, name=Clean Code, price=450.00, stock=1]] Response[status=409, body=Only 1 left of Clean Code] Response[status=404, body=Product 9 not found] Response[status=200, body=[Product[id=1, name=Wireless Mouse, price=799.00, stock=120], Product[id=2, name=Clean Code, price=450.00, stock=1]]]
The Same Classes in Spring
@Repository
class InMemoryProductRepository implements ProductRepository { ... } // later replaced by JpaRepository
@Service
class ProductService {
private final ProductRepository repo;
ProductService(ProductRepository repo) { this.repo = repo; } // injected by Spring
...
}
@RestController
@RequestMapping("/api/products")
class ProductController {
private final ProductService service;
ProductController(ProductService service) { this.service = service; }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Product create(@RequestBody Product p) { return service.create(p); }
// NotFound / Conflict are turned into 404 / 409 by @RestControllerAdvice
}Exercise: Add a restock(Long id, int qty) rule to the service that rejects qty <= 0 with an IllegalArgumentException. Which HTTP status should the handler map it to?
Show answer
Product restock(Long id, int qty) {
if (qty <= 0) throw new IllegalArgumentException("qty must be positive");
Product p = get(id);
return repo.save(new Product(p.id(), p.name(), p.price(), p.stock() + qty));
}
// Map IllegalArgumentException to 400 Bad Request: the client sent a bad value.