Для кого статья: для собеседующих в первую очередь, и для кандидатов.
О чём статья: о задачах, разработанных мною для технических интервью бэкенд-разработчиков уровня middle и выше.
Об авторе: лид стрима в облачном провайдере, набирал большую часть команды в 2024-2025, пришлось скорректировать процесс проведения интервью.

В прошлой статье я рассказывал об этапах проводимых мною собеседований. Рассказывал об особенностях найма в IT в 2024-25. Были немного обрисованы задачи, мотивация их особенностей, специальные подходы. Теперь пора уделить внимание хардскиловой составляющей. В этой статье подробнее расскажу о задачах и разберу сходные вариации.

Задачи в целом делятся на три категории: работа с базой, написать код на java/kotlin, провести ревью кода на java. Порядок этих категорий может меняться, как говорят умные люди: "в зависимости от ...". В рамках каждой категории есть условные уровни сложности, для простоты пусть будут junior+/middle-, middle, middle+. В зависимости от опыта/возраста кандидата начинаю со второго или первого уровня, при успешном выполнении и наличии времени идёт усложнение, при эпическом фейле задача упрощается по возможности или заменяется другой похожего уровня сложности. Обычно на базы успевали от 1 до 3 задач, на live coding на java чаще всего одну, редко - две. Куски кода на ревью разной сложности и объёма старался подбирать под предполагаемый уровень кандидата, изредка приходилось заменять на более простой или более сложный, благо формат ревью кода позволяет это делать, просто добавив или заменив участок кода.

Инструменты

Для онлайн-интервью использую чаще корпоративную, реже – google meet или zoom. Есть еще телемост от яндекса, но я его мало использовал в принципе. Конечно же, не подходят те, которые заставляют ставить десктопный клиент или имеют долгую регистрацию. Кажется, в свете последних событий из общедоступных и бесплатных остается только последний.

Для live coding предпочитаю не заставлять человека демонстрировать (шарить) экран рабочего или личного ноутбука (в первом случае можно случайно нарушить NDA, во втором случае риск поставить человека в неловкое положение), использую онлайн-редакторы. Поскольку я давал достаточно простые задачи, базовой подсветки синтаксиса достаточно. В спорной ситуации я мог бы продублировать код в свою Intellij Idea, но ни разу не приходилось.

Аналогично для код ревью. Заранее заготовил несколько вариантов листинга, копирую для интервью один из фрагментов. Достаточно вставить в онлайн редактор – будет хорошая эмуляция МР/ПР в гитлабе. 

Для обсуждения схем и рисования схем мне нравятся сервисы, не требующие регистрации и VPN. Да, многие любят фигму, но она не вполне подходит для архитектурной секции интервью, может быть недостаточно быстрой, требует от соискателя регистрации (да ещё и не любая почта подходит, как выяснилось). Тратить время интервью на регистрации и технические вопросы считаю нецелесообразным. Заставлять шарить экран, ожидая, что на той стороне есть весь необходимый софт – тоже несколько наивно. Я могу использовать https://excalidraw.com/ или https://www.edraw.ai/, смотря какой формат обсуждения нужен.

Базы данных

GolovatyyMG > Гибкость технического интервью > ChatGPT Image 9 февр. 2026 г., 15_26_07.png
Какое-то хранилище или база.

Задачи всегда предворяются разговором о базах на свободную тему на разные темы, основываясь на рассказе кандидата или его резюме. Какой-то общей канвы нет, но если время позволяет, начинаю разговор с абстрактных тем вроде ACID/BASE/CAP, уровнях изоляции, транзакцонности. Как правило 1-2 простых вопроса показывают, куда можно двигаться дальше. Стараюсь спрашивать про объекты баз, особенности хранения, без глубокой детализации. Как правило опытный специалист первыми двумя-тремя словами показывает свой уровень. А совсем опытные полностью отвечают на такие вопросы 1-2 названиями или терминами. Да, могут быть непонятны формулировки или луна не в том созвездии резюме слишком чуть приукрашено, тогда приходится разговаривать более многословно и по несколько раз переформулировать вопросы. Если большинство вопросов кандидат не понимает или не может ответить кратко/быстро -- это намекает на уровень компетенций и влияет на последующую задачу. Некоторые пытаются перевести разговор в сторону хайповых noSQL, графов, полнотекстового поиска, но, как правило, за пару уточняющих вопросов по этим технологиям становится понятно, что это попытка скрыть свою некомпетентность взять на понты ("Да кому нужны эти ваши реляционки!"). У меня, к счастью, есть опыт работы с разными базами. Конечно, опыт с хипстерскими технологиями у меня поверхностный, не сравнится с 16-летним опытом разработки в классических базах, но, всё ж, откровенную чушь про эластик или кассандру услышать я способен.

Для практической задачи даю модель данных в виде DDL-кода, как правило это 2-4 таблицы. Всё оформлено так, чтоб у более сильного или внимательного кандидата появлялись вопросы, с которых можно начать разговор на конкретную прикладную тему. Однако, большая часть собеседников пропускают этот момент и встречных вопросов не задают. Вообще, как оказалось, хорошо разбирающихся в РСУБД людей не так уж много. Некоторые вообще не знают SQL, мотивируя тотальным использованием ORM, некоторые не признаются, пытаясь на ходу то ли вспомнить, то ли придумать синтаксис. Что показательно, обычно в этих случаях незнание синтаксиса прямо коррелирует с непониманием модели данных, принципов нормализации даже на минимальном уровне. Конечно, это слово может быть произнесено, но до конкретных предложений по реорганизации модели, критики представленного кода, даже после наводящих вопросов дело обычно не доходит. Лично я считаю, что использование хибернейтов не дает индульгенцию на игнорирование архитектуры слоя хранения.

Обсуждения

По представленной модели у меня заготовлен пяток задач разного уровня сложности, которые, в зависимости от корректировок модели, я могу тоже корректировать, добавляя или изменяя условия выборок. Чаще всего даю одну задачу, и одну готовлю как альтернативную или более сложную. Если резюме или рассказ о своём опыте даёт повод думать, что передо мной «эксперт SQL» (цитата из резюме) – могу предложить сразу сложную задачу.

Такие секции проходят в одном из онлайн-редакторов. Я обычно использую от яндекса. Перед собеседованием создаю несколько досок, предзаполняю задачами. Далее в звонилке даю ссылки.

DDL
CREATE TABLE person (
 id UUID PRIMARY KEY,
 name varchar(255) NOT NULL,
 surname varchar(255) NOT NULL,
 address varchar(255) NOT NULL,
 created_at timestamp
);

CREATE TABLE company (
 id UUID PRIMARY KEY,
 name varchar(255) NOT NULL,
 type varchar(255) NOT NULL,
 inn varchar(255) NULL,
 created_at timestamp
);

CREATE TABLE goods (
 id UUID PRIMARY KEY,
 name varchar(255) NOT NULL, 
 description varchar(2000) NULL, 
 price numeric(10,2) NOT NULL,
 company_id UUID,
 created_at timestamp,
 CONSTRAINT company_fk FOREIGN KEY (company_id) REFERENCES company(id)
);

CREATE TABLE purchase (
 id UUID PRIMARY KEY,
 amount numeric(10,0) NOT NULL,
 person_id int,
 goods_id int,
 name varchar(255) NOT NULL,
 created_at date NOT NULL,
 CONSTRAINT person_fk FOREIGN KEY (person_id) REFERENCES person(id),
 CONSTRAINT goods_fk FOREIGN KEY (goods_id) REFERENCES goods(id)
);

Здесь специальный такой нейминг, который даёт возможность для критики и обсуждений. Пользовались этой возможностью всего несколько раз.

Какие встречные вопросы можно ожидать (в зависимости от предполагаемого грейда в этой области):

  • нормализация (junior+);

  • синтаксис (junior+);

  • переполнение (middle);

  • оптимизация поиска (middle);

  • нейминг (middle);

  • переполнение (middle+);

  • ограничения (middle+);

  • оптимизация хранения (senior).

Я нарочно не буду пояснять нюансы, предоставив читателям попрактиковаться в критическом восприятии заданий.

Даже если собеседник не видит нюансов, можно при ошибках в live coding (или, наоборот, слишком быстром и идеальном решении практической задачи), если остаётся время, задавать наводящие вопросы, намёки, заставляя присмотреться к задаче и оценить её.

Тем не менее с учётом допущений модель рабочая. На её основе можно придумать массу простых (и не очень) задач. Можно добавлять или изменять как столбцы, так и таблицы. Можно тасовать порядок столбцов, менять наименования, типы, дополнительные объекты. Обычно это у меня занимает не больше минуты, в момент подготовки к интервью (когда время позволяет) или в момент разговора копирую и изменяю листинг, прежде чем дать ссылку на код.

Также можно менять предметную область, примерно сохраняя модели и отношения между ними. Я, например, чаще использовал банковскую сферу (кредиторы, дебиторы, комиссии, счета, переводы), так как у большинства людей был опыт работы в финтехе. Возможна также складская (приходы/расходы/инвентаризация) или транспортная логистика (водители, пассажиры, парк автобусов, маршруты), библиотеки (книги, стеллажи, читатели, абонементы, разные залы), интернет-магазины и прочее. Перечень задач в целом не особо меняется.

