Перейти к содержанию

Собес в Альфа-Банк (Alfa Digital) · Java Middle

Гайд для подготовки к Java Middle interview в Альфа-Банк / Alfa Digital: Spring Boot, Kafka, PostgreSQL, Docker, Kubernetes, JUnit и live coding. Внутри — вопросы, задачи и фокус технического интервью.

Темы: Java 11+ · Spring Boot · Kafka · PostgreSQL · Docker · Kubernetes · JUnit · Microservices

← Ко всем гайдам · Канал JavaJub в Telegram


1. Про Альфа-Банк и формат собеса

Альфа-Банк — крупнейший частный банк России, №1 в рейтинге работодателей hh.ru (2025). IT-подразделение Alfa Digital разрабатывает мобильный банк, антифрод-системы, платёжные сервисы, кредитные конвейеры и десятки других продуктов. Для Java Middle это означает: микросервисы, высокие нагрузки, промышленный стек. По отзывам кандидатов на DreamJob, собеседование в Альфу длится 1–2 часа и включает live-coding, вопросы по SQL, обсуждение опыта и проектов. Интервьюеры ценят умение рассуждать вслух и объяснять свои решения.

Как устроен процесс

Этап Длительность Что проверяют
Скрининг HR 20–30 мин Мотивация, ЗП, стек, ожидания
Тех-интервью 60–90 мин Java Core, live-coding, SQL, Spring
Системный дизайн 30–45 мин Архитектура микросервисов, Kafka
Проверка СБ до недели Стандартная для банков: анкета, проверка данных
Оффер Условия, команда, формат работы

ФИШКА. Ключевая особенность По отзывам кандидатов: в Альфе ценят умение рассуждать вслух и писать код в реальном времени. Это не экзамен по теории — это проверка инженерного мышления. Если не знаешь — скажи «не сталкивался, но предположу...» — честность ценят.

2. Стек по вакансии

На апрель 2026 года Альфа-Банк активно нанимает Java-разработчиков в Alfa Digital. Стек определён по вакансиям на hh.ru и job.alfabank.ru. Проекты: антифрод, кредитный конвейер, Альфа-Онлайн, электронное подписание.

Обязательный минимум

  • Java 11+ (по некоторым вакансиям до Java 21), опыт от 1–2 лет

  • Spring / Spring Boot — знать, как работает «под капотом» (прокси, автоконфигурация)

  • Микросервисная архитектура — понимание принципов, декомпозиция, взаимодействие

  • Kafka — брокеры сообщений, топики, партиции, consumer groups

  • PostgreSQL / Oracle — SQL, индексы, транзакции, EXPLAIN ANALYZE

  • Docker, Kubernetes / OpenShift — контейнеризация и оркестрация

  • JUnit 5, Testcontainers, WireMock — обязательно умение писать тесты

  • CI/CD (Jenkins / GitLab CI) — понимание пайплайна

  • Git, code review — ежедневный рабочий инструмент

Будет плюсом

  • MongoDB, Elasticsearch — NoSQL-хранилища

  • Redis / Hazelcast / Infinispan — кэширование данных

  • RxJava / Project Reactor — реактивное программирование

  • Kotlin — встречается в некоторых командах

  • Опыт в банковских / финтех проектах

ВНИМАНИЕ · Что это значит Стек Альфы заточен под микросервисы и высокие нагрузки. Kafka, Kubernetes и кэширование — не «плюсы», а рабочие инструменты. На собесе спросят: «А что будет, если Kafka-consumer упадёт?» или «Как обеспечить идемпотентность?»

3. Java Core — что точно спросят

По отзывам кандидатов в банковские IT: equals/hashCode + HashMap — абсолютные чемпионы по частоте. Спрашивают все, абсолютно все. Далее идут Stream API, исключения и String Pool.

