Демо-отчёт. Проект и код написаны для примера — в них специально оставлены частые ошибки тестовых заданий. Это не работа реального клиента.
CodeCheckupОтчёт по код-ревью
Сервис заказов — тестовое задание
REST API заказов: создание с резервом товара, список, смена статуса. Java 21, Spring Boot 3, PostgreSQL, Docker Compose.
Заявка
№DEMO
Стек
Backend
Что проверяли
Тестовое задание
Уровень
Middle
Репозиторий
демо-проект
Ревьюер
Backend-разработчик CodeCheckup
Дата
20 октября 2026
Главное за минуту
4Исправить обязательно
3Желательно поправить
2На будущее
Проект аккуратно разложен по слоям и запускается одной командой. До отправки исправьте 4 пункта P1 — это около 3 часов.
Передавайте статус в SQL параметром — P1-1. Склейка строки запроса — первое, что ищут в коде бэкенда.
Создавайте заказ в транзакции — P1-3. Сейчас при ошибке остаётся резерв товара без заказа.
Уберите пароль базы и секрет JWT из application.yml — P1-2. Они уже в истории git.
Уберите N+1 при выдаче заказов с позициями — P1-4. На списке из 50 заказов сейчас 51 запрос к базе.
Если останется время — пагинация P2-1 и понятные ошибки вместо 500 P2-2.
Ответ на ваш вопрос
«Достаточно ли тестов и как обычно тестируют такие сервисы?»
Тестов мало, но исправить это несложно P2-3. Сейчас есть только проверка, что контекст поднимается. Для тестового хватит двух уровней:
Юнит-тесты сервиса на правила: нельзя заказать больше остатка, нельзя перевести отменённый заказ в «оплачен». Репозитории здесь подменяются фейками или моками Mockito.
Интеграционные тесты API через MockMvc и настоящий PostgreSQL в Testcontainers: создать заказ → получить его → сменить статус. H2 вместо PostgreSQL не подойдёт — у вас нативные запросы, которые H2 выполняет иначе.
Что проверяют интеграционные тесты
POST /orders→201 + Location→GET /orders/{id}→200, позиции на месте
POST /orders (товара нет)→409, остаток не изменился
PATCH /orders/{id}/status (отменён→оплачен)→422
Сильные стороны
Слои без смешения: контроллеры только принимают запрос и отдают DTO, логика в сервисах, SQL в репозиториях.
Docker Compose с PostgreSQL: проект запускается одной командой, README объясняет порты и переменные.
Валидация входных данных:@Valid и аннотации на DTO, ошибки формата отдаются как 400.
Современная Java к месту:record для DTO и запросов, конструкторная инъекция через final-поля, без @Autowired на полях.
Что не так. Статус из параметра запроса склеивается со строкой SQL. Запрос ?status=x' OR '1'='1 вернёт все заказы, а более хитрый — изменит данные.
Было · order/OrderRepository.java
publicList<Order> findByStatus(String status) {
return jdbc.query("SELECT * FROM orders WHERE status = '" + status + "'", orderMapper);
}
Как исправить. Передавайте значение параметром, а статус принимайте как enum — тогда неизвестное значение отсечётся ещё на входе с ответом 400:
Стало · order/OrderRepository.java
publicList<Order> findByStatus(OrderStatus status) {
return jdbc.query("SELECT * FROM orders WHERE status = ?", orderMapper, status.name());
}
Исправить обязательноP1-2
Пароль базы и секрет JWT лежат в application.yml
Где
src/main/resources/application.yml:6–14
Время
~20 мин
Пароль PostgreSQL и jwt.secret записаны значениями и попали в историю git. Удаление строк не поможет — смените секрет и читайте значения из переменных окружения:
В docker-compose.yml передайте их через env_file: .env, а в репозиторий положите .env.example без настоящих значений.
Данные и транзакции
Исправить обязательноP1-3
Заказ создаётся без транзакции
Где
order/OrderService.java:40
Время
~20 мин
Что не так. Сначала резервируется товар, потом сохраняется заказ — двумя отдельными операциями. Если сохранение упадёт (нарушение ограничения, обрыв соединения), резерв останется, а заказа не будет. Проверяющий почти наверняка спросит, что произойдёт при ошибке на середине.
Как исправить. Оберните операцию в транзакцию. Резерв делайте одним условным UPDATE, чтобы два параллельных заказа не забрали последний товар:
Стало · order/OrderService.java
@TransactionalpublicOrdercreate(NewOrder order) {
for (OrderItem item : order.items()) {
if (!stock.reserve(item.productId(), item.quantity())) {
thrownewOutOfStockException(item.productId());
}
}
return orders.save(order.toEntity());
}
Стало · stock/StockRepository.java
@Modifying@Query(value = """
UPDATE stock SET available = available - :quantity
WHERE product_id = :productId AND available >= :quantity
""", nativeQuery = true)
int decrease(@Param("productId") long productId, @Param("quantity") int quantity);
reserve возвращает decrease(...) == 1. OutOfStockException наследует RuntimeException, поэтому откатит всю транзакцию, включая уже зарезервированные позиции.
Позиции заказа загружаются лениво, а OrderDto.from() обращается к ним в цикле: один запрос за список и ещё по одному на каждый заказ. Включите spring.jpa.show-sql и посмотрите на лог — проверяющие делают именно так.
Было · order/OrderEntity.java
@OneToMany(mappedBy = "order") // LAZY по умолчаниюprivateList<OrderItemEntity> items = newArrayList<>();
Было · order/OrderService.java
// OrderDto.from() потом читает order.getItems() у каждого заказа — по запросу на заказpublicList<OrderEntity> list() {
return orders.findAll();
}
И spring.data.web.pageable.max-page-size: 100 в настройках, чтобы ?size=100000 не положил сервис.
Желательно поправитьP2-2
Ошибки отдаются как 500 со стектрейсом
Где
order/OrderService.java:63
Время
~40 мин
Недопустимый переход статуса и несуществующий заказ превращаются в 500 Internal Server Error с текстом исключения. Клиент API не может отличить свою ошибку от сбоя сервера.
Было · order/OrderService.java
publicOrderEntitychangeStatus(long id, OrderStatus status) {
OrderEntity order = orders.findById(id).orElseThrow();
if (!order.getStatus().canChangeTo(status)) {
thrownewIllegalStateException("Нельзя перевести заказ из " + order.getStatus() + " в " + status);
}
order.setStatus(status);
return order;
}
Бросайте свои исключения и сопоставьте их с кодами ответа в одном месте:
Стало · order/OrderService.java
OrderEntity order = orders.findById(id).orElseThrow(() -> newOrderNotFoundException(id));
if (!order.getStatus().canChangeTo(status)) {
thrownewIllegalStatusTransitionException(order.getStatus(), status);
}
spring.jpa.hibernate.ddl-auto: update удобен в начале, но не показывает, как схема будет меняться дальше. Пара миграций V1__init.sql, V2__stock.sql в Flyway — привычный для проверяющих способ.
На будущееP3-2
Описание API в OpenAPI
springdoc-openapi даст страницу /swagger-ui.html с описанием эндпоинтов. Проверяющему проще посмотреть API в браузере, чем читать контроллеры.
Вопросы по отчёту — 1 раунд в течение 7 дней, пишите в тот же чат. Доступ к репозиторию можно отозвать уже сейчас.
Что упомянуть в сопроводительном письме: чистое разделение на слои и запуск одной командой через Docker Compose, а после исправлений — транзакционное создание заказа с защитой от гонок за остаток.