SQL live coding

Простая задача

Обычно заключается в выборе некоторого набора полей по нескольким условиям и нескольким таблицам. Проверяется знание синтаксиса в принципе. Как правило, сильно оптимизировать запрос не получится или не имеет смысла, однако, можно обсудить подходы к оптимизации при наличии времени. 

-- Вывести все названия производителей, чьи товары покупали за раз более 10 единиц, с ценой более 1000 р.

Вариант решения
select
  distinct c.name
from
  purchase p
join goods g on g.id = p.goods_id and g.price > :price
join company c on c.id = g.company_id
where p.amount > :amt

Здесь хорошая вариативность по условиям, можно в обсуждениях затронуть такие темы, как оптимизации хранения и уместность использования тех или иных данных.

Задача уровня чуть выше, чем junior. Давал для очень молодым кандидатам или тем, кто сразу говорит, что с прямой работой с БД не очень знаком, предпочитая фреймворки и языки запросов более высокой абстракции. Для большинства кандидатов, которые приходили, эту задачу пропускал.

Основные задачи

Цель задачи: проверить понимание представленной модели данных, способность написать если не самый эффективный, то точно не самый сложный или медленный запрос. У этой задачи я сделал больше всего модификаций.

Правильных решений может быть несколько. Большинство решений можно улучшить, о способах можно поговорить после решения задачи.

-- Вывести имена ТОП-5 компаний, у которых было продано более 5 уникальных товаров и количество уникальных проданных товаров с сортировкой этого количества по убыванию.

Вариант решения

Как правило мало кто с ходу начинает писать правильные строки. Я бы рекомендовал бы обговорить сначала алгоритм, разбив его на этапы.

Например, так:

  • написано про уникальные товары, но таких данных нет. Значит надо получить выборку.

  • имея данные по уникальным товарам уже считать и фильтровать то, что нужно. Явно прослеживаются два зависящих друг от друга запроса.

Первый этап подразумевает ответы на такие вопросы:

  • уникальность каким ключевым словом определяется?

  • подсчет какой функцией?

  • подсчет в рамках чего?

  • какие ещё данные нужны для подсчета?

  • какие из полученных данных нужны для дальнейшего выполнения задачи?

Второй этап фактически украшают предыдущую выборку:

  • что вывести надо по условиям задачи?

  • что нужно из предыдущей выборке: какие поля, какой фильтр?

  • как ещё можно отфильтровать данные?

Один из несложных вариантов решения может быть таким:

WITH stats AS (
    SELECT 
        c.id AS company_id,
        c.name AS company_name,
        COUNT(DISTINCT p.goods_id) AS uniq_sold
    FROM company c
    JOIN goods g ON c.id = g.company_id
    JOIN purchase p ON g.id = p.goods_id
    GROUP BY c.id, c.name
)
SELECT 
    company_name,
    uniq_sold
FROM stats
WHERE uniq_sold > 5
ORDER BY uniq_sold desc
LIMIT 5   

Ещё возможен неплохой вариант без именнованного подзапроса. Но тут дело вкуса или привычки.

Если задача решена быстро – можно обсудить или более сложную задачу или слегка модифицировать эту. Например, при «неидеальном» решении обсудить упрощение или оптимизацию, как на уровне запроса, так и  на уровне хранения данных в базе. Можно попросить добавить в выборку какое-то хитрое значение.

-- Вывести имена ТОП-5 компаний, у которых было продано более 5 уникальных товаров и количество уникальных проданных товаров с сортировкой этого количества по убыванию.

-- Для каждой из них вывести "общую прибыль" (общую сумму проданных товаров).

Вариант решения
WITH stats AS (
    SELECT 
        c.id AS company_id,
        c.name AS company_name,
        COUNT(DISTINCT p.goods_id) AS uniq_sold,
        SUM() ... as overall_sum
    FROM company c
    JOIN goods g ON c.id = g.company_id
    JOIN purchase p ON g.id = p.goods_id
    GROUP BY c.id, c.name
)
SELECT 
    company_name,
    uniq_sold,
    overall_sum
FROM stats
WHERE uniq_sold > 5
ORDER BY uniq_sold desc
LIMIT 5

Для интереса тут не стал писать имплементацию.

Можно авторитарно добавить дополнительные ограничения, например, использовать having или не использовать операторы сравнения (<, >).

Сложная задача

До самых интересных моментов разговор доходит нечасто, но пару раз удавалась задать сложные вопросы и показать задачку на продвинутый уровень владения SQL. В основе – использование хитрых группировок или оконных функций. Решение через четырёхкратную вложенность запросов тоже допускается, хотя я и порицаю. 

Вот примерно такие задачи или чуть проще подготовлены для самых стойких:

-- Нужно проанализировать покупателей и присвоить им рейтинг.
-- Для каждого покупателя вывести:
-- Имя и фамилию
-- Общую сумму его покупок
-- Ранг по сумме покупок (1 - самый богатый)
-- Процент от общей суммы всех покупок
-- Кумулятивную сумму (сколько потратили этот и все предыдущие в рейтинге)

Скрытый текст

Основное, на мой взгляд, понять, как рассчитывать каждое из запрошенных полей: какие агрегирующие или аналитические функции бывают, как параметризовать.

Здесь тоже возможно разбить алгоритм на этапы, реализовать сначала через подзапросы. Если такое происходит, дальше можно обсудить варианты упрощения или оптимизации.

Например, один из неплохих вариантов:

SELECT 
    p.name AS buyer_name,
    p.surname AS byyer_surname,
    SUM(pu.amount) AS overall,
    RANK() OVER (ORDER BY SUM(pu.amount) DESC) AS rang,
    ROUND(SUM(pu.amount) * 100.0 / SUM(SUM(pu.amount)) OVER (), 2) AS percent,
    SUM(SUM(pu.amount)) OVER (ORDER BY SUM(pu.amount) DESC) AS summa
FROM person p
JOIN purchase pu  ON p.id = pu.person_id
GROUP BY p.id, p.name, p.surname
ORDER BY rang;

Конечно это не 1-в-1 задача, которую даю, но весьма похожа, решение примерное и не самое качественное.

Не зная оконные функции решить такого типа задачу будет долго, придется прорабатывать многоступенчатую логику и писать много кода, есть риск не уложиться в тайминг интервью. Подобного типа задачи решали всего несколько раз.

Специфика разрабатываемых нами продуктов такова, что настолько сложные запросы приходится писать редко. Но при промышленной эксплуатации и при разборе результатов регрессионных тестов бывает надо найти какие-то данные, проанализировать причины возникновения ошибки или нестандартного поведения. Понимание устройства данных, как и понимание способов их получения, сильно упрощает анализ инцидентов.

Как правило первая фаза секции баз данных, в которой мы обсуждаем особенности хранения и модели, даёт повод предположить успешность выполнения этой задачи.

Java code review

GolovatyyMG > Гибкость технического интервью > ChatGPT Image 9 февр. 2026 г., 15_25_49.png
Вариант архитектуры легаси.

Для меня это самая простая секция. У меня сохранен большой кусок кода, из которого копирую заголовок и – выборочно – от 4 до 8 методов. При сопутствующем настроении (и времени!) что-то там меняю. Могу удалить лишние строки из середины метода, если методов многовато, могу дописать что-то или переименовать. В любом случае полученный фрагмент содержит десятки ошибок, которые можно обсудить, проверить не только знание синтаксиса и практик, но и прямоту мышления, насмотренность, понимание паттернов подходов. 

Задание формулирую так:

К вам на ревью пришёл джун со своим мерж реквестом. Он может быть из другой команды, другого продукта (понимать предметную область необязательно). Требуется провести ревью кода с точки зрения работы jvm, spring, микросервисного подхода. Здесь много разных ошибок, обсуждать можно как сверху вниз, так и с любого места. Желательно сначала найти наиболее критические ошибки. Что проще – можно исправить тут же в редакторе, что сложно – можно выделить и обсудить.

Встречные вопросы конечно не только принимаются, но и ожидаются. В большинстве случаев напрасно.

Пример Code Review

Большой листинг
package ru.logisticscompany.wms.application.service;
......

@Slf4j
@RequiredArgsConstructor
@Service
public class WarehouseManagementServiceImplSample implements WarehouseManagementService {

    @Autowired
    private final KafkaProducer kafkaProducer;

    @Autowired
    private final RestTemplate restTemplate;

    public static final BigDecimal DEFAULT_CONVERSION_FACTOR = ONE;
    
    private final WarehouseConfiguration configuration;
    private final StorageLocationRepository locationRepository;
    private final ProductRepository productRepository;


    private final ShipmentRepository shipmentRepos;


    private static final String warehouse_Type_Code_Moscow = "RU:MOS";
    private static final String warehouse_Type_Code_Samara = "RU:SAM";