Базовые вопросы

  • Что такое JDK, JRE, JVM? JVM — виртуальная машина, исполняет байт-код. JRE = JVM + стандартные библиотеки (нужно для запуска). JDK = JRE + инструменты разработки: javac, jdb, jar. С Java 9 JRE как отдельный дистрибутив не поставляется.

  • Области памяти JVM. Heap — хранение объектов, управляется GC. Stack — фрейм для каждого вызова метода (локальные переменные, ссылки, примитивы). Metaspace (до Java 8 — PermGen) — метаданные классов, пул интернированных строк. PC Register — адрес текущей инструкции потока. Native Method Stack — для JNI-вызовов.

  • Почему String immutable? Четыре причины: 1) безопасность — строки передаются в конструкторы файлов, БД, сетевых соединений, изменение привело бы к уязвимостям; 2) потокобезопасность — можно шарить между потоками без синхронизации; 3) кэширование hashCode — вычисляется один раз; 4) String Pool — экономия памяти.

  • Контракт equals/hashCode. Если a.equals(b) == true, то a.hashCode() == b.hashCode(). Обратное необязательно (коллизии допустимы). equals(null) → false. Переопределяешь один — переопределяй оба. Практический пример: положить объект в HashSet, изменить поле в hashCode — объект потеряется, contains() вернёт false.

  • Что сломается, если hashCode() вернёт константу? Все объекты попадут в один bucket HashMap. Java 7: связный список O(n). Java 8+: при 8 коллизиях И capacity ≥ 64 — перестройка в red-black tree O(log n). Это лучше O(n), но деградация по сравнению с O(1).

  • checked vs unchecked исключения. Checked: от Exception (не RuntimeException) — компилятор требует throws/catch. Примеры: IOException, SQLException. Unchecked: от RuntimeException и Error. Примеры: NullPointerException, IllegalArgumentException, ClassCastException. Современные фреймворки (Spring, Hibernate) предпочитают unchecked.

  • try-with-resources. Автоматически вызывает close() в finally. Ресурс реализует AutoCloseable. Закрытие в обратном порядке объявления. Если и try, и close бросают исключение — close-исключение подавляется (getSuppressed()). С Java 9 можно передать effectively final переменную.

  • Функциональный интерфейс. Ровно один абстрактный метод (SAM). @FunctionalInterface — опциональная аннотация, но защищает от случайного добавления второго метода. Основные: Function, Predicate, Consumer, Supplier, UnaryOperator, BiFunction, Comparator, Runnable, Callable.

  • Stream API: промежуточные vs терминальные. Промежуточные (filter, map, flatMap, sorted, distinct, peek, limit, skip) — ленивые, возвращают Stream, не выполняются пока нет терминальной. Терминальные (collect, forEach, count, reduce, findFirst, toList) — запускают конвейер. Стрим одноразовый — после терминальной повторно нельзя.

  • map vs flatMap. map: T → R, один элемент → один. flatMap: T → Stream с уплощением. Пример: List> → flatMap(Collection::stream) → плоский Stream. Для Optional: flatMap избегает Optional>.

  • Optional — когда? Для возвращаемых значений методов. НЕ для полей (не Serializable), НЕ для параметров, НЕ для коллекций (пустая лучше). Антипаттерны: Optional.get() без isPresent(), Optional в полях, Optional.

  • final — класс, метод, поле. Класс — нельзя наследовать (String, Integer). Метод — нельзя переопределить. Поле — нельзя переприсвоить ссылку, но можно менять внутреннее состояние (final List — можно add/remove). effectively final — нужна для лямбд.

  • Абстрактный класс vs интерфейс. Абстрактный класс: состояние, конструкторы, реализация. Наследуется один. Интерфейс: контракт, любое число. Java 8 — default/static. Java 9 — private. Когда что: общее состояние → абстрактный класс, контракт для разных иерархий → интерфейс.

Задачи «Что выведет?»

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

Тест 1. Передача по значению

Integer i = Integer.valueOf(1);
inc(i);
System.out.println(i); // ?

private static void inc(Integer i) { i++; }

Ответ: 1. Java передаёт ссылки по значению. i++ создаёт новый объект Integer(2) и присваивает локальной переменной. Оригинал не меняется. То же самое со String — неизменяемый объект.

Тест 2. Integer cache

Integer i1 = Integer.valueOf(127);
Integer i2 = Integer.valueOf(127);
System.out.println(i1 == i2); // ?

Integer i3 = Integer.valueOf(128);
Integer i4 = Integer.valueOf(128);
System.out.println(i3 == i4); // ?

Ответ: true, false. IntegerCache кэширует значения от -128 до 127. Для 127 — один объект, ссылки совпадают. Для 128 — два разных объекта. Мораль: для объектов всегда equals(), никогда ==.

Тест 3. Stream API — ленивость

List<Integer> nums = List.of(1, 2, 3, 4, 5);
nums.stream()
    .map(x -> { System.out.print(x+" "); return x; })
    .filter(x -> x > 2)
    .map(x -> { System.out.print(x+" "); return x; })
    .toList();

Ответ: 1 2 3 3 4 4 5 5. Стрим обрабатывает элементы вертикально: каждый элемент проходит всю цепочку. Элементы 1 и 2 не проходят filter → печатаются один раз. Элементы 3,4,5 проходят → печатаются дважды.

