Для кого статья: для собеседующих в первую очередь, и для кандидатов.
О чём статья: о задачах, разработанных мною для технических интервью бэкенд-разработчиков уровня 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
Результаты хорошего интервью.

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Не раздражает ли обилие кода в статье?
22.22%Нет, не раздражает2
0%Да, не раздражает0
11.11%Люблю код в статье в любых количествах1
44.44%Большие листинги в статье не читаю4
22.22%Я просто посмотреть статистику2
Проголосовали 9 пользователей. Воздержавшихся нет.