    @Override
    public ShipmentResponseDto processIncommingShipment(@NotNull ShipmentData shipmentData) {
        try {
            final String mainWarehouseCode = configuration.getMainWarehouseCode();
            final LocationData locSrc;
            final LocationData locDst;
            if (mainWarehouseCode.equals(shipmentData.getDestinationLocation().getWarehouseCode())) {
                locationRepository.findById(shipmentData.getDestinationLocation().getCode())
                        .orElseThrow(() -> new ValidationException(format("Location %s not found", shipmentData.getDestinationLocation().getCode())));
                locDst = shipmentData.getDestinationLocation();
                locSrc = shipmentData.getSourceLocation();
            } else if (mainWarehouseCode.equals(shipmentData.getSourceLocation().getWarehouseCode())) {
                throw new ValidationException("Wrong source location warehouse code");
            } else {
                throw new ValidationException("Destination warehouse unknown");
            }

              Shipment shipment = buildExternalShipment(locSrc, locDst, shipmentData);
              shipment.setAcceptanceDate(LocalDateTime.now());
            ShipmentData realShipmentData = ConvertersKt.toShipmentData(shipmentRepos.save(shipment),ShipmentData.ShipmentDirection.INBOUND,configuration.getMainWarehouseCode());
            return new ShipmentResponseDto("ACCEPTED", realShipmentData);
        } catch (ValidationException e) {
            return new ShipmentResponseDto("FAILURE", shipmentData, e.getMessage());
        }
    }

    @Override
    public int calculateHandlingFee(String locationCode, Integer shipmentQuantity) {
        if (StringUtils.startsWith(locationCode, "BULK")) {
            return HandlingFeeUtils.calculateBulkHandlingFee(shipmentQuantity);
        } else if (StringUtils.startsWith(locationCode, "PICK")) {
            return HandlingFeeUtils.calculatePickHandlingFee(shipmentQuantity, HandlingFeeUtils.getWarehouseHandlingMultiplier(locationCode, warehouse_Type_Code_Moscow));
        } else {
            var whMultiplier = HandlingFeeUtils.getWarehouseHandlingMultiplier(locationCode, warehouse_Type_Code_Samara);
           int overallHandlingFee = 0;
            bulkHandlingFee = HandlingFeeUtils.calculateBulkHandlingFee(shipmentQuantity);
            if (whMultiplier < 0) {
                overallHandlingFee = HandlingFeeUtils.defaultSamaraHandlingFee();
                return overallHandlingFee;
            } else {
                int handlingFee = HandlingFeeUtils.calculateMoscowHandlingFee(shipmentQuantity, whMultiplier);
                overallHandlingFee = handlingFee + bulkHandlingFee;
                return overallHandlingFee;
            }

        }
    }

    @Override
    public ShipmentResponseDto doExternalShipment(ShipmentData shipmentData) {
        try {
            final StorageLocation foundLocationSrc = locationRepository.findById(shipmentData.getSourceLocation().getCode())
                    .orElseThrow(() -> new RuntimeException(format("Location %s not found", shipmentData.getSourceLocation().getCode())));
            
            final BigDecimal availableQuantitySrc = BigDecimal.valueOf(foundLocationSrc.totalReceived() - foundLocationSrc.totalShipped(), 2);
            final BigDecimal minStockLevel = foundLocationSrc.getMinStockLevel() != null ? foundLocationSrc.getMinStockLevel() : BigDecimal.ZERO;
            if (availableQuantitySrc.subtract(shipmentData.getQuantity()).compareTo(minStockLevel) < 0) {
                throw new RuntimeException("Shipment would violate minimum stock level.");
            }
            
            if (shipmentData.getProductId() != null) {
                Product product = productRepository.findById(shipmentData.getProductId())
                    .orElseThrow(() -> new RuntimeException("Product not found"));
                if (product.getExpiryDate() != null && product.getExpiryDate().isBefore(LocalDate.now().plusDays(7))) {
                    throw new RuntimeException("Product is near or past expiry date.");
                }
                
                if (product.getStorageTemperature() != null && 
                    !foundLocationSrc.getTemperatureZone().equals(product.getStorageTemperature())) {
                    throw new RuntimeException("Product temperature requirements not met.");
                }
            }
            
            if (availableQuantitySrc.compareTo(shipmentData.getQuantity()) < 0) {
                throw new RuntimeException("Insufficient stock for shipment.");
            }
            if (!configuration.getMainWarehouseCode().equals(shipmentData.getSourceLocation().getWarehouseCode())) {
                throw new RuntimeException("Только исходящие отгрузки из основного склада можно внешние.");
            }
            final LocationData locSrc = shipmentData.getSourceLocation();
            final LocationData locDst = shipmentData.getDestinationLocation();
            if (List.of(locDst.getCode(), locDst.getWarehouseCode(), locDst.getUnitOfMeasure()).stream().anyMatch(StringUtils::isBlank)) {
                throw new RuntimeException("Destination location should not be empty");
            }
            
            if (shipmentData.getWeight() != null && shipmentData.getWeight().compareTo(configuration.getMaxShipmentWeight()) > 0) {
                throw new RuntimeException("Shipment weight exceeds limit.");
            }
            
            Shipment externalShipment = shipmentRepos.save(buildExternalShipment(locSrc, locDst, shipmentData));
            ShipmentData externalShipmentData = ConvertersKt.toShipmentData(externalShipment, ShipmentData.ShipmentDirection.OUTBOUND, configuration.getMainWarehouseCode());
            kafkaProducer.sendOutgoingShipment(externalShipmentData);
            
            calculateDeliveryCost(externalShipmentData);
            
            return new ShipmentResponseDto("in_process", externalShipmentData);
        } catch (ValidationException e) {
            return new ShipmentResponseDto("FAILURE", shipmentData, e.getMessage());
        }
    }

    public void calculateDeliveryCost(ShipmentData shipmentData) {
        String url = "http://transport-logistics-service/api/v1/delivery/calculate";
        
        Map<String, Object> request = new HashMap<>();
        request.put("fromWarehouse", shipmentData.getSourceLocation().getWarehouseCode());
        request.put("toWarehouse", shipmentData.getDestinationLocation().getWarehouseCode());
        request.put("weight", shipmentData.getWeight());
        request.put("volume", shipmentData.getVolume());
        request.put("hazardous", shipmentData.getHazardousMaterial());
        
        try {
            ResponseEntity<Map> response = restTemplate.postForEntity(url, request, Map.class);
            
            if (response.getStatusCode().is2xxSuccessful()) {
                Map<String, Object> result = response.getBody();
                shipmentData.setDeliveryCost((BigDecimal) result.get("cost"));
                shipmentData.setDeliveryDays((Integer) result.get("estimatedDays"));
            }
        } catch (Exception e) {
            log.warn("Failed to calculate delivery cost: {}", e.getMessage());
        }
    }

    private Shipment buildExternalShipment(LocationData locSrc, LocationData locDst, ShipmentData shipmentData) {
        if (locDst == null) throw new ValidationException("Invalid destination location (not found)");
        if (locSrc == null) throw new ValidationException("Invalid source location (not found)");
        if (locDst.getUnitOfMeasure() == null) throw new NullPointerException("Invalid destination location (unit of measure not found)");
        if (locSrc.getUnitOfMeasure() == null) throw new NullPointerException("Invalid source location (unit of measure not found)");
        if (!locSrc.getUnitOfMeasure().equals(locDst.getUnitOfMeasure())) throw new ValidationException("Invalid shipment (different measurement units)");
        if (shipmentData.getHazardousMaterial() && !locDst.getHazardousStorageApproved()) {
            throw new ValidationException("Destination location not approved for hazardous materials");
        }
        // if (shipmentData.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
        //     throw new ValidationException("Shipment quantity must be positive");
        // }
        // if (locSrc.getCode().equals(locDst.getCode())) {
        //     throw new ValidationException("Source and destination cannot be the same");
        // }
        return Shipment.reserved(
                locSrc.getCode(),
                locDst.getCode(),
                DEFAULT_CONVERSION_FACTOR,
                  shipmentData.getQuantity(),
                  LocalDateTime.now(),
                  shipmentData.getDescription() != null
                        ? shipmentData.getDescription()
                        : "Shipment from external system"
        );
    }

    @Override
    public int processRejectedOutcommingShipmentsReester(Long shipmentId) {
        var foundShipment = shipmentRepos.findById(shipmentId);
        processLocationReservations(foundShipment.get().getSourceLocationCode());
        StorageLocation loc = locationRepository.getByLocationCode(foundShipment.getSourceLocationCode());
        Shipment[] reester = loc.getOutcommingShipments();
        int summa = 0;
        for(int k = 0; k < reester.length; k++) {
            if (!reester[k].getRejectionDate().isNull()) {
                if (reester[k].getShipmentType().getName() = "handling_fee") {
                    k+=1;
                    summa += calculateHandlingFee(foundShipment.getSourceLocationCode(), reester[k].getQuantity().intValue());
                } else {
                    BigDecimal a = reester[k].getQuantity();
                    summa += a.intValue();
                }
            }
        }
        return summa;
    }