СОВЕТ. Лайфхак На вопрос про equals/hashCode приведи практический пример: «если положить объект в HashSet и изменить поле в hashCode — объект потеряется, contains() вернёт false». Это показывает понимание, а не заученность.

4. Коллекции

HashMap — абсолютный чемпион. По опыту кандидатов в банки: «обсасывают бедную мапу со всех сторон». Готовься рассказывать устройство, коллизии, treeify threshold, resize, null-ключи.

  • Основные интерфейсы Collection Framework. Collection (List, Set, Queue) и Map (отдельно, НЕ наследует Collection). List — упорядоченный с дубликатами. Set — без дубликатов. Queue — очередь. Map — пары ключ-значение. Реализации: ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, LinkedHashMap, ConcurrentHashMap, ArrayDeque, PriorityQueue.

  • Как устроен HashMap? Массив бакетов Node[]. Размер — степень двойки (default 16). Индекс: (n-1) & hash(key), hash — XOR верхних 16 бит с нижними (spread). Коллизии — связный список. Java 8+: при TREEIFY_THRESHOLD=8 элементах в бакете И capacity ≥ MIN_TREEIFY_CAPACITY=64 — перестройка в red-black tree. При уменьшении до 6 — обратно (UNTREEIFY_THRESHOLD).

  • load factor и resize. Порог 0.75 по умолчанию. При size >= capacity * loadFactor — resize: массив вдвое + перехеширование ВСЕХ элементов (дорого!). Совет: если знаешь количество элементов — задай initialCapacity = expectedSize / 0.75 + 1.

  • HashMap vs ConcurrentHashMap. HashMap: не потокобезопасен, допускает 1 null-ключ, null-значения. ConcurrentHashMap: потокобезопасен. Java 7 — Segment. Java 8+ — CAS + synchronized на головах бакетов (лучше параллелизм). null-ключи и null-значения ЗАПРЕЩЕНЫ. computeIfAbsent — атомарная «проверь и вставь».

  • ArrayList vs LinkedList. ArrayList: O(1) доступ по индексу, O(n) вставка в середину, дружелюбно к CPU-кэшу (непрерывная память). LinkedList: O(1) вставка в начало/конец (если есть ссылка), O(n) доступ по индексу. На практике ArrayList почти всегда лучше — даже вставка в середину быстрее из-за локальности кэша.

  • Fail-fast итератор. ConcurrentModificationException при изменении коллекции не через сам итератор. Реализован через modCount. Не гарантирован в многопоточной среде — только best effort. Альтернатива: CopyOnWriteArrayList (fail-safe, работает с копией).

  • TreeMap vs LinkedHashMap. TreeMap: красно-чёрное дерево, O(log n), ключи отсортированы (Comparable/Comparator). LinkedHashMap: HashMap + двусвязный список — порядок вставки. accessOrder=true — LRU-кэш. removeEldestEntry() для ограничения размера.

  • Как сделать List неизменяемым? List.of(1,2,3) — Java 9+, immutable, null запрещён. List.copyOf(list) — Java 10+, глубокая неизменяемая копия. Collections.unmodifiableList(list) — обёртка (изменения в оригинале видны!). Предпочитай List.of или List.copyOf.

5. Многопоточность и JMM

Для Middle ждут уверенное понимание synchronized, volatile, happens-before и пулов потоков. JMM — обязательная тема для банков с высоконагруженными системами.

  • synchronized. Захватывает монитор объекта. Instance-метод — монитор this. Static — монитор Class. Блок — указанный объект. Только один поток. Reentrant: поток может повторно захватить свой монитор (счётчик). Гарантирует: mutual exclusion + happens-before (видимость).

  • volatile. Видимость между потоками (запрет кэширования в регистрах/L1-кэше) + запрет reordering вокруг volatile. НЕ гарантирует атомарность i++ (read-increment-write = три операции). Для атомарных — AtomicInteger.

  • happens-before. Ключевое отношение JMM. Если A happens-before B, то записи в A видны в B. Примеры: unlock → lock того же монитора. volatile write → read. Thread.start() → первая инструкция потока. Последняя инструкция → Thread.join(). final-поля видны после конструктора.

  • Atomic-классы. AtomicInteger, AtomicLong, AtomicReference. CAS (Compare-And-Swap) — атомарная инструкция CPU. Lock-free. Методы: get, set, compareAndSet, incrementAndGet. Для счётчиков с высоким contention — LongAdder (лучше масштабируется).

  • wait/notify — почему while? Spurious wakeup: JVM/ОС может разбудить поток без notify. while (!condition) { wait(); }. Важно: вызывать ТОЛЬКО внутри synchronized на том же объекте, иначе — IllegalMonitorStateException. notify() будит один поток, notifyAll() — все.

  • Пулы потоков. newFixedThreadPool(n) — фикс. число, LinkedBlockingQueue. newCachedThreadPool — неограниченно (ОПАСНО — OOM). newScheduledThreadPool — периодические задачи. В проде: ThreadPoolExecutor вручную с corePoolSize, maxPoolSize, BlockingQueue и RejectedExecutionHandler.

  • ThreadLocal — опасность. С пулами потоков: поток переиспользуется, ThreadLocal остаётся от предыдущей задачи (утечка данных/памяти). Обязательно: try { ... } finally { threadLocal.remove(); }. В Reactor/WebFlux — не работает, нужен Context.

  • Deadlock. Два+ потока ждут друг друга. Условия Коффмана: mutual exclusion, hold and wait, no preemption, circular wait. Избегать: упорядочить захват мониторов (lock A → lock B), tryLock с таймаутом. Обнаружение: jstack, VisualVM.

  • CompletableFuture. thenApply: T → R. thenCompose: T → CF (flatMap). thenCombine: объединение двух CF. exceptionally: обработка ошибки. Async-варианты выполняются в ForkJoinPool.commonPool() или указанном Executor.

