Этап 10 - Архитектура Spring Boot REST API
Backend code становится тяжелым для поддержки, когда важное поведение спрятано в случайных controller methods. Эта статья про controller, service, repository, DTO и mapper. Тема выглядит технической, но настоящий вопрос практический: что произойдет, когда реальный client отправит реальный request, а в нем что-то неполное, слишком большое, запрещенное, медленное или конфликтующее с текущим состоянием?
Request на создание order не должен сохранять данные прямо из controller. Controller читает HTTP data, service выполняет use case, repository загружает и сохраняет entities, а mapper преобразует API DTO и internal objects.

Картинка, которую нужно держать в голове
Представь маленький online shop. User открывает frontend, нажимает кнопку, и browser вызывает backend. Backend принимает HTTP data, преобразует их в Java objects, проверяет rules, трогает database и возвращает JSON. Если у каждого шага есть понятный владелец, систему можно читать. Если все шаги смешаны в одном методе, первый production incident становится болезненным.
Для этой темы последовательность такая:
- Request DTO.
- controller.
- service.
- repository.
| Часть | Ответственность |
|---|---|
| Controller | Принимает HTTP data и возвращает публичный response. |
| Service | Выполняет business use case и защищает business rules. |
| Repository/adapter | Работает с persistence или external systems. |
| DTO/contract | Описывает, что внешний мир может отправить или получить. |
Конкретный Spring пример
@RestController
class OrderController {
@PostMapping("/api/orders")
ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {
return ResponseEntity.status(201).body(orderService.create(request));
}
}
@Service
class OrderService {
@Transactional
OrderResponse create(CreateOrderRequest request) {
Order order = orderRepository.save(Order.create(request.productId(), request.quantity()));
return orderMapper.toResponse(order);
}
}
Код специально маленький, потому что главное здесь - граница ответственности. Controller не должен незаметно решать business rules, которые принадлежат service. Service не должен зависеть от servlet objects. Repository не должен знать, какое JSON field name ожидает frontend. Когда граница чистая, то же поведение можно документировать, тестировать и менять без правок во всех слоях.
Почему это важно
В demo project многие shortcuts выглядят безобидно. Вернуть все records нормально, пока в таблице пять строк. Вернуть entity нормально, пока в ней не появилось скрытое поле. Логировать без request id нормально, пока несколько users не падают одновременно. Работать с вручную созданной database нормально, пока staging не получает другую schema. Production work в основном состоит в том, чтобы убрать такие hidden assumptions до того, как они станут incidents.
Самое простое полезное правило: сделай behavior явным на границе, enforce его в правильном слое и держи public contract стабильным. Если frontend знает request format, response format и error behavior, он работает увереннее. Если tests проверяют тот же contract, refactoring становится безопаснее. Если logs и configuration отражают тот же design, operations меньше зависят от угадывания.
Частые ошибки
- Помещать весь use case в controller method.
- Позволять persistence details протекать в API contract.
- Обрабатывать happy path, но оставлять failure behavior неопределенным.
- Использовать local shortcut, который не сможет работать в staging или production.
Чеклист понимания
- Я могу нарисовать последовательность этой темы от request до response.
- Я могу объяснить, какой слой владеет решением.
- Я могу назвать production problem, которую эта тема предотвращает.
Вопросы для самопроверки
- Что произойдет, если каждый endpoint реализует это правило по-своему?
- Какая часть behavior должна быть задокументирована для API clients?
- Какой test докажет, что правило работает, а не только happy path?