    @Override
    public int processOutcommingShipmentsReester(Long shipmentId) {
        Shipment foundShipment = shipmentRepos.findById(shipmentId);
        StorageLocation loc = locationRepository.getByLocationCode(foundShipment.getSourceLocationCode());
        Shipment[] reester = loc.getOutcommingShipments();
        int summa = 0;
        for(int k = 1; k <= reester.length; k++) {
            if (reester[k].getRejectionDate().isNull()) {
                if (reester[k].getShipmentType().getName() = "europe") {
                BigDecimal a = reester[k].getQuantity();
                summa += a.intValue();
                }
            }
        }
        return summa;
    }

    @Override
    public void processOutcommingContragentWarehouseResponse(Long shipmentId, @NotNull ShipmentResponseDto shipmentResponse) {
        Optional<Shipment> foundShipment = shipmentRepos.findById(ConvertIdUtils.convertWarehouseCode(shipmentId));
        processLocationReservations(foundShipment.get().getSourceLocationCode());
        if ("ACCEPTED.OUTGOING".equals(shipmentResponse.getStatus())) {
            foundShipment.map(p -> {
                p.setAcceptanceDate(shipmentResponse.getData().getCreationDate());
                return p;
            }).ifPresent(shipmentRepos::save);
        } else {
            foundShipment.map(p -> {
                p.setRejectionDate(LocalDateTime.now());
                return p;
            }).ifPresent(shipmentRepos::save);
        }
    }

    @Override
    public void processOutcommingShipmentResponse(Long shipmentId, @NotNull ShipmentResponseDto shipmentResponse) {
        var foundShipment = shipmentRepos.findById(shipmentId);
        if ("ACCEPTED".equals(shipmentResponse.getStatus())) {
            foundShipment.map(p -> {
                p.setAcceptanceDate(shipmentResponse.getData().getCreationDate());
                return p;
            }).ifPresent(shipmentRepos::save);
        } else {
            foundShipment.map(p -> {
                p.setRejectionDate(LocalDateTime.now());
                return p;
            }).ifPresent(shipmentRepos::save);
        }
    }

    @Transactional
    private Optional<Restocking> processLocationReservations(String locCode) {
        try {
            locationRepository.findByCode(locCode)
                    .map(StorageLocation::getId)
                    .ifPresent(locId -> restockingService.processReservations(locId));
            return Optional.of(restockingService.calculateRestockingNeeds(locId));
        } catch (RestockingException ex) {
            log.warn(ex.getMessage());
        }
    }
}

Код примерный, для ознакомления, призван дать представление о количестве вариаций листингов, которые можно из него нарезать почти персонально под каждого.

На что можно было бы посмотреть в первую очередь:

  • работа с DI;

  • области видимости;

  • AOP;

  • магия в цифрах и буквах;

  • общая стилистика и рисунок форм выражений, инструкций, методов;

  • перегруженность смыслом, единственная ответственность, небольшая копипастность;

  • проверки, fail-fast (так же тут), исключения.

Дальше, при более пристальном чтении не структуры кода, а каждого метода, я бы заметил:

  • небезопасные вызовы методов у сомнительных данных, потенциальные места для NPE;

  • интересные конвертации типов и сомнительного характера переменные;

  • заподозрил бы необходимость транзакционности;

  • поставил бы под сомнение уместность валидаций, не говоря уже о их имплементациях;

  • может быть нашёл бы странное использование синтаксиса;

  • повозмущался бы грубым игнорированием DRY, захотел бы что-то инкапсулировать наконец.

Более сложные ошибки выявляются после более глубокого изучения кода. Здесь на них намекать не стану.

Один листинг закрывает несколько потребностей. Во-первых, ошибки/недочёты разного уровня сложности и критичности. Код содержит ошибки, которые должен найти junior (синтаксис, NPE), и проблемы, над которыми должен задуматься senior (транзакции, архитектура, асинхронность). Во-вторых из-за наличия «шума» помогает буквально по первым словам отделить действительно сильных профессионалов:

Так, тут проблема с форматом и именами, это смотреть не будем.

В третьих, ошибки разной направленности даже в пределах одного «грейда» позволяют частично оценить реальный опыт кандидата, его кругозор, спектр проблем, которые он решал. И, наконец, код выглядит как фрагмент реальной enterprise-системы, а не синтетическая задача. Это проверяет способность кандидата работать со сложной, неидеальной codebase. Ошибки приближены к реальному легаси, а не фантазии из идеального мира литкода

Итоговая сводка по количеству ошибок (оценочно):

Примерное количество ошибок и группы ошибок (анализ ИИ плюс мои правки):

  • Intern/Junior – 11:

    • синтаксис и базовые конструкции;

    • NPE;

    • форматирование, имена;

  • Middle – 14:

    • стиль;

    • логика;

    • проверки;

    • исключения;

    • dead code, magic strings;

    • инкапсулирование, DRY, KISS;

    • верхнеуровневые паттерны типа SOLID, ACID;

    • шаблоны проектирования (как минимум – порождающие и композиционные);

  • Senior – 9 ключевых архитектурных проблем:

    • стилистика сущностей на уровне проекта, системные подходы, бест практики;

    • транзакционность/нетранзакционность;

    • конкурентность;

    • более абстрактный SOLID;

    • интеграции;

    • шаблоны, повышающие надёжность;

    • возможно, микросервисные шаблоны.

Java live coding

GolovatyyMG > Гибкость технического интервью > ChatGPT Image 10 февр. 2026 г., 14_26_38.png
Это не реклама телефонов, а перебор элементов из контейнеров.

Общекорпоративный стек обычен, как во всех современных проектах:

  • JVM (на момент старта 17, рассматриваются варианты повышения версии);

  • как основной язык Java, в некоторых случаях Kotlin 2+;

  • Spring Boot 3.3, Hibernate:

  • Kafka;

  • Postgres 15+;

  • Redis, caffeine;

  • Junit и разные дополнения к нему (springmock, mockito, instancio, mockk, kotest);

  • В некоторых проектах testcontainers.

Обычно хватает времени на 1, очень редко - 2 задачи. В целом есть 2 класса задач: на java streams и более абстрактная, на использование стандартных абстракций JDK. Первая эмулирует довольно тривиальные и характерные кейсы из почти любой энтерпрайз-системы (по крайней мере, мне встречались постоянно за последние лет 8, на 5 разных проектах). Вторая задача менее рутинная, проверяет абстрактное мышление, гибкость, и позволяет взглянуть с необычного ракурса на привычные структуры. И в том, и в другом случае не требуется ни компилятор, ни даже подсветка синтаксиса.

Чего я не просил и не буду просить на собеседовании:

  • написать сортировку. В энтерпрайзе используются библиотечные. Не обязательно из JDK, может из любого местного SDK или из одной из известной опенсорсной библиотек типа апача или гуавы.

  • написать микросервис. Нормально за время интервью этого не сделать. Давать т.н. домашнее задание – это чересчур. Делать через специфические аннотации тоже неправильно, в продуктиве такого почти никогда не бывает.

  • реализовывать кастомные аналоги JDK компонент, например HashMap. Это не имеет большого смысла в реальной работе. Это может говорить о плохом воображении опыте и насмотренности интервьювера.

  • писать сложные алгоритмы, где многабукв. Время поджимает, а стресс растёт. Для крупных задач нужно выделенное собеседование.

Задача на стримы

Задача на стримы выглядит как среднего размера листинг, в котором объявлены модели данных, их иерархия, возможно пример заполнения в виде большой константы-списка. Далее идёт текст программы или метода юнит-теста. Вычислительная часть алгоритмов обозначена многоточием, сопровождается кратким комментарием того, что нужно сделать. В качестве подсказки может быть указан тип возвращаемого алгоритмом результата. Таких участков кода обычно три, они не зависят друг от друга, но выполняются в общем контексте.

Если кратко, то задача такова:

  • даны структуры;

  • даны три переменный с комментариями, что вычислять;

  • для упрощения задачи могут быть указаны типы данных переменных;

  • для упрощения задачи может быть указан краткий тест для сверки результатов.

Очень условно можно сказать, что тут три алгоритма на три грейда:

  • junior: проверка минимального понимания стримов. Как правило без вложенности в обработке;

  • middle: композитные условия, вложенность обработки, распрямление вложенных коллекций, возможно с простой группировкой или преобразованием коллекций;

  • senior: либо что-то ближе к аналитике и статистике, либо нетривиальные коллекторы, хитрая группировка, более редко используемые операции со стримами.

Первое вычисление должно в идеале быть реализовано за пару минут, без особых вопросов. Сел и написал. Второе может допускать разные трактовки, разные оптимизации, вообще должно быть интереснее для обсуждения. Третий алгоритм стоит давать ещё сложнее второго, разумеется. Важно даже не сколько знать все методы стримов, но, главное, понимать как работать с данными, умение выстроить алгоритм. Максимум из того, что я вижу, это вот задача поиска дублей во вложенных коллекциях. Чаще третью задачку я давал не настолько сложную, как в представленном примере:

  record Product(
            String id, 
            String name,
            String category,
            BigDecimal price,
            LocalDate expiryDate,
            boolean isHazardous
    ) {}

  record StorageLocation(
            String id,
            String name,
            String zoneType,
            BigDecimal capacity,
            BigDecimal currentQuantity,
            boolean allowsHazardous,
            List<Product> products
    ) {}

  record Warehouse(
            String id,
            String name,
            List<StorageLocation> locations
    ) {}

  private static final List<Warehouse> WAREHOUSES = <константа>

  // Задание 1. Вывести список опасных товаров.
  List<String> hazardousProducts = ...

  // Задание 2. Склад с наибольшей общей стоимостью товаров
  Optional<Map.Entry<String, BigDecimal>> richestWarehouse = ...
   
  // Задание 3. Найти товары, которые есть на нескольких складах, с указанием складов    
  Map<String, List<String>> productsOnMultipleWarehouses = ... 