6. Spring и Spring Boot

В вакансии Альфы прямо указано: «знаешь, как работает Spring/Spring Boot под капотом». Поверхностного понимания недостаточно — будут копать в прокси, автоконфигурацию, жизненный цикл бинов.

  • IoC и DI. IoC — не ты создаёшь зависимости, а контейнер. DI — реализация: зависимости внедряются через конструктор/сеттер/поле. Преимущества: слабая связанность, подмена реализаций, тестируемость (можно подставить мок).

  • Какой DI лучше? Constructor injection. Поля можно final, явно видны зависимости, легко тестировать без контекста (new Service(mockRepo)), невозможен циклический DI при старте (сигнал о проблеме в дизайне). С Spring 4.3: если один конструктор — @Autowired не нужен.

  • Жизненный цикл бина. Инстанцирование → DI → Aware-интерфейсы → BPP.postProcessBefore → @PostConstruct → InitializingBean → init-method → BPP.postProcessAfter → ГОТОВ → @PreDestroy → DisposableBean → destroy-method. BeanPostProcessor — точка для создания прокси (@Transactional, @Async).

  • @Transactional — под капотом. Spring создаёт прокси-обёртку. JDK dynamic proxy (если интерфейс) или CGLIB (наследование). Прокси: открывает транзакцию (PlatformTransactionManager), вызывает метод, commit при успехе, rollback при RuntimeException/Error. Checked НЕ откатывают — нужно rollbackFor.

  • self-invocation. this.method() минует прокси → @Transactional/@Async/@Cacheable не работают. Решения: вынести метод в другой бин (рекомендуемый), инжектить self через @Lazy или ObjectProvider, использовать AspectJ compile-time weaving.

  • Propagation. REQUIRED (default) — присоединяется/создаёт. REQUIRES_NEW — всегда новая, приостанавливает текущую (аудит/логирование). NESTED — savepoint внутри текущей. SUPPORTS — присоединяется если есть, нет — без. MANDATORY — требует существующей. NOT_SUPPORTED — приостанавливает. NEVER — если есть → исключение.

  • @SpringBootApplication. @Configuration + @EnableAutoConfiguration + @ComponentScan. Автоконфигурация через @Conditional: если DataSource в classpath — настроит JPA. Список в META- INF/spring/...AutoConfiguration.imports (Spring Boot 3+, раньше spring.factories).

  • Scopes бина. singleton (default — один на контекст), prototype (новый при каждом запросе), request (один на HTTP-запрос), session, application, websocket. Важно: инжект prototype в singleton через Provider или ObjectFactory, иначе prototype-бин создастся один раз.

  • Обработка исключений. @RestControllerAdvice + @ExceptionHandler. ErrorDto с code, message, timestamp. HTTP-статусы: 400 (валидация), 404 (не найдено), 409 (бизнес-конфликт), 500. MethodArgumentNotValidException для @Valid, ConstraintViolationException для @Validated на query-параметрах.

