Skip to main content
DevScallix

Try before you apply

Your first code review: a REST endpoint that creates an order

Read a beginner-level Spring Boot endpoint, pick the review comments a good reviewer would leave, then compare with the improved version.

25 minutesThe lesson is in English.

In this lesson

  • Explain why code review exists and what makes a review comment good
  • Separate the responsibilities of controller, service and repository
  • Validate input at the edge with Bean Validation and return the right HTTP status (201 + Location, 400 + Problem Details)
  • Show why returning an entity directly and swallowing exceptions are risky

1 · Learn

Why does code review exist?

Code review is not about approving or mocking code. It has three jobs: catch mistakes before users meet them, keep the team's understanding of the code aligned, and share knowledge. A good review comment is specific (which line), explains the risk, and suggests a way forward. "This is bad" is not useful; "this line accepts a quantity of 0, let's add @Positive" is. It also helps to say whether a comment is blocking (must be fixed before merge) or a nit (a matter of taste). More: code review culture.

Layers: controller, service, repository

  • The controller translates HTTP: it reads the request, calls a service, and returns the right status and body. Business rules do not live here.
  • The service holds business rules and transaction boundaries (pricing, discounts, stock checks).
  • The repository only stores and reads data.

If a rule lives in a controller you cannot reuse it from a job or a message consumer, and testing it means starting the HTTP layer. More: Spring Boot layering.

Validate input at the edge

Everything that comes from a client is untrusted. Write a typed request DTO, add Bean Validation constraints, and put @Valid on the controller parameter. When validation fails, Spring throws MethodArgumentNotValidException and you turn it into a 400 Bad Request in a @RestControllerAdvice. RFC 9457 (Problem Details) defines the response format; Spring 6 provides the ProblemDetail class, so clients always get errors in the same shape. More: validation and errors.

Return the right HTTP status

When a new resource is created, return 201 Created and put the new resource's address in the Location header. A 200 OK says "something happened"; 201 says "a new resource exists and here is where". On failure, never return 200 with an empty body.

Do not expose entities

A JPA entity is a database model. If it becomes your API response, internal fields leak (internal statuses, for example), the API breaks whenever a table changes, and lazy associations cause surprise queries or serialization errors. Write a separate response DTO so the API contract does not depend on the database.

Naming

The author who writes q, p, o understands it today; in three months nobody does. quantity, product, order and OrderStatus.NEW make the code readable without comments. Magic numbers such as 10 and 0.9 should be named constants so the rule is visible.

A short example

@PostMappingpublic ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {    OrderResponse created = orderService.create(request);    URI location = URI.create("/orders/" + created.id());    return ResponseEntity.created(location).body(created);}

The controller stays thin: validation is in the annotations, the rule is in the service, and only the status and header are set here.

2 · Review

Read the code carefully. From the comments below, choose the ones a good reviewer would leave (some are wrong or trivial), then compare your choices with the explanations and the improved version.

@RestController@RequestMapping("/orders")public class OrderController {    @Autowired OrderRepository repo;    @Autowired ProductRepository prodRepo;    @PostMapping    public Order create(@RequestBody Map<String, Object> body) {        try {            Product p = prodRepo.findById(Long.valueOf(body.get("pid").toString())).get();            int q = (int) body.get("q");            Order o = new Order();            o.setProduct(p);            o.setTotal(p.getPrice().multiply(BigDecimal.valueOf(q)));            if (q > 10) o.setTotal(o.getTotal().multiply(new BigDecimal("0.9")));            o.setStatus("N");            return repo.save(o);        } catch (Exception e) {            return null;        }    }}

Pick the comments a good reviewer would leave, then press “Check”.

3 · The improved version

@RestController@RequestMapping("/orders")public class OrderController {    private final OrderService orderService; // constructor injection    @PostMapping    public ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {        OrderResponse created = orderService.create(request);        URI location = URI.create("/orders/" + created.id());        return ResponseEntity.created(location).body(created);    }}public record CreateOrderRequest(@NotNull Long productId, @Positive @Max(1000) int quantity) {}public record OrderResponse(Long id, BigDecimal total, OrderStatus status) {}@Servicepublic class OrderService {    private static final int BULK_THRESHOLD = 10;    private static final BigDecimal BULK_FACTOR = new BigDecimal("0.90");    // productRepository and orderRepository: constructor injection    @Transactional    public OrderResponse create(CreateOrderRequest request) {        Product product = productRepository.findById(request.productId())                .orElseThrow(() -> new ProductNotFoundException(request.productId()));        BigDecimal total = product.getPrice().multiply(BigDecimal.valueOf(request.quantity()));        if (request.quantity() > BULK_THRESHOLD) {            total = total.multiply(BULK_FACTOR);        }        Order saved = orderRepository.save(new Order(product, request.quantity(), total, OrderStatus.NEW));        return new OrderResponse(saved.getId(), saved.getTotal(), saved.getStatus());    }}

What changed and why

  • Input validation: the CreateOrderRequest record carries the contract with @NotNull, @Positive and @Max. Thanks to @Valid, bad input is rejected with a 400 before it reaches the service.
  • A thin controller: it only calls the service and returns 201 Created with a Location header. Constructor injection is safer than field injection: dependencies are visible and can be passed by hand in tests.
  • The rule lives in the service: the discount logic is in OrderService, the numbers are named constants, and the operation is @Transactional.
  • A response DTO: OrderResponse exposes only what the client should see; the entity never leaves.
  • No swallowed exception: if the product is missing, ProductNotFoundException is thrown. A separate @RestControllerAdvice class maps it to a 404 in ProblemDetail format and validation errors to a 400, so every endpoint answers errors in the same shape.

The trade-off

It means more classes: request DTO, response DTO, service, exception handler. For a tiny CRUD this can look like overhead, and mapping between DTOs and entities needs extra code. In return the API stays stable, the rule is testable and failures are visible. The split pays off as the project grows; for a very small prototype, keeping it simple can be a reasonable choice, as long as the choice is deliberate.

4 · Check yourself

  1. 1. A new order was created successfully. Which response is the most appropriate?
  2. 2. Where is the best place to reject a quantity of 0 or less?
  3. 3. Why is returning a JPA entity directly as the API response discouraged?
  4. 4. What is wrong with catch (Exception e) { return null; }?

To think about

  • In your own latest code, which line could confuse you in three months? How would you name it?
  • When you write a comment, how would you show the difference between a nit and a blocking issue, and why does it matter?
  • In which project would you accept this lesson's trade-off (more classes), and in which would you not?

Every assignment in the program works like this.

Code is submitted as a Pull Request, reviewed, improved and defended. If you liked this lesson, look at the Java Backend Career Track.