Полный текст задания (большой!)
package ru.logisticscompany.wms;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class WarehouseStreamTest {

    record Product(
            String id, String name, String category, BigDecimal price, LocalDate expiryDate, boolean isHazardous
    ) {}

    record StorageLocation(
            String id,
            String name,
            String zoneType,
            BigDecimal capacity,
            BigDecimal currentQuantity,
            boolean allowsHazardous,
            List<Product> products
    ) {}

    record Warehouse(
            String id,
            String name,
            List<StorageLocation> locations
    ) {}

    private static final List<Warehouse> WAREHOUSES = List.of(
            new Warehouse(
                    "WH1", "Москва Центральный",
                    List.of(
                            new StorageLocation(
                                    "L1", "Зона приемки", "RECEIVING",
                                    new BigDecimal("1000"), new BigDecimal("150"),
                                    false,
                                    List.of(
                                            new Product("P1", "Ноутбук Lenovo", "Электроника", new BigDecimal("75000"), LocalDate.of(2025, 12, 31), false),
                                            new Product("P2", "Мышь Logitech", "Электроника", new BigDecimal("2500"), LocalDate.of(2026, 6, 30), false)
                                    )
                            ),
                            new StorageLocation(
                                    "L2", "Холодильная камера", "COLD",
                                    new BigDecimal("500"), new BigDecimal("300"),
                                    false,
                                    List.of(
                                            new Product("P3", "Молоко", "Молочные", new BigDecimal("85"), LocalDate.of(2024, 3, 15), false),
                                            new Product("P3", "Молоко", "Молочные", new BigDecimal("85"), LocalDate.of(2024, 3, 20), false)
                                    )
                            )
                    )
            ),
            new Warehouse(
                    "WH2", "Самара Логистик",
                    List.of(
                            new StorageLocation(
                                    "L3", "Стеллаж", "BULK",
                                    new BigDecimal("2000"), new BigDecimal("1800"),
                                    false,
                                    List.of(
                                            new Product("P4", "Кресло", "Мебель", new BigDecimal("15000"), null, false),
                                            new Product("P5", "Стол", "Мебель", new BigDecimal("9500"), null, false),
                                            new Product("P1", "Ноутбук Lenovo", "Электроника", new BigDecimal("75000"), LocalDate.of(2025, 12, 31), false)
                                    )
                            ),
                            new StorageLocation(
                                    "L4", "Хим.хранилище", "HAZARDOUS",
                                    new BigDecimal("300"), new BigDecimal("50"),
                                    true,
                                    List.of(
                                            new Product("P6", "Краска", "Химия", new BigDecimal("520"), LocalDate.of(2025, 8, 1), true),
                                            new Product("P7", "Растворитель", "Химия", new BigDecimal("320"), LocalDate.of(2024, 12, 31), true)
                                    )
                            )
                    )
            ),
            new Warehouse(
                    "WH3", "Новосибирск Северный",
                    List.of(
                            new StorageLocation(
                                    "L5", "Зона комплектации", "PICKING",
                                    new BigDecimal("800"), new BigDecimal("600"),
                                    false,
                                    List.of(
                                            new Product("P8", "Телевизор", "Электроника", new BigDecimal("55000"), LocalDate.of(2027, 5, 1), false),
                                            new Product("P9", "Наушники", "Электроника", new BigDecimal("12000"), LocalDate.of(2026, 10, 15), false),
                                            new Product("P2", "Мышь Logitech", "Электроника", new BigDecimal("2500"), LocalDate.of(2026, 6, 30), false)
                                    )
                            ),
                            new StorageLocation(
                                    "L6", "Огнеопасные", "HAZARDOUS",
                                    new BigDecimal("200"), new BigDecimal("30"),
                                    true,
                                    List.of(
                                            new Product("P10", "Лак для волос", "Косметика", new BigDecimal("450"), LocalDate.of(2025, 3, 1), true),
                                            new Product("P11", "Аэрозоль", "Химия", new BigDecimal("280"), LocalDate.of(2024, 11, 30), true)
                                    )
                            )
                    )
            )
    );

    @Test
    void testAllLevels() {
        LocalDate checkDate = LocalDate.of(2024, 3, 20);
         
       // Задание 1. Вывести список опасных товаров. 
        List<String> hazardousProducts = ...
        
        assertEquals(List.of("Аэрозоль", "Краска", "Лак для волос", "Растворитель"), hazardousProducts);
         
        // Задание 2. Склад с наибольшей общей стоимостью товаров 
        Optional<Map.Entry<String, BigDecimal>> richestWarehouse = ...
        
        assert richestWarehouse.isPresent();
        assertEquals("Москва Центральный", richestWarehouse.get().getKey());
        assertEquals(new BigDecimal("150170"), richestWarehouse.get().getValue());
        
        // Задание 3. Найти товары, которые есть на нескольких складах, с указанием складов     
        Map<String, List<String>> productsOnMultipleWarehouses =  ...
        
        assert productsOnMultipleWarehouses.containsKey("Ноутбук Lenovo");
        assertEquals(List.of("Москва Центральный", "Самара Логистик"), 
                productsOnMultipleWarehouses.get("Ноутбук Lenovo"));
        assert productsOnMultipleWarehouses.containsKey("Мышь Logitech");
        assertEquals(2, productsOnMultipleWarehouses.get("Мышь Logitech").size());
    }
}

Представленный код носит иллюстративный характер, но не отличается принципиально от реально заданных задач.

Решение 1 с разбором

Первый алгоритм очень простой, околоджуновский уровень.

Главная задача – распрямить наборы вложенных коллекций в одну, отфильтровать по опасности каким-либо образом.

 List<String> hazardousProducts = WAREHOUSES.stream()
                .flatMap(warehouse -> warehouse.locations().stream()
                        .filter(loc -> "HAZARDOUS".equals(loc.zoneType()))
                        .flatMap(loc -> loc.products().stream()
                                .filter(Product::isHazardous)
                                .map(Product::name)))
                .distinct()
                .toList();

Тут возможно объединение условий с OR/AND, выделение в лямбды или приватные методы, наличие или отсутствие сортировки и уникальности.

Решение 2 с разбором

Второй чуть сложнее, но кроме вложенности при обработке стримов не должен вызвать сложностей. Некоторые могут забыть как правильно суммировать, Интересный нюанс – суммирование композитного типа BigDecimal, не прямое суммирование элементов стрима.
Что важно:

  • понимание, что из списка надо получать мапу или пару;

  • выстроить по каждому складу набор стоимостей или цен (экономические термины не обсуждаем);

  • рассчитанные значения поставить в соответствие с каким-то идентификатором склада;

  • выстроить кастомную сортировку;

  • взять один элемент, а не весь набор данных;

В отличие от первого варианта, тут выстраивается уже определенная последовательность действий, при нарушении которой сильно меняется результат.

Optional<Map.Entry<String, BigDecimal>> richestWarehouse = WAREHOUSES.stream()
                .collect(Collectors.toMap(
                        Warehouse::name,
                        warehouse -> warehouse.locations().stream()
                                .flatMap(loc -> loc.products().stream())
                                .map(Product::price)
                                .reduce(BigDecimal.ZERO, BigDecimal::add)
                ))
                .entrySet().stream()
                .max(Map.Entry.comparingByValue());
Решение 3 с разбором

Посмотрим, что можно тут сделать.

Сначала надо сформировать алгоритм, текстом или псевдокодом. Можно насоздавать много переменных или приватных методов, для которых подстраивать вычисления. Важна структурированность мышления.

Вариантов тут может быть много. Например:

  • Сначала можно за каждой записью товара закрепить идентификатор склада (имя, код, ID). Если подразумевается. что товар может быть на разных складах, то ведущее значение товар, вторичное – склад.

  • Получается список пар значений (Map.Entry или Pair, кому как удобнее), нужно найти одинаковые ключи и сгруппировать, сформировав список из значений. Тут важно понимать, что есть терминальная операция группировки в мапу.

  • Получили мапу идентификатором товаров на список складов. Надо посчитать количество складов для каждого товара и выбросить те, которые с 1 складом.

  • Дальше отбросить склады и их количество, ведь нас интересует только товар.