7. Hibernate и Spring Data JPA

  • Что такое ORM? Object-Relational Mapping — маппинг Java-объектов на таблицы. Hibernate — реализация JPA. Позволяет работать с БД через объекты. НО: всегда следи за тем, какой SQL реально выполняется (Hibernate может генерировать неоптимальные запросы — включи hibernate.show_sql).

  • Состояния сущности. Transient — new User(), Hibernate не знает. Persistent/Managed — связан с сессией, dirty checking автоматически синхронизирует изменения. Detached — сессия закрыта/detach. Removed — помечен на удаление. persist: Transient→Managed. merge: Detached→Managed. remove: Managed→Removed.

  • LazyInitializationException. Обращение к lazy-полю после закрытия сессии. Решения (от лучшего к худшему): DTO-проекция (SELECT NEW), JOIN FETCH (@Query), @EntityGraph(attributePaths), @BatchSize(size=100). НЕ решение: OpenSessionInView (анти-паттерн — создаёт N+1 в представлении).

  • N+1 — детально. 1 findAll() + N доп. запросов на lazy-коллекции. 1000 заказов → 1001 SQL. Обнаружение: hibernate.generate_statistics=true, datasource-proxy. Решения: JOIN FETCH, @EntityGraph, @BatchSize(100), @Fetch(FetchMode.SUBSELECT), DTO-проекция. EAGER — неправильный ответ.

  • Оптимистичная блокировка. @Version (int/long/Timestamp). UPDATE ... WHERE version = ?. Если не совпала — OptimisticLockException. Подходит для low-contention (веб-приложения). Пессимистичная: @Lock(PESSIMISTIC_WRITE) → SELECT FOR UPDATE — блокирует строку.

  • Кэши Hibernate. First-level: автоматический, привязан к Session. Гарантирует identity (один PK = один объект). Second-level: опциональный, общий (EhCache, Caffeine, Infinispan). Query cache: кэширует JPQL-результаты. В банковских проектах: first-level всегда, second-level осторожно.

  • Spring Data — как работает? Интерфейс extends JpaRepository. Spring создаёт прокси в рантайме. Имя метода парсится: findByEmailAndStatus → JPQL. @Query для сложных запросов. Pageable → Page/Slice. JpaRepository добавляет flush(), saveAllAndFlush(), deleteAllInBatch() к CrudRepository.

ЛОВУШКА · Про EAGER «Просто поставить EAGER» — неправильный ответ на N+1. EAGER грузит коллекцию ВСЕГДА, даже когда она не нужна — медленнее и съедает память. Правильно: LAZY по умолчанию + FETCH/EntityGraph там, где нужны данные. Золотое правило: все @OneToMany и

@ManyToMany  LAZY.

8. SQL и PostgreSQL

По отзывам кандидатов Альфа-Банка: SQL спрашивают отдельно — пишешь запросы вживую. Готовь JOIN, GROUP BY, HAVING, оконные функции, EXPLAIN ANALYZE.

  • ACID. Atomicity — целиком или никак. Consistency — валидное состояние БД (constraints). Isolation — параллельные транзакции изолированы (зависит от уровня). Durability — после COMMIT данные сохранятся (WAL в PostgreSQL).

  • Уровни изоляции. READ_UNCOMMITTED — dirty read. READ_COMMITTED (default PG) — только закоммиченные. REPEATABLE_READ — повторное чтение = тот же результат. SERIALIZABLE — полная изоляция. Выше уровень → больше консистентности, меньше параллелизма, больше шанс deadlock.

  • Типы индексов PostgreSQL. B-tree (default) — =, <, >, BETWEEN, ORDER BY, LIKE 'abc%'. Hash — только =. GIN — массивы, JSONB, full-text. GiST — геометрия, диапазоны. BRIN — большие таблицы с порядком (timestamp). Покрывающий индекс (INCLUDE) — добавляет колонки в лист, Index Only Scan.

  • Когда индекс НЕ поможет? Маленькие таблицы. Низкая selectivity (boolean). Функции (LOWER(email) — нужен expression index). LIKE '%abc' (wildcard в начале). Часто обновляемые колонки (индекс замедляет INSERT/UPDATE/DELETE).

  • Оконные функции. ROW_NUMBER(), RANK(), DENSE_RANK() — нумерация/ранжирование. LAG/LEAD — предыдущая/следующая строка. SUM/AVG/COUNT OVER (PARTITION BY ... ORDER BY ...) — агрегация без GROUP BY. Пример: зарплата сотрудника vs средняя по его отделу.

  • MVCC. Каждая строка: xmin (создавшая транзакция), xmax (удалившая/обновившая). Читатели не блокируют писателей. Snapshot isolation. Старые версии чистит VACUUM (autovacuum). Проблема: table bloat если VACUUM не успевает.

  • EXPLAIN ANALYZE. EXPLAIN — план без выполнения. EXPLAIN ANALYZE — реальное выполнение + actual time. Seq Scan на большой таблице — плохо. Index Scan / Index Only Scan — хорошо. Rows Removed by Filter — индекс не помогает. estimated ≠ actual rows — нужен ANALYZE.

  • WHERE vs HAVING. WHERE — до группировки, работает с индексами. HAVING — после GROUP BY, для агрегатов. Пример: HAVING COUNT(*) > 5. Ошибка: ставить в HAVING то, что можно в WHERE — медленнее.

  • DELETE vs TRUNCATE. DELETE — построчно, триггеры, WAL, можно WHERE, можно откатить. TRUNCATE — мгновенная очистка, сброс автоинкремента, не пишет каждую строку в WAL.