Map<String, List<String>> productsOnMultipleWarehouses = WAREHOUSES
                .parallelStream()
                .flatMap(warehouse -> warehouse.locations().stream()
                        .flatMap(loc -> loc.products().stream()
                                .map(product -> new AbstractMap.SimpleEntry<>(
                                        product.name(),
                                        warehouse.name()
                                ))
                        )
                )
                .collect(Collectors.groupingByConcurrent(
                        Map.Entry::getKey,
                        ConcurrentHashMap::new,
                        Collectors.mapping(
                                Map.Entry::getValue,
                                Collectors.toList()
                        )
                ))
                .entrySet().stream()
                .filter(entry -> {
                    long uniqueWarehouseCount = entry.getValue().stream().distinct().count();
                    return uniqueWarehouseCount > 1;
                })
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> entry.getValue().stream()
                                .distinct()
                                .toList()))
                ));

Дальше можно обсуждать параллелизм, его уместность, потокобезопасность и другие интересные, но малополезные здесь инструменты и явления.

Я всё-таки предлагаю решить задачи самостоятельно до подглядывания решений.

Задача на абстракции

Моя любимая задача на оценку мыслительного процесса. Также, как с удивлением узнал, может привести к небанальным разговорам о простых вещах в JDK. Даже разработчики с неплохим опытом работы на java могут не помнить некоторые методы и классы, но при этом зачастую правильно предполагают набор методов, которые надо реализовать и их сигнатуру. В целом большинство собеседников так или иначе справляются с задачей. Другое дело, что есть тенденция к переусложнению реализации и долгим раздумьям из-за незнания нюансов JDK (что не является минусом само по себе, но может затянуть процесс).
Как и у других, у задачи несколько вариаций, вот самая интересная на мой взгляд. Чаще всего задачи даю на java, но в исключительных ситуациях могу на kotlin. Пусть тут для примера будет kotlin, на java реализация такая же с точностью до собак и других знаков препинания.

// Есть два итерируемых множества: первое- функции преобразования (трансформации) данных без изменения типа данных, (T) -> T, второе - набор данных того же типа T.
// Данные обычные простые типы: числа, строки. Пусть для примера будут целые числа. Но реализация методов должна работать и для других типов.
// Эти множества- обычные структуры данных JDK: списки, множества. Никаких хитрых граничных условий. Наборы данных всегда заданы.
// Первый набор трансформеры, второй - значения.// Необходимо написать свою реализацию Iterable со следующей логикой: перебрать трансформеры, для каждого трансформера вызвать по одному разу значение, когда значения закончатся перейти к следующему трансформеру.
// В целом наша реализация должна удовлетворять JDKшным контрактам и поведению.


fun testInt() {
        val iterablePaired = IterableFunc(setOf({ it + 100 }, { 100 * it }), setOf(10, 16, 12, 12, 14));
        iterablePaired.forEach { System.out.println(it) }
}
fun testStr() {
        val iterablePaired = IterableFunc(setOf({ it.length.toString() }, { "<-%s->".format(it) }), hashSetOf("abc", "aaa-aaa", "cccdd", "abc"));
        iterablePaired.forEach { System.out.println(it) }
}

 private class IteratorFunc<T>(
        private val ....
        private val ....
    ): Iterator<T> {
        ......

    }

 private class IterableFunc<T>(
        iterableTransform: Iterable<(T) -> T>,
        iterableValue: Iterable<T>
    ): Iterable<T> {
        private val iterator: IteratorFunc<T> = IteratorFunc(...., ....)
        override fun iterator() = iterator
    }

В данном случае ожидается произведение этих множеств:

Для testInt:

110
116
112
114
1000
1600
1200
1400

Для testStr:

7
5
3
<-aaa-aaa->
<-cccdd->
<-abc->


Советую не смотреть ответ, а решить самостоятельно, это действительно несложно

Вариант решения

Логично выделить несколько этапов при решении:

  • Понять, что один набор данных прогоняется от начала до конце один раз, а второй надо переначитывать заново время от времени. То есть с одним набором надо работать как с обычным итератором, а для второго сохранять итератор мало, надо сохранить ещё и контейнер с элементами.

  • Вспомнить или логически подойти к тому, что нужны методы next() и hasNext() в итераторе. 

  • В качестве первого приближения предположить например, что итератор работает с другими итераторами, сформировать конструктор и вызов конструктора.

  • Сформировать тело метода hasNext(), тут достаточно простая логика.

  • Начать писать тело метода next(). Тут рано или поздно профессионал поймёт, что обратить вспять данные, выдаваемые итератором, невозможно. Значит, надо хранить ещё состояние Iteable.

  • Добавить поле с состоянием, изменить конструктор и его вызов.

  • Дальше возникнет вопрос как сбрасывать счетчик позиции у Iterable, будут вопросы по простому копированию данных (возможно несколько вариантов).

  • И финальная часть – понять как инициализировать и изменять те переменные, которые хранят состояния наборов данных values и transform.

Вот как, например, я могу реализовать этот алгоритм, если не хочу тратить слишком много времени для подготовки статьи:

private class IteratorFunc<T>(
        private val iteratorTransform: Iterator<(T) -> T>,
        private val iterableValue: Iterable<T>
    ): Iterator<T> {
        private var currentTransform: ((T)->T) = if (iteratorTransform.hasNext()) { iteratorTransform.next() } else { it: T -> it }
        private var iteratorValue = iterableValue.toList().iterator()

        override fun hasNext() = iteratorValue.hasNext() || iteratorTransform.hasNext()

        override fun next(): T = if (iteratorValue.hasNext()) { currentTransform(iteratorValue.next()) } else {
            currentTransform = iteratorTransform.next()
            iteratorValue = iterableValue.toList().iterator()
            currentTransform(iteratorValue.next())
        }

    }

Не предполагается, что сразу будет предложено идеальное решение, итеративный подход вполне себе жизнеспособен.

Задача нарочно сформулирована так, чтоб дать повод интересно пообщаться на разные темы: от паттернов и устройства памяти до особенностей синтаксиса или даже оптимизации по скорости или памяти.

Итоги

Техническое интервью содержит несколько секций: монологи (рассказ о компании и проекте, самопрезентация), диалоги (технически-практические и более теоретически-философские), «работа руками». По набору факторов можно оценить примерный уровень собеседника (не оппонента!) даже с учётом того, что невозможно знать и уметь всё. Из моей практики именно общение уже даёт представление о том, какой код будет «накожен» и насколько подробно надо формулировать задачу и ограничения. Именно диалоги и анализ ответов позволяет выбрать наилучшие варианты задач.

Более подробная мотивация и нюансы выбора задач, вопросов, необходимость «подстройки» под собеседника и варианты снижения стресса я описывал раньше. Теперь, надеюсь, стало понятнее, к каким практическим результатам привела такая философия.

По совокупности успешности выполнения разных секций мы с руководителем принимали решение о аппруве/отказе и назначении грейда. Как правило наш взгляды полностью совпадали, реже – расходились совсем в мелких деталях. В итоге около десятка удачных трудоустройств как в мою команду, так и в смежные.

GolovatyyMG > Гибкость технического интервью > ChatGPT Image 9 февр. 2026 г., 19_08_49.png
Результаты хорошего интервью.

А как создаёте задачи для технического интервью вы? Что интересного встречалось в других компаниях? 

Комментарии (16)


  1. ruomserg
    13.02.2026 17:36

    Объясните, зачем вам задача на SQL live coding, и на стримы ?!

    Лично я с позиции своего опыта - убил бы за запросы со сложной иерархией. Во-первых, если вы их каждый день не пишете - то фига с два вы помните все особенности синтаксиса. Особенно - приятные различия между Oracle и Postgres. Во-вторых, вы офигеете это отлаживать, в особенности когда план запроса не захочет строиться перебором, а потребует чего то типа Genetic Query Plan Optimization. То есть - у вас будет выходить РАЗНЫЙ план запроса на разных базах и в разное время. Будет еще смешнее если вы туда воткнете параметризацию - потому что в сочетании с ORM - оно вам закеширует query plan построенный без знания реальных значений параметров - а потом будет его исполнять, даже если с конкретным значением в подстановках - он будет сильно неоптимальный... И с этим вы будете отдельно бороться!

    А стримы - ИМХО вообще дико перегретая тема. Да - много на чем можно людей подловить, ибо разных методов там дофига и больше (а еще запомнить где какой параметр в темплейтах - м-м, песня). На практике, когда мне кто-то приносит MR в котором очень умно скомбинированы все возможности стримов - я спрашиваю, есть ли шанс что мы вот в этом получим реальный выигрыш за счет ленивых операций и (возможно) параллелизма? И если нет (а зачастую там коллекция из пары десятков, если не единиц элементов) - начинаю цитировать новгородскую берестяную грамоту: "Брате, (нецензурное слово из трех букв на "е") лежа!". Если у вас в стриме сложная обработка - пишите кастомный фильтр, или вообще выразьте свои намерения в императивном, а не декларативном формате через for с итерацией по коллекции...

    А вот за секцию с код-ревью - однозначный плюс в карму. Ибо 70% времени мы читаем код - а проверяем на собесах совершенно другое!

    За секцию с итератором - не знаю. Если вы регулярно такое пишете по необходимости у себя в проекте - тогда наверное плюс. Если же вы это придумали как головоломку которую надо решить в условиях стресса (или знать решение заранее) - тогда минус. Потому что вы опять проверяете не то, что надо - а то, что легче проверить. Вменяемый человек не держит в памяти особенности реализации JDK, и в первую очередь должен задать вопрос - обязательно ли вашу задачу надо решать через вот такие извращения с итераторами, и неявным сохранением (копированием) коллекций под капотом ? Ибо интуитивно это похоже на то, что человека подводят к столу, и просят съесть апельсин, но по ходу дела выясняется, что руками трогать его нельзя, наклоняться к столу тоже нельзя - а ожидается что кандидат достаточно находчив и гибок чтобы это сделать отвернувшись от стола, и обязательно через жопу...

    Чтобы не выглядеть огульным критиканом - скажу свою задачу на собеседование для лайв-кодинга. Есть устройство с дисплеем на 20 знако-мест. Интерфейс простейший: device.display(String). Время от времени нам приходят сообщения от системы явно больше чем 20 символов длины. Нужно придумать и реализовать показ их пользователю. Принимается любое разумное решение, которое кандидат сможет реализовать, время пошло. И дальше там тоже есть о чем поговорить...


    1. shadowphoenix Автор
      13.02.2026 17:36

      Есть набор требований "сверху". Их здесь оценивать не будем. Среди них- умение разбираться в некоторых вопросах. Значит можно или скопипастить задачу с какого-то известного сайта или дать свою. Я не вносил новые секции, я переработал уже существовавшие. Если было требование sql, значит пишем sql. Кажется, я сразу задал контекст рассматриваемого вопроса: я не владелец бизнеса и не CTO, я делаю по набору требований. Но знать запросы надо, полтора года эксплуатации прода подтверждают.

      Запросы в целом пишем, так что не понятен негатив. И при разработке, и при поддержке. Если человек перенервничал например на ревью или на теоретической секции, а утверждает, что проектировал базы, почему бы не дать шанс и не пообщаться на такие темы? Чаще всего достаточно простых вариаций задач. Это я описал, какие задачи когда и как часто используются.

      Не будет там разных баз, так что однозначность и детерминированность есть.

      Задача на стримы не про показ новых технологий и чистого кода. Со стримами почти все работали, понижает сложность и стресс. Хорошо или плохо в продуктиве - не тема этой статьи. Цель использования задач со стримами вроде доходчиво мотивирована в тексте.

      Вопросы параллелизма стримов на интервью вообще касаются только каждый сотый раз. Не вижу смысла сильно акцентрироваться. Пример кода это не must have. Хорошо, что вы заметили этот нюанс.

      Итератор как пример. Я вариаций 6 давал в разное время. Есть и более простые. Надо знать или понимать примитивы. Интервью это не допрос и не дуэль, а диалог. На правильный вопрос будет правильный ответ. И эти вопросы не менее важны, чем решение. Если убрать цель "самоутвердиться" или цель унизить, то задача становится по силам большинству кандидатов.

      У вас есть на готове настолько же короткая задача, которая проверяет и знание jdk, и мыслительный процесс? Описанная не выглядит настолько же краткой, не уверен, что сотрудник банка быстро поймет предметную область. В любом случае, тут больше про вкус фламастеров, а не качество фильтра.


      1. ruomserg
        13.02.2026 17:36

        Если вам требования спускают сверху, а вы их только исполняете в ходе интервью - к вам претензий нет, скорее они к тем, кто вам их спустил...

        Про запросы - я с базами данных работаю года этак с 96'го. Помню и Query-by-example в Paradox, и синтаксис 4GL в Progress v9, и разумеется Oracle/Postgres в их развитии. Но видит бог, я вам сходу на бумажке не напишу сложный запрос с window functions, having, и прочими интересными вещами. И если вы на моих глазах начнете такое писать, то попробую вас отговорить от самоубийства. Ибо сложные запросы - это обычно признак того, что в проекте по недосмотру архитектора смешаны OLTP и аналитика! И вы пытаетесь достать аналитические значения из OLTP базы. Понятно, что если прижало - то еще не так раскорячишься - но я буду корячиться в обнимку с документацией, итеративно отлаживая запрос - и поминутно гляля в explain analyze чтобы понять - не положу ли я продашкн устраивая fullscan или hash-join на таблицах реальных размеров...

        По задачам на стримы, как и по задачам на странные итераторы - претензия в том, что опять непонятно, что именно вы проверяете. Я помню как стримы и итераторы появились в толстой книге Страуструпа - и дальше разошлись по разным языкам программирования. Это совершенно не значит, что я на месте вам вспомню детали их реализации. Максимум чего бы я в этом смысле смотрел - это понимает ли человек как работают стримы - и может ли он переписать код со стримами на код без них ? Потому что есть деятели, которые запоминают методы стримов как магические слова...

        В мире финансов - моя любимая задача - это дать на ревью код, где проценты и остатки хранятся в double. В зависимости от того, насколько быстро и насколько матерно кандидат начнет ругаться - можно сделать вывод о его практическом опыте в отрасли. :-) Дальше - если мы говорим о серьезном уровне, я бы продолжил говорить в сторону того, как этот код изменять и поддерживать если к нам будут приходить умные маркетологи и периодически менять правила игры - кому, что и за что начисляем...

        Вообще - главный вопрос который надо решить на собеседовании - это умеет ли человек ясно и однозначно мыслить о свойствах сложных систем ? Ибо есть люди у которых принципиально каша в голове - и какой бы вы не дали им язык программирования и фреймворк - результат будет неудовлетворительный. Второй вопрос - это опыт кандидата с языком и нашей предметной областью - что влияет на время адаптации и самостоятельность. Поэтому я против задач с leetcode, и за то чтобы давать задачи приближенные к предметной области (но упрощенные для того чтобы влезть в стандартные 30 минут на решение). И однозначно против "китайского экзамена по каллиграфии", когда мы обсуждаем все более специальные вопросы (типа устройства JDK или конкретных правил happens-before) в надежде что кандидат в какой-то момент дойдет до предела своих знаний...


        1. shadowphoenix Автор
          13.02.2026 17:36

          Ибо сложные запросы - это обычно признак того, что в проекте по недосмотру архитектора смешаны OLTP и аналитика

          С чего бы? Для расследования ошибок в проде не надо писать запросы? Будут ли они такие же, как на hql? Не стоит же задача "написать запрос для hibernate"?

          Максимум чего бы я в этом смысле смотрел - это понимает ли человек как работают стримы - и может ли он переписать код со стримами на код без них ? Потому что есть деятели, которые запоминают методы стримов как магические слова...

          Я проверял, а знает ли человек эту магию вообще. Про устройство даже не начинаю говорить. Незачем, цель другая.

          Вообще - главный вопрос который надо решить на собеседовании - это умеет ли человек ясно и однозначно мыслить о свойствах сложных систем ?

          Еще требование знать N библиотек, M паттернов. От платформы, от корпорации.

          Приоритет могут ставить выше.


          1. ruomserg
            13.02.2026 17:36

            Какая, блин, глупость! Я имею в виду - ставить вперед "N библиотек и M паттернов"! Следуя вашей логике - мне году в 2004 надо было в сибирской глубинке искать специалистов по 4GL... Как-то я сомневаюсь, что они там вообще были в это время. Тем более - безработные. Если у человек мозг работает в правильную сторону, то он прекрасно переходил от решения задач на Delphi к 4GL, а еще к "C", и далее к Java.

            С тем же успехом можно слесаря так нанимать: "Скажите, а вы красным (!) молотком умеете по полуоси стучать? Нет я понимаю, что у вас опыт работы - но вот у нас молотки только красные - и мы предпочитаем кандидатов которые именно с этим инструментом работали...".


            1. shadowphoenix Автор
              13.02.2026 17:36

              Эти требования спускаются выше. Глупость или не глупость я буду решать будучи CTO, но сейчас у меня другая должность.

              Я не указываю вам как надо было искать, я не знаю нюансов вашего проекта. А вы не знаете всех требований моего проекта и холдинга.


            1. shadowphoenix Автор
              13.02.2026 17:36

              Если директор сказал искать стучальщиков красными молотками- мне надо выполнять или надо приводить абстрактные аргументы и вставать в позу "я самый умный"?

              У конкретного бизнеса есть конкретные потребности и запросы, лучше удовлетворять их без оглядки что у кого-то абстрактного было в 2004. Я решаю задачи конкретного проекта, а не занимаюсь философией. Есть конкретный стек, конкретные библиотеки, конкретный продукт, и это никак не сыязано с Сибирью, хрущевской оттепелью, холодной войной или Жанной д'Арк. Таких параметров в этой задаче просто нет. И я не рассказываю, как в 1982 году искать программистов на Ruby.


              1. ruomserg
                13.02.2026 17:36

                Вопросов к вам, как я написал выше - у меня нет. К тому, кто вам выдает такие требования - есть... Но вот опять же из жизненного опыта - есть две стратегии: можно в рамках организационного люфта (я надеюсь что ваш начальник - не микроменеджер) исправлять глупости начальства, а можно лихо и задорно их усугублять. Вот ваша статья - это мануал на тему: как глуповатые требования руководства превратить в абсурдные вопросы на интервью.

                Но правда и в том, что я работаю не с вами, и не в вашем холдинге. Поэтому, продолжайте в том же духе - если вы не правы, жизнь вам это рано или поздно объяснит...


                1. shadowphoenix Автор
                  13.02.2026 17:36

                  Да причём тут глуповатые или нет? Все действие и развитие в чётко очерченных рамках. Можно копать, можно не копать. А можно там, где ограничения чуть менее явно заданы, что-то налаживать и улучшать.

                  Да, большинство задач проксирование и крудостроение по заданным правилам. И часто используются стримы (без референса на тотхорошо или плохо). Но есть 10% нестандартных задач и, по ходу выкатки продуктов на прод, будет всё больше и больше задач ребусов поддержки. И вот там критически важно быстро и надёжно выявлять и править ошибки. Там и всякие SLA, и репутация, и прочее. В таких задачах почти никогда не прокатит отговорка "это всё сделает ORM". Как показывает опыт, почти все при выявлении ошибок в данных используют 100500 однострочных скриптов, копируя туда-сюда UUID'ы. Это, мягко говоря, неэффективно-- использовать непонятные селекты, магически комбинируя, вместо написания 1 запроса.

                  И реализация по интерфейсы - вовсе не олимпиадная задача. Похожее периодически встречается при разработке пользовательских историй, и на более сложных интерфейсах. Тем более, задача на live coding, как я писал пару раз, направлена не сколько на знание синтаксиса, а на другое.

                  Кажется, я двумя статьями так и не смог до всех донести мысль о том, что главное в интервью - слышать и говорить. Лично мне разговоры почти всегда дают понять заранее, какие результаты кодинга будут.


    1. shadowphoenix Автор
      13.02.2026 17:36

      На мой взгляд, задача с нечеткой постановкой на более высокий уровень. Именно потому что не только НФТ отсутствуют, но и ФТ не все прописаны:

      • есть ли органы управления?

      • можно ли прокручивать?

      • может ли устройство само включать бегущую строку?

      • одинаков ли буфер для кириллицы и латиницы?

      • какие ограничения по кодировке?

      • почему изначально на входе нет фильтра при сохранении в системе длинных строк?

      Ну и в целом, лично у меня такая задача вызовет чувство абсурдности:

      1. пишем на джаве, то есть контейнер в контейнере в контейнере

      2. сверху всякая оркестрация и гейтвеи

      3. и вывод на дисплей с 20 символами.

      Это как вообще?

      Я бы, встретив такую задачу, вспомнил бы бесмыссленность некоторых оптимизаций на литкоде.

      Ну и, самое, пожалуй, главное. Это задача больше инженерная. Те разработчики, миддлы с галер и финтехов, чаще возраста 25-30,не инженеры, часто не имеют (не показывают) инженерный склад ума. Кандидаты возраста 40+ может более целевая аудитория для таких проверок. Но их мало приходило. И абстракции с перечислениями были поеятны, затруднений не вызывало.

      Да, задача прикольная и интересная, но не для моих задач, которые описываю. Она имеет другую постановку, требует выявления совсем других требований, нежели продуктовая разработка. В продукте стримы, мапперы, ретраи, работа со структурными списками. Так зачем давать задачу про дисплей? Не уместнее задачи на сортировки, маппинг, изоляцию и транзакционность?


      1. ruomserg
        13.02.2026 17:36

        Ну блин - эта задача про дисплей абсолютно реальная. Производственное оборудование - оно вот такое бывает... Со странностями. :-) И да, я приветствую заданные вопросы. И если вы предложите архитектурное решение - типа пробросить device capabilities внутрь сессии, и там сделать умный i18n который будет из базы выбирать сообщение не только по языку, но и по capabilities устройства - в моей голове вы получите жирный плюс. Но я предложу пока оставить это для следующего мажорного релиза и придумать как заткнуть проблему "прямщас". И я приму любое решение, если вы сможете сделать чтобы оно работало: хотите - бегущую строку, хотите - пейджинг, хотите - слова сокращайте, хотите что-то еще придумайте. Мне важно понять, что вы вообще умеете программировать - я видел лично людей, которые блин не могли реализовать бегущую строку в цикле... А работы (если вы хоть как-то программировали) - там на 10 минут!

        Опять же - я не защищаю конкретную задачу. В зависимости от отрасли - я бы давал то, с чем мы работаем, и что позволяет строить разговор дальше. Про транзакционность - моя любимая задача была - это единый вход в transactional-annotated, и там некая бизнес-операция и логгирование успеха или неудачи операции с выкидыванием экспшена. Ну и понятно, что если транзакция помечается как rollback-only, то и в лог-таблицу ничего не пишется. И дальше смотрим как человек предлагает с этим разбираться...


        1. shadowphoenix Автор
          13.02.2026 17:36

          Для стека джава не подходит. Буду собеседовать дельфиста - вспомню.


  1. Tzimie
    13.02.2026 17:36

    некоторые не признаются, пытаясь на ходу то ли вспомнить, то ли придумать синтаксис

    То есть ведут себя как классическая LLM)


  1. Tzimie
    13.02.2026 17:36

    Я использовал этот вопрос

    Рядовой SNAFU идет в DBA / Хабр https://share.google/qPPbyN4drwqVrqxkJ


  1. rootCore
    13.02.2026 17:36

    Автор проделал, без сомнения, большую работу, систематизируя свой подход. Однако, при ближайшем рассмотрении, он создал не систему отбора лучших инженеров, а элитный клуб для любителей олимпиадных задач и викторин на знание, который в современных реалиях абсолютно неэффективен.

    Лайв-кодинг в блокноте - главный провал методики Это самая абсурдная и оторванная от жизни часть. Требовать писать код в онлайн-редакторе без привычной IDE - это как просить хирурга провести операцию перочинным ножом, а потом удивляться, что он не помнит точное латинское название каждого инструмента.

    Проверка памяти, а не интеллекта. Современная разработка - это не про запоминание всех методов Collectors или сигнатур функций. Это про умение решать бизнес-задачи с помощью инструментов. IDE - наш главный инструмент. Отказывая в нём, интервьюер проверяет не способность мыслить, а банальную зубрежку.

    Искусственный стресс. Никто не пишет код в таких условиях. Это искусственно созданная стрессовая ситуация, которая отсеивает не плохих разработчиков, а тех, кто хуже справляется с бессмысленным давлением.

    Игнорирование реальности. Автор сам пишет про Spring Boot, Hibernate, Kotlin. Все эти технологии подразумевают активное использование IDE для навигации, автодополнения и рефакторинга. Заставлять писать на них код вслепую - это полный нонсенс и демонстрация непонимания рабочего процесса.

    Гонка вооружений с нейросетями, которую невозможно выиграть Автор создает настолько сложные и "хитрые" задачи, что сам же толкает кандидатов на жульничество.

    Стимуляция обмана. Чем сложнее и академичнее задача, тем выше соблазн быстро "спросить" у ChatGPT. В итоге соревнование идет не в знаниях, а в скорости и незаметности использования нейросетей.

    Неверный результат. Вместо того чтобы нанять человека, который умеет рассуждать, компания нанимает лучшего "оператора" нейросети. Автор жалуется на некомпетентность, но его же система и способствует ее сокрытию.

    "Хитрые" SQL-задачи и Code Review как поиск "пасхалок" Подход к базам данных и ревью кода страдает той же болезнью - фокусом на эзотерических знаниях, а не на практической ценности.

    SQL-акробатика. Автор сам признает, что большинство использует ORM, но упорно продолжает гонять кандидатов по "оконным функциям" и "хитрым группировкам". Да, это полезно знать, но для 95% задач бэкенд-разработчика это не является ключевым навыком. Гораздо важнее уметь спроектировать нормальную модель данных, а не написать монструозный запрос, который потом никто не сможет поддерживать.

    Ревью ради ревью. Задача ревью - улучшить код и помочь коллеге, а не сыграть в "найди 10 отличий" с кодом, который нарочно написан как можно хуже. Этот "фрагмент реальной enterprise-системы" выглядит как свалка всех возможных анти-паттернов. Хороший инженер может не найти все 34 "ошибки", потому что в реальной жизни он бы написал этот код совершенно иначе с самого начала.

    Итог: Кого на самом деле ищет автор?
    Эта система отбирает людей, которые:

    Обладают феноменальной памятью.

    Отлично справляются с бессмысленным стрессом.

    Имеют опыт решения олимпиадных задач.

    Либо очень хорошо умеют жульничать.

    Но являются ли они лучшими командными игроками, архитекторами и инженерами, способными создавать и поддерживать сложные системы? Очень сомнительно.

    В погоне за "идеальным" кандидатом, который знает наизусть все тонкости SQL и JDK, автор рискует упустить десятки отличных, прагматичных инженеров, которые просто хотят делать свою работу с помощью современных инструментов, а не сдавать экзамены из прошлого века.


    1. shadowphoenix Автор
      13.02.2026 17:36

      Я не упустил, я набрал. Писал ранее. Вывод-догадка не верен.