9. Микросервисы и Kafka

Микросервисная архитектура — основа стека Альфы. Kafka — главный брокер. Готовься объяснять не только «что это», но и «что делать, когда что-то пошло не так».

  • Что такое микросервис? Автономный сервис с одной зоной ответственности, собственной БД, независимым деплоем. Общается через REST/gRPC (синхронно) или брокер (асинхронно). Плюсы: независимый деплой, масштабирование, технологическая гибкость. Минусы: распределённые транзакции, сетевая латентность, сложность отладки.

  • Kafka — зачем? Распределённый лог-ориентированный брокер. Данные хранятся на диске (retention), не удаляются после прочтения. Topic → Partition → Offset. Partition — единица параллелизма. Зачем: асинхронное взаимодействие, буферизация нагрузки, event sourcing, аудит-лог.

  • Consumer Group. Каждая партиция — ровно один consumer из группы. Больше партиций → больше параллелизма. Если consumers > partitions — лишние простаивают. Rebalancing при добавлении/падении consumer. Offset commit: auto (рискованно) или manual (контролируемо).

  • Гарантии доставки. at-most-once — может потеряться. at-least-once — может продублироваться (default). exactly-once — idempotent producer + transactional + read_committed. В проде: at-least-once + идемпотентная обработка (upsert в БД, дедупликация по event_id).

  • Circuit Breaker. Защита от каскадных сбоев. CLOSED → OPEN (блокирует) → HALF_OPEN (тестовые запросы). Resilience4j + Spring Boot стартер. Конфигурация: failureRateThreshold, waitDurationInOpenState. Fallback: дефолтное значение или кэш.

  • Saga. Распределённые транзакции через цепочку локальных + компенсирующие действия. Choreography (события, децентрализованно) vs Orchestration (центральный оркестратор). Пример: оплата → списание → бронирование. Бронирование упало → возврат → отмена.

  • Idempotent consumer. Таблица processed_events(event_id UUID PK). INSERT перед обработкой — конфликт → skip. Или upsert по бизнес-ключу (ON CONFLICT DO UPDATE). В транзакции с бизнес-логикой.

10. Docker, Kubernetes, CI/CD

  • Образ vs контейнер. Образ — неизменяемый шаблон из слоёв (каждая инструкция Dockerfile — слой). Контейнер — запущенный экземпляр с writable-слоем. docker images → образы, docker ps → контейнеры.

  • Multi-stage build. FROM maven:3.9 → сборка. FROM eclipse-temurin:21-jre → копируем только JAR. Финальный образ ~200 MB вместо ~800 MB. Безопаснее (меньше attack surface), быстрее деплой.

  • Kubernetes: Pod, Deployment, Service. Pod — 1+ контейнер, общий network. Deployment — управляет ReplicaSet: реплики, rolling update, rollback. Service — стабильный IP/DNS: ClusterIP (внутренний), NodePort, LoadBalancer. Ingress — L7 маршрутизация.

  • Liveness vs Readiness vs Startup probe. Liveness — жив ли? Нет → перезапуск. Readiness — готов к трафику? Нет → убирается из LB. Startup — для медленного старта: пока не пройдёт, остальные не проверяются. Spring Boot Actuator: /actuator/health/liveness, /actuator/health/readiness.

  • CI vs CD. CI — автосборка+тесты при push. CD — Delivery (ручной деплой) или Deployment (автодеплой). Pipeline: checkout → build → test → docker build → push registry → deploy staging → (approval) → prod.

11. Тесты: JUnit, Mockito, TDD

В вакансиях Альфы тесты — обязательное требование: JUnit 5, Testcontainers, WireMock. Могут попросить написать тест прямо на собесе.

  • Пирамида тестирования. Unit (много, быстро) → интеграционные (связь компонентов) → e2e (мало, хрупко). В банках: обязательно unit + integration. E2e — на критичных сценариях.

  • mock vs spy. mock() — пустой объект, всё default. spy() — обёртка над реальным: методы вызываются, если не замокать. Для spy: doReturn().when(spy).method() (иначе вызовется реальный метод).

  • ArgumentCaptor. Захват аргумента: verify(repo).save(captor.capture()); assertThat(captor.getValue().getName()).isEqualTo("Alice"). Когда нужно проверить не факт вызова, а конкретные значения.

  • Testcontainers. Реальные БД/Kafka в Docker из тестов. @Testcontainers + @Container PostgreSQLContainer. Зачем: тесты на реальной PostgreSQL, а не H2 (которая отличается). Проверка миграций Flyway, SQL, Kafka-consumers.

  • TDD. Red: падающий тест. Green: минимальный код. Refactor: улучшаем без изменения поведения. Private-методы не тестируем — через публичные. Если сложно — выдели класс.

  • WireMock. Мок HTTP-сервисов для интеграционных тестов. WireMockServer или @WireMockTest. stubFor(get("/api/...").willReturn(okJson("..."))). Позволяет тестировать взаимодействие с внешними API без реального вызова.

12. Практические задачи (live-coding)

По отзывам кандидатов: задачи прикладные, без LeetCode Hard. REST-контроллер, unit-тест, code review, Stream API, SQL. Рассуждай вслух!

Задача 1. Найти дубликаты

Дан List. Вернуть Set чисел, встречающихся более одного раза.

public Set<Integer> findDuplicates(List<Integer> list) {
    Set<Integer> seen = new HashSet<>();
    Set<Integer> duplicates = new HashSet<>();
    for (Integer n : list) {
        if (!seen.add(n)) {
            duplicates.add(n);
        }
    }
    return duplicates;
}

O(n) время, O(n) память. seen.add() возвращает false, если элемент уже есть — элегантная проверка. Альтернатива через Stream: Collectors.groupingBy + counting() + filter > 1.

Задача 2. Группировка Stream API

// Отдел → список сотрудников Map> byDept = employees.stream()

.collect(Collectors.groupingBy(Employee::getDepartment));

// Отдел → количество Map counts = employees.stream() .collect(Collectors.groupingBy(

Employee::getDepartment, Collectors.counting()));

// Отдел → средняя зарплата Map avgSalary = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment,

Collectors.averagingDouble(Employee::getSalary)));

Задача 3. SQL вживую

Отделы со средней зарплатой > 100 000:

SELECT d.name, AVG(e.salary) AS avg_salary
FROM employees e
JOIN departments d ON d.id = e.department_id
GROUP BY d.name
HAVING AVG(e.salary) > 100000
ORDER BY avg_salary DESC;

Индексы: employees.department_id, покрывающий (department_id, salary). Продолжат: сотрудники с зарплатой выше средней по СВОЕМУ отделу → AVG() OVER (PARTITION BY department_id).

Задача 4. SQL из собесов Альфы

Покупатели, купившие Laptop И Monitor в марте 2024:

SELECT c.name
FROM customer c
JOIN purchase p ON c.customer_key = p.customer_key
JOIN product pr ON p.product_key = pr.product_key
WHERE pr.name IN ('Laptop', 'Monitor')
  AND EXTRACT(MONTH FROM p.date) = 3
  AND EXTRACT(YEAR FROM p.date) = 2024
GROUP BY c.customer_key, c.name
HAVING COUNT(DISTINCT pr.name) = 2;

Ключ: HAVING COUNT(DISTINCT pr.name) = 2 — покупатель купил ОБА товара. Без DISTINCT — дубли покупок сломают подсчёт.

Задача 5. REST-контроллер

@RestController
@RequestMapping("/api/users")
public class UserController {

     private final UserService userService;

     public UserController(UserService userService) {
         this.userService = userService;
     }

     @PostMapping
     @ResponseStatus(HttpStatus.CREATED)
     public UserDto create(@RequestBody @Valid CreateUserRequest req) {
         return userService.create(req);
     }

     @GetMapping("/{id}")
     public UserDto getById(@PathVariable Long id) {
         return userService.findById(id);
     }
}

public record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(18) int age
) {}

Constructor injection, DTO (не Entity), @Valid, 201 Created для POST, record (Java 16+). Обработка MethodArgumentNotValidException через @RestControllerAdvice.

Задача 6. Unit-тест Mockito + AssertJ

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock private UserRepository userRepository;
    @Mock private EmailSender emailSender;
    @InjectMocks private UserService userService;

     @Test
     void shouldRegisterUserAndSendEmail() {
         // given
         User saved = new User(1L, "Alice", "alice@example.com");
         when(userRepository.save(any(User.class))).thenReturn(saved);
         // when
         User result = userService.register("alice@example.com", "Alice");
         // then
         assertThat(result.getId()).isEqualTo(1L);
         ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
         verify(userRepository).save(captor.capture());
         assertThat(captor.getValue().getName()).isEqualTo("Alice");
         verify(emailSender).sendWelcome("alice@example.com");
    }
}

Задача 7. Потокобезопасный кэш

public class SimpleCache {
    private final Map<String, String> cache = new ConcurrentHashMap<>();

    public String get(String key, Function<String, String> loader) {
        return cache.computeIfAbsent(key, loader);
    }
}

ConcurrentHashMap (не HashMap — бесконечный цикл в Java 7, потеря данных). computeIfAbsent (не containsKey+put — гонка). Ограничение размера: Caffeine/Guava Cache с maxSize и TTL.

Задача 8. Code Review

public class UserCache {
    private static Map<Long, User> cache = new HashMap<>();

    public static User getUser(Long id) {
        if (cache.containsKey(id)) {
            return cache.get(id);
        }
        User user = loadFromDb(id);
        cache.put(id, user);
        return user;
    }
}

Проблемы: 1) HashMap не потокобезопасен → ConcurrentHashMap. 2) containsKey+get → computeIfAbsent. 3) Кэш бесконечный → memory leak → нужен TTL/maxSize. 4) static глобальное состояние → Spring-бин. 5) null от loadFromDb не обработан.

Задача 9. N+1 — найти и починить

@Entity Order с @OneToMany(mappedBy="order") List<OrderItem> items (LAZY). В сервисе: findAll() +
цикл по items.size(). Решения: JOIN FETCH в @Query, @EntityGraph(attributePaths="items"),
@BatchSize(size=100), DTO-проекция. «Просто поставить EAGER»  неправильный ответ.

13. План подготовки + чек-лист

За 2–3 недели

  • 20–30 задач LeetCode (Easy + Medium): массивы, строки, HashMap, Stream API

  • Pet-проект: Spring Boot 3 + PostgreSQL + Kafka + Testcontainers

  • Hibernate: N+1, состояния сущности, JOIN FETCH, @EntityGraph, @Version

  • SQL: JOIN, GROUP BY, HAVING, оконные функции, EXPLAIN ANALYZE

  • 5–10 unit-тестов с Mockito + AssertJ по TDD

  • Docker: Dockerfile multi-stage, docker-compose с БД + Kafka

За неделю

  • 2–3 мок-интервью: pramp.com, interviewing.io или друзья

  • Прорешать все вопросы из гайда ВСЛУХ — мысли ≠ слова

  • 2–3 истории: проблема → что сделал → результат (с цифрами)

  • Решить задачи «что выведет» на Integer cache, Stream API, equals

  • Перечитать вакансию Альфы, выписать незнакомые слова

В день собеса

  • Камера, микрофон, интернет — за 30 минут

  • IDE наготове (IntelliJ IDEA), терминал с docker-compose

  • Рассуждать вслух — молчание хуже «дай подумать»

  • Не знаешь — честно: «не сталкивался, но предположу...»

  • 2–3 вопроса в конце: проект, команда, code review, стек

ВНИМАНИЕ · Про банковские проекты Альфа-Банк проводит проверку СБ — стандарт для банков. Удалёнка возможна (полная или гибрид). IT-хабы: Москва, Питер, Екатеринбург. Сезонный коворкинг в Сочи.

Финальный чек-лист

Блок Готов, если можешь...
Java Core equals/hashCode на примере + как нарушение

ломает HashSet

Коллекции HashMap в Java 8+: бакеты, treeify, resize, mutable-ключ

Многопоточность Потокобезопасный singleton + volatile в DCL + happens-before

Spring @Transactional под капотом + self-invocation + Propagation

Hibernate N+1: найти, объяснить, 2+ способа починить

SQL JOIN+GROUP BY+HAVING + оконная функция + обосновать индекс

Kafka Consumer Group, гарантии, идемпотентность
Docker/K8s Dockerfile multi-stage + Pod/Deployment/Service +

probes

Тесты Mockito + ArgumentCaptor + AssertJ (given-when-then)

Live-coding 15 мин чистое решение + сложность + edge cases


Удачи на собесе!

// git push origin offer

Что дальше

  • Повторить: Spring Boot, @Transactional, Kafka, PostgreSQL, Docker/Kubernetes, live-coding.
  • Спросить в канале: свежие вопросы по Альфа-Банку, формат live-coding, кейсы по Kafka и SQL.
  • Получать новые разборы: @java_jub.
  • Проверить знания: тесты JavaJub.

← Ко всем гайдам