1273
Как успешно пройти backend собеседование и получить лучший оффер
Как мы превращаем "понимаю" в "умею"
На собесах чаще всего сыпятся не те, кто не учил, а те, кто всё понял, но ни разу не применил на практике.
Можно часами смотреть видео и кивать в такт. Можно читать статьи и думать: "Ну да, логично". Но потом на практике – ступор.
Потому что между "знаю" и "умею" лежит одно – действие.
На менторстве мы не зубрим. Мы закрепляем знания через код, задачи и мини-проекты.
Лайвкодинг вместо заучивания
Многие темы мы изучаем через совместный кодинг. Смотрим, что под капотом, как поведёт себя код, что сломается, почему поведение не такое, как ожидали.
Вместо "volatile обеспечивает видимость" – пишем без volatile, нагружаем и видим расхождение.
Вместо "ConcurrentHashMap потокобезопасна" – проверяем, где она реально спасает, а где нет.
Вместо "Transactional откатывает изменения" – воспроизводим случай, где rollback не сработал.
Когда видишь поведение своими глазами, знание перестаёт быть теорией.
Мини-проекты вместо сухих задач
Мы не учим темы изолированно. Многие блоки превращаем в маленький, но живой проект.
Потоки – пишем собственный ThreadPool.
Коллекции – реализуем LRUCache с ограничением по размеру.
Транзакции – делаем мини-сервис, где нужно гарантировать целостность данных.
Spring – собираем упрощённый DI-контейнер, чтобы понять, что делает Spring за нас.
Это практические модели реальных систем, где теория проявляется в действии.
Создаём ситуации, где сломается
Самые полезные задачи те, где что-то идёт не по плану. Мы специально даём разработчикам код, который работает почти правильно, но:
– Поток затирает данные
– HashMap теряет элемент
– Транзакция не откатывается
И вместе разбираем почему.
Так формируется настоящая глубина: уверенность не потому, что выучил, а потому, что пережил на практике.
Диалог вместо экзамена
Мы не оцениваем "правильно или нет". Мы обсуждаем, почему именно так.
– Почему ты выбрал synchronized, а не lock
– Как поведёт себя код при 10 потоках
Каждый такой разговор – это развитие инженерного мышления. Разработчик перестаёт просто писать код – он начинает обосновывать свои решения.
Задачи без очевидных ответов
В реальной разработке нет единственного верного решения – есть компромиссы.
Мы разбираем такие задачи, где нужно выбрать подход, аргументировать и защитить:
– Как реализовать кэш для миллионов ключей
– Как спроектировать очередь задач с retry и timeout
– Как гарантировать целостность данных без транзакций
Объяснение = финальная проверка
Каждую тему мы закрываем простым принципом: "Если можешь объяснить своими словами – значит, понял".
Поэтому разработчик не просто сдаёт код. Он рассказывает:
– Как работает
– Почему сделал именно так
– Что бы изменил, если бы нагрузка выросла в 10 раз
Это момент, где из знания рождается экспертиза.
Мы учим через опыт
Мы не даём "задания на оценку". Мы создаём пространство, где можно ошибаться.
Ошибся? Отлично.
Поймал баг? Супер.
Сделал дедлок? Прелестно.
Потому что реальный прогресс начинается не с идеальных ответов, а с честного "не знаю, но хочу понять".
Как выстроить систему подготовки, чтобы не выгореть
Мы начали готовиться. Собрали темы, нашли материалы, даже поставили цели.
Первые дни всё идёт хорошо. Мозг впитывает, темы ложатся одна за другой. А потом наступает момент, когда знания вроде растут, а внутри – усталость, раздражение, апатия.
Кажется, что мы идём вперёд, но не чувствуем прогресса. Каждый день похож на предыдущий. И мы начинаем терять энергию.
Почему мы выгораем не в конце, а в середине
В начале нас держит мотивация.
В конце – цель (оффер, повышение).
А посередине – пустота.
Это та самая яма, где мы учим, но не чувствуем результата. Где знаний уже много, но уверенности всё ещё нет. Где каждое новое "понял" тут же сменяется "ничего не помню".
И вот здесь система становится критична. Потому что без неё мы просто тонем в информационном потоке.
Как построить систему, которая держит темп
1. Строим ритм, а не план
План – это список дел.
Ритм – это темп, в котором мы можем учиться.
У каждого свой темп. Кто-то может по 3 часа в день. Кто-то – по 30 минут.
Главное – не сжечь топливо за первую неделю. Лучше идти каждый день по чуть-чуть, чем рваться и срываться.
2. Чередуем типы нагрузки
Подготовка – не только теория.
Есть разные типы умственной деятельности:
– аналитическая, когда мы читаем и разбираемся в механизмах
– практическая, когда пишем код
– рефлексивная, когда повторяем, рассказываем, объясняем
Если день тяжёлый – можно просто пересмотреть конспект или объяснить тему коллеге. Это тоже часть подготовки.
Мы не обязаны быть на 100% продуктивными каждый день. Важно – оставаться в движении.
3. Позволяем отдыхать
Наш мозг не может постоянно быть в режиме "учись – запоминай – отвечай".
Нужны точки выдоха:
– прогулки после учёбы
– смена контекста
– ленивые дни
Самое опасное – не усталость, а чувство вины за усталость. Как только мы начинаем себя грызть – мотивация исчезает.
4. Даём обратную связь
Мы выгораем, когда не видим результата. Поэтому нужен индикатор прогресса.
Простая привычка – каждый вечер записывать одно из трёх:
– сегодня понял, зачем нужен X
– сегодня объяснил Y вслух
– сегодня закрепил тему Z
Через неделю перечитываешь – и видишь, что путь есть. А мозг любит доказательства: я двигаюсь.
5. Меняем формат, когда застряли
Если тема не идёт – не значит, что мы тупые. Значит, формат не подходит.
Например:
– не идёт книга: найди видео
– не заходит лекция: попробуй руками закодить
– устал от практики: открой статью с иллюстрациями
Учёба – не конвейер. Иногда, чтобы двигаться, нужно просто поменять вектор.
6. Добавляем элемент удовольствия
Готовиться можно не только из под палки.
Добавь в процесс удовольствие:
– пиши заметки в блог
– делись инсайтами с коллегами
– собирай личную базу знаний, которую потом покажешь ученикам
Когда появляется смысл – усталость перестаёт быть врагом.
Главный вывод
Выгорание – это сигнал, что мы пытаемся жить в чужом ритме. Когда мы строим путь под себя – всё вдруг становится легче. Мы просто планомерно двигаемся вперёд.
❤️ и обсудим как учить глубоко, а не поверхностно.
Почему документацию никто не пишет, но все хотят её читать
У каждой команды есть такая мечта:
Вот бы была нормальная документация…
– Напиши доку сам?
– Да ну нафиг, потом
0xCAFEBABE
Вот кто не любит пасхалки?
Давайте создадим простой класс:
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}javac Main.java
xxd Main.class
00000000: cafe babe 0000 003d 001d 0a00 0200 0307 .......=........
00000010: 0004 0c00 0500 0601 0010 6a61 7661 2f6c ..........java/l
00000020: 616e 672f 4f62 6a65 6374 0100 063c 696e ang/Object...<in
00000030: 6974 3e01 0003 2829 5609 0008 0009 0700 it>...()V.......
00000040: 0a0c 000b 000c 0100 106a 6176 612f 6c61 .........java/la
00000050: 6e67 2f53 7973 7465 6d01 0003 6f75 7401 ng/System...out.
00000060: 0015 4c6a 6176 612f 696f 2f50 7269 6e74 ..Ljava/io/Print
00000070: 5374 7265 616d 3b08 000e 0100 0b48 656c Stream;......Hel
00000080: 6c6f 2057 6f72 6c64 0a00 1000 1107 0012 lo World........
00000090: 0c00 1300 1401 0013 6a61 7661 2f69 6f2f ........java/io/
000000a0: 5072 696e 7453 7472 6561 6d01 0007 7072 PrintStream...pr
000000b0: 696e 746c 6e01 0015 284c 6a61 7661 2f6c intln...(Ljava/l
000000c0: 616e 672f 5374 7269 6e67 3b29 5607 0016 ang/String;)V...
000000d0: 0100 044d 6169 6e01 0004 436f 6465 0100 ...Main...Code..
000000e0: 0f4c 696e 654e 756d 6265 7254 6162 6c65 .LineNumberTable
000000f0: 0100 046d 6169 6e01 0016 285b 4c6a 6176 ...main...([Ljav
00000100: 612f 6c61 6e67 2f53 7472 696e 673b 2956 a/lang/String;)V
00000110: 0100 0a53 6f75 7263 6546 696c 6501 0009 ...SourceFile...
00000120: 4d61 696e 2e6a 6176 6100 2100 1500 0200 Main.java.!.....
00000130: 0000 0000 0200 0100 0500 0600 0100 1700 ................
00000140: 0000 1d00 0100 0100 0000 052a b700 01b1 ...........*....
00000150: 0000 0001 0018 0000 0006 0001 0000 0001 ................
00000160: 0009 0019 001a 0001 0017 0000 0025 0002 .............%..
00000170: 0001 0000 0009 b200 0712 0db6 000f b100 ................
00000180: 0000 0100 1800 0000 0a00 0200 0000 0300 ................
00000190: 0800 0400 0100 1b00 0000 0200 1c .............
cafe babe.cafe babe.java.lang.ClassFormatError: Incompatible magic value ...
Мы раньше ходили обедать в одно место под названием St Michael's Alley. По местной легенде, в далёком прошлом там выступали Grateful Dead, ещё до того, как стали знаменитыми. Это было довольно странное, но атмосферное заведение – определённо «место в стиле Grateful Dead». Когда Джерри (Гарсия) умер, там даже сделали что-то вроде маленького буддийского алтаря.
Когда мы туда ходили, мы называли это место Cafe Dead. В какой-то момент кто-то заметил, что это ещё и HEX-число. Я как раз перерабатывал код для одного файлового формата и мне нужны были парочка «магических чисел»: одно – для формата файлов с объектами, другое – для классов.
Я использовал CAFEDEAD для объектного формата. А потом, просматривая четырёхсимвольные HEX-слова, которые подходят после CAFE (тема показалась удачной), я наткнулся на BABE и решил использовать его.
Тогда это не казалось чем-то особенно важным или судьбоносным – скорее временным решением, которому место на свалке истории. Но в итоге CAFEBABE стало «магическим числом» для class-файлов, а CAFEDEAD – для объектного формата. Правда, объектный формат со временем исчез, и вместе с ним ушло использование CAFEDEAD – его в итоге заменил RMI.
На удивление, многие не знали про существование LongAdder.
Почему же он такой быстрый на запись, но такой медленный на чтение?
В основе LongAdder лежит Striped64.
Зачем он вообще нужен?
Когда мы используем AtomicLong, то под высокой нагрузкой все потоки дерутся за один атомарный long.
Striped64 делит счётчик на ячейки, каждая из которых может инкрементироваться независимо.
Каждый поток работает в своей ячейке, снижая конкуренцию.
При чтении, значения суммируются из всех ячеек и базового значения. Поэтому чтение такое медленное. В конкретный момент времени неизвестно текущее значение счётчика – его нужно вычислить путём суммирования.
Внутреннее устройство
volatile long base – основное значение (используется, если конкуренции нет).
volatile Cell[] cells – массив ячеек, каждая хранит long value. Каждая ячейка расширена так, чтобы не было false sharing (об этом позже).
probe (через ThreadLocalRandom) определяет, в какую ячейку будет писать поток.
cellsBusy – CAS-замок на инициализацию/расширение массива cells.
Алгоритм обновления
Попробовали сделать CAS на base. Если получилось – готово.
Если CAS на base не удался:
– Берём cells. Если они ещё не созданы – пробуем создать массив на 2 ячейки.
– Вычисляем индекс ячейки. Пробуем сделать CAS в ячейку cells[index].
Если CAS в ячейку не удался (потоки попали в одну и ту же ячейку):
– Меняем probe (перемешиваем хэш потока).
– При сильной конкуренции пробуем расширить массив ячеек в 2 раза.
Размер cells может расти до числа ядер процессора. Так достигается баланс: чем больше конкуренция, тем больше ячеек.
false sharing
У процессоров память кешируется не побайтно и не по переменной, а блоками фиксированного размера – cache line (обычно 64 байта).
Когда поток читает или пишет переменную, процессор подгружает в кеш всю линию (64 байта).
Если два разных потока работают с разными переменными, но эти переменные попали в одну кеш-линию, то процессор начинает синхронизировать линию между ядрами при каждой записи.
В итоге кеш постоянно инвалидируется, даже если логически данные не связаны.
Это и есть false sharing – ложное совместное использование данных, которых на самом деле не должно быть.
В Striped64 разные потоки пишут в разные ячейки.
Если бы ячейка была просто volatile long value, то несколько ячеек массива подряд оказались бы в одной кеш-линии.
Поэтому Striped64 использует аннотацию Contended, которая дополняет поля внутри классов пустыми байтами.
То есть каждая ячейка раздута, чтобы точно не пересекаться с соседями по кеш-линии. Это убирает false sharing и позволяет потокам работать независимо.
Делаем свой ThreadPool
Создавать новый поток под каждую задачу – всё равно что покупать новый ноутбук каждый раз, когда нам нужно открыть вкладку в браузере.
Дорого, долго и бессмысленно.
Поэтому существует пул потоков.
Он состоит из четырёх ключевых компонентов:
1. Очередь задач: в неё кладутся новые задачи от пользователей, а потоки берут их в работу.
2. Потоки: каждый поток в пуле работает в бесконечном цикле. Берёт задачу из очереди -> выполняет -> берёт следующую. Если задач нет, поток ждёт.
3. Механизм управления потоками: нужно решать, сколько потоков поддерживать - фиксированное количество или динамически расширяемое.
4. Политика отказа: принимаемые действия, когда пул не может принять новую задачу.
Зачем вообще делать свой пул, когда есть готовые решения? Ответ простой. Чтобы разобраться как он работает под капотом.
Полную версию с кодом, объяснениями, стратегиями остановки и скейлинга выложил здесь
hashCode() по умолчанию
Мы привыкли думать, что если не переопределяем hashCode(), то по умолчанию он равен адресу объекта в памяти.
Но так ли это?
Может ли hashCode быть равен рандомному числу или вообще единице?
В этой статье провел расследование.
Как я учился говорить "нет" менеджерам и не чувствовать себя мудаком
Менеджер пишет:
Там мелкая доработка, надо просто поле добавить. Сможешь сегодня?
Срочно надо пофиксить расчёт формулы, иначе отчёты не сходятся
Что происходит, когда мы пишем new в Java: от байткода до оптимизаций
Кажется, мы просто создаём объект:
var user = new User();
var user = new User() превращается во что-то вроде:0: new #2
3: dup
4: invokespecial #1
7: astore_1
new – резервирует место в heap, но не вызывает конструктор. dup – после new ссылка на объект оказывается на вершине стека. dup нужен, чтобы использовать её дважды: и для вызова конструктора, и для присваивания переменной. invokespecial – вызывает <init>-метод (конструктор). astore_1 – сохраняет ссылку в переменную user.
Как учиться, когда у тебя работа, семья, дела и вообще ни на что не хватает времени
Учиться легко, когда тебе 18, ты живешь в общаге, ешь пельмени и всё свободное время твое.
Но если тебе за 25-30, ты работаешь full-time, у тебя семья, дети, ипотека, собака, кошка, больная спина и прод упал – то любая мысль о том, чтобы ещё учиться, вызывает злость, вину и выгорание.
Я устал.
У меня нет времени.
У меня нет сил.
Но я хочу лучшую работу.
Я слабак. У других же получается.
Наверное, не так уж и хочу.
Надо просто собраться и начать.
Практикум по Java собеседованиям
Весной я решил попробовать новый для себя формат подготовки разработчиков к собеседованиям.
Вместо индивидуального менторства – встречи в небольшой группе.
Я опасался, что это будет не так эффективно, как личная работа 1 на 1.
Но по завершении группового практикума, я могу с уверенность сказать, что это не так.
Нас было 7 человек, включая меня.
Мы встречались 2 раза в неделю по вечерам на протяжении месяца и обсуждали разные вопросы: от внутрянки jvm до системного дизайна и оформления резюме.
Трое уже нашли новую работу с повышением должности и дохода. Двое получили контр-офферы на текущей работе и приняли их. Один в процессе прохождения собесов. Я уверен и у него все получится.
Главный плюс, который я вижу в групповой работе – это комьюнити, которое тебя подталкивает.
Пример.
Чтобы более менее уверенно чувствовать себя на алгоритмических секциях, нужно самостоятельно прорешать не один десяток задач.
Когда ты один – трудно заставить себя тратить на это время.
Ментор тебя тоже не заставит. Он же не будет бить тебя палкой.
А когда ты в группе – возникает азарт.
Петя решил 30 задач, Ваня решил 40 задач, а я решил всего лишь одну – непорядок, я что ленивее других?
Более того, все разбились на пары, самостоятельно встречались и вместе решали задачки. В том числе и по системному дизайну.
Плюс коллективный разум. Кто-то может задать такой вопрос, о которым ты даже не подозревал. Это очень сильно расширяет кругозор.
Поэтому я считаю, что групповая работа ни чуть не уступает по эффективности личному менторству.
Кроме того, она намного выгоднее по деньгам.
Менторство предполагает пред-оплату 10/20/30 тысяч и пост-оплату в процентах от полученного оффера – 50/100/150% в зависимости от уровня человека.
В группу же можно залететь за несколько тысяч рублей в месяц при использовании рассрочки.
Суммы отличаются в разы.
Что в итоге?
Я решил продолжить развивать формат групповой подготовки к собеседованиям:
– Переработал программу
– Увеличил количество групповых встреч до 10
– Включил проверку домашнего задания
– Добавил одну личную встречу в конце практикума для большей уверенности, что человек усвоил весь материал и готов выйти на рынок
Какие темы изучаем?
– Java (от core до virtual threads)
– Spring (от бинов до hibernate)
– SQL/NoSQL (от индексов до explain)
– Kafka (от устройства до гарантий доставки)
– Docker, Kubernetes
– Задачи на код-ревью и алгоритмы
– Паттерны проектирования
– Системный дизайн
– Soft skills (от оформления резюме до рассказа о себе)
Короче, суть в том, чтобы ты:
– Закрыл все вопросы по собеседованиям
– Чувствовал себя уверенно в любых ситуациях
– Нашел новую интересную работу, на которой сможешь расти
– Увеличил свой доход
Если тебе интересен такой формат подготовки, пиши мне в личку @principal_swe
Созвонимся, обсудим все вопросы и решим подойдет ли это для тебя.
Стартуем в начале августа.
Так как это небольшая группа, то, конечно, количество мест ограничено.
Поэтому выбор – действовать сейчас или отложить – только за тобой.
Я уверен это отличный буст для любого разработчика.
Я шёл на собеседование, а попал на допрос
Вот какую цель ставит перед собой любой адекватный интервьюер, проводя собеседование?
Правильно. Найти человека к себе в команду.
Должен ли этот человек знать 100500 фреймворков? Нет. Любой технологии можно обучить.
Должен ли этот человек молниеносно и оптимизировано решать алгоритмические задачи? Нет. Любой алгоритм требует неспешных размышлений.
Должен ли этот человек вызубрить методы класса Object? Нет. Он не энциклопедия.
Что же должен этот человек?
Да ничего никому он не должен.
Как говорят психологи, долженствование – это когнитивное искажение.
Кстати, почитайте про когнитивные искажения. Очень познавательно.
Так вот. Если тебе комфортно общаться с этим человеком, если ты видишь его рост, если ты видишь его стремления, то этого достаточно, чтобы работать с ним.
Соответственно и вопросы интервьюера должны быть направлены на это.
На опыт человека, на то как он принимает решения, как подходит к компромиссам, к решению проблем, какую ответственность он готов брать.
Неадекватный интервьюер же пришел на собеседование не для того, чтобы найти коллегу себе в команду, а чтобы самоутвердиться.
Он не спрашивает, он соревнуется.
Он не слушает, он выискивает момент, когда ты ошибешься.
Он не хочет понять, он хочет почувствовать себя крутым.
Отсюда и вопросы «А если я сделаю вот такую дичь, где будет NPE?».
Это не собеседование.
Это допрос с элементами показательного унижения.
А после – его ехидная улыбка.
Каждый день мы разгребаем дерьмо:
– запутанное легаси
– ошибки прома
– тонны фич без полной аналитики
– отсутствие архитектуры и нормального техдолга
– бесконечные митинги
И ты ещё приходишь на собеседование, чтобы расти, менять проект, доход – а тебе: «Что делает GC, если у тебя в finalize вылетело исключение?».
Серьёзно? Это то, по чему ты судишь про мой опыт?
Если ты когда-нибудь выходил с собеса с мыслью: «Я вроде сеньор… но чувствую себя идиотом» – это не ты глупый. Это интервьюер сделал глупость – не смог раскрыть тебя. А иногда – и не хотел.
В предыдущем посте вы ставили 🔥 чтобы узнать как застать интервьюера врасплох на вопросах про прокси.
Адекватного интервьюера вам и не захочется заставать врасплох. Он не даст причин.
А на неадекватного – не тратьте свои силы, время и нервы. Он того не стоит. Поблагодарите судьбу за то, что работать вы с ним не будете.
❤️ если согласен
Что будет с твоим кодом через 250 лет?
На выходных с девушкой посетил Екатерининский дворец в Царском Селе.
Во-первых, это безумно красиво!
Благодаря колоннам и пальмам складывается ощущение будто ты в Древней Греции.
Во-вторых, это архитектурная медитация.
Ты смотришь на детали: колонны, золото, лепнину, геометрию.
Каждый зал – как модуль.
Каждая анфилада – как правильно выстроенный маршрут пользователя.
И в какой-то момент ловишь себя на мысли:
Чёрт, так ведь и с системным дизайном то же самое!
Карточки про гарантии доставки Apache Kafka
При работе с сообщениями могут возникать различные проблемы как на стороне producer, так и на стороне consumer.
Часть проблем решается настройками на уровне Kafka, а часть - использованием определенных паттернов проектирования.
Рассмотрим:
– At most once
– At least once
– Exactly once
– Transactional Outbox
– Идемпотентный Consumer
Как учить глубоко, а не поверхностно
Мы все хоть раз попадали в эту ловушку.
Учишься неделями, читаешь статьи, смотришь курсы – а потом на собесе спрашивают:
А почему именно так?
Гарантирует видимость между потоками
Как не утонуть в подготовке к собесам
Мы садимся готовиться.
Открываем список тем: Java Core, Collections, Concurrency, Spring, SQL, System Design, Live Coding, HR-вопросы…
И кажется, что чтобы пройти, нужно выучить всё.
В первый день – энтузиазм.
Во второй – усталость.
На третий – паника.
Кажется, знаний слишком много, а времени слишком мало.
И все вокруг будто умнее.
Кто-то уже всё прошёл, всё повторил, всё знает, а мы третий день пытаемся вспомнить разницу между ACID и BASE.
Почему мы тонем
Потому что часто готовимся по-учебному: берём список тем и начинаем учить всё подряд.
Но собеседование – это не экзамен.
Это проверка того, как мы думаем. Как мы связываем концепции, рассуждаем, понимаем причины, а не просто факты.
А тонем мы в основном потому, что:
– хватаемся за всё сразу
– читаем, но не закрепляем
– забываем повторять
– и сравниваем себя с другими, вместо того, чтобы видеть свой прогресс
Как выбраться
Я даю ученикам следующий подход, который помогает держаться на плаву.
1. Определяем цель
Мы не можем готовиться "вообще".
Если мы идём на live coding – одно дело. Если на system design – другое.
Разный уровень глубины, разные акценты.
Главное – понимать, под какую цель мы выстраиваем путь.
2. Делим на блоки
Java Core -> Collections -> Concurrency -> Spring -> SQL -> System Design.
Закрываем блок – чувствуем прогресс.
Это даёт мотивацию и убирает ощущение хаоса.
3. Учимся понимать, а не запоминать
Не "что делает volatile", а "почему без него не работает".
Не "как написать Lock", а "какие проблемы он решает".
Когда мы понимаем зачем – знание остаётся.
4. Связываем темы
Например: JMM -> happens-before -> volatile -> ConcurrentHashMap.
Так мозг видит систему, а не кучу разрозненных фактов.
5. Закрепляем практикой
Реализуем свой ThreadPool, мини-Spring, LRU Cache.
Когда мы руками повторяем механику – она становится частью мышления.
6. Повторяем
Мозг забывает быстро.
Поэтому каждые выходные – день повторения: пролистали конспекты, рассказали темы вслух, закрыли пробелы.
7. Не забываем про баланс
Если мы грузим себя по 5 часов в день – выгорим за неделю.
Лучше по часу, но стабильно. И обязательно отдых – мозгу нужно время, чтобы переварить информацию.
8. Не ругаем себя
Даже если забыли, даже если не успели.
Это часть процесса.
Главное – понимать, как рассуждать, когда не знаем ответ.
Главный вывод
Подготовка – это не зубрёжка. Это сборка системы знаний.
Когда у нас складывается картина, мы перестаём бояться неожиданных вопросов – потому что можем рассуждать, а не вспоминать.
🔥 и разберём как выстроить систему подготовки, чтобы не выгореть.
Собеседование – штука стрессовая.
Реальность может подкинуть сюрпризы даже опытному разработчику:
– вопросы, которые в теории знаешь, но формулируешь сумбурно
– live coding, где залипаешь на синтаксисе и теряешь ход мысли
– code review, где кажется, что у тебя слишком мягкий взгляд на чужой код
– системный дизайн, где голова идёт кругом с чего начать, что рисовать, на что обращают внимание
Я через это проходил не раз. И понял: лучший способ подготовиться – это прожить собес до того, как он реально случится.
Особенно в текущих реалиях, когда приглашение на собес получить не так-то просто. И не хочется слить его из-за глупой ошибки.
Поэтому я со своей командой провожу мок-собесы в формате 4 секций:
1. Java/Spring/БД – классические технические вопросы. Проверим глубину знаний и умение объяснять.
2. Live Coding – решаем задачи на алгоритмы. Здесь видно, как кандидат думает и насколько чисто пишет код в стрессовой обстановке.
3. Code Review – рассмотрим реальный кусок кода, где нужно найти проблемы и предложить улучшения.
4. Системный дизайн – разберём архитектуру сервиса: от постановки требований до продакшен-решения. Здесь проверяется не только знание инструментов, но и умение мыслить на более высоком уровне.
Можно выбрать произвольное количество секций: хоть одну, хоть все.
После каждой секции мы даём подробный фидбек:
– что выглядело уверенно
– где были пробелы
– что стоит подтянуть и как именно это сделать
Это не экзамен. Цель – подсветить слабые места и дать рекомендации, чтобы на реальном собесе чувствовать себя уверенно.
Если хочешь проверить свои силы в таком формате – бронируй встречу
Сегодня столкнулся с OOM в микросервисе, который вообще ничего не делал. Только был запущен. Трафик не принимал. Фоновые задачи не выполнял.
Прикол в том, что до этого, трафик в него поступал. Но из-за проблем в сторонней системе, трафик перевели в другой неймспейс кубера.
И через день стрельнул OOM. Контейнер перезапустился. Через сутки снова ООМ.
Размер хипа 256 МБ.
В логах пусто.
Снял хипдамп.
75% хипа забито очередью из класса org.springframework.cloud.sleuth.autoconfig.actuate.BufferingSpanReporter
Что это за класс?
Он активируется при включённом конфиге spring.sleuth.enabled и хранит трассировки.
Размер очереди ограничен 10.000 элементов. При переполнении самый старый элемент удаляется.
Все трассировки, которые мне попались при изучении хипдампа, относились к ошибкам интеграции со spring-boot-admin.
Микросервис пытался постоянно зарегистрировать себя в spring-boot-admin, но был указан невалидный урл и регистрация завершалась ошибкой.
Эта ошибка со стектрейсом и попадала в очередь BufferingSpanReporter. Что в итоге приводило к ООМ.
Почему ООМ не возникал при трафике?
Думаю из-за того, что другие трассировки были небольших размеров, их было больше и они вытесняли тяжёлые ошибочные трассировки от интеграции со spring-boot-admin.
Это лишь предположение. После указания валидного урла проблема ушла. Но я буду копать дальше, чтобы дойти до сути.
Сравнимаем производительность потокобезопасных счётчиков
Что обычно отвечает человек, когда его просят реализовать потокобезопасный счётчик?
– synchronized, как что-то стыдное
– AtomicLong, как что-то величественное
Но всегда ли synchronized проигрывает в производительности и есть ли что-то быстрее атомиков?
На самом деле потокобезопасный счётчик можно реализовать множеством разных способов:
– synchronized
– Semaphore
– ReentrantLock
– ReentrantReadWriteLock
– StampedLock
– AtomicLong
– LongAdder
Рассмотрим все эти варианты, сравним производительность в многопоточной среде и в разных сценариях использования чтения/записи.
Полная статья здесь
Роадмап подготовки к Java собеседованиям
Цель роадмапа – предоставить список тем и источников для быстрой подготовки к собеседованиям.
Это обновлённая версия. Работая над ней, я хотел собрать в одном месте как можно больше материалов для подготовки к собесам, и, что самое главное, подтолкнуть разработчиков к развитию.
Это не конечная версия. Она будет дополняться и поддерживаться в актуальном состоянии. Поэтому следите за обновлениями.
Так же хочу заметить, что роадмап это не развернутый список вопросов и ответов с собесов. Нет смысла заучивать ответы. Нужно понимать суть.
Темы:
– Java (архитектура jvm, gc, многопоточность)
– Spring (aop, transaction, cloud)
– SQL/NoSQL (acid, base, уровни изоляций, explain)
– Kafka/Docker/Kubernetes
– Паттерны проектирования, ООП, SOLID
– Алгоритмы и структуры данных
– Системный дизайн
– Soft skils
Полная версия роадмапа со всеми темами и источниками лежит здесь
Когда перфекционизм мешает писать хороший код
Я просто хочу, чтобы было красиво
Надо всё обобщить
Этот сервис можно сделать универсальным
4 ошибки, которые я делал, когда думал, что "всё понимаю"
Каждый из нас проходит через это.
Вроде бы уже не джун, копаешься в исходниках, дебажишь прод, можешь на ходу объяснить, как работает gc и зачем нужен circuit breaker.
И в какой-то момент в голове появляется опасная мысль:
Ну всё, я вроде понял, как оно работает
CompletableFuture.runAsync(() -> callExternalService());
Как говорить про проект на собеседовании: 3 удачные схемы
Что делает большинство?
Ну… это был корпоративный портал. Мы там использовали Spring. И делали интеграцию с 1С. Я там пилил фичи.
Бизнес хотел ускорить расчёт скидок в корзине: было слишком медленно, 3+ секунды. Я переписал калькулятор на чистом Java, оптимизировал SQL-запросы и закешировал правила. Это был отдельный сервис на Spring Boot с Redis и асинхронной обработкой. Время ответа – 300 мс.
Делали сервис нотификаций для внутренних систем. При получении события генерировались уведомления по шаблону и отправлялись через Kafka -> Email/Push. Использовали Spring Cloud Stream, FreeMarker, RetryTemplate, Prometheus, Zipkin для трейсинга.
Была монолитная система, которую тяжело было масштабировать. После перехода на микросервисы – катим фичи независимо, проще локализовать баги, уменьшили время отклика на 30%. Внедрили Spring Cloud, API Gateway, Eureka, и распилили 3 ключевых модуля: аутентификацию, биллинг и отчёты.
Пет-проекты не нужны
Ты уже долго работаешь на одном месте, хочешь сменить работу, попробовать новый стек, чувствуешь, что отстал от рынка, или решаешь просто прокачать резюме.
И вот первая мысль:
А может, мне сделать пет-проект? Ну, типа… TODO-лист или трекер задач.
Карточки про синхронизаторы Java
Синхронизаторы позволяют управлять потоками более гибко, мощно и безопасно, чем низкоуровневые и примитивные synchronized, wait, notify и join.
Примеры использования:
– Ограничить количество одновременных действий
– Дождаться завершения нескольких потоков
– Запустить все потоки одновременно
– Обменяться данными между потоками
Виды:
– Semaphore
– CountDownLatch
– CyclicBarrier
– Phaser
– Exchanger
Вечный Junior
С Антоном мы пересеклись на одном митапе, когда он работал джуном в аутстаффе уже 3 года.
Джуном 3 года, Карл!
Никакого роста и архитектурных задач. Ревью – формальность. Сеньоры заняты, а тимлид кидает таску и исчезает. Джуниорская вилочка и бесконечное "ещё не время".
Он чувствовал себя застрявшим.
И он был прав.
Но он боялся.
Боялся собеседований, общения с hr, рассказов о своем опыте.
«А вдруг я не пройду собес?»
«А вдруг я ничего не умею?»
«А вдруг я уйду, и окажется, что я действительно слабый?»
Правда в том, что это нормальное состояние.
Так чувствуют себя многие, кто остался без настоящей обратной связи и без возможности развиваться.
И это не их вина.
Это можно исправить.
Но нужен план, поддержка и кто-то, кто скажет: «Ты не тупой, просто ты слишком долго был один в этом болоте».
Я понимал, что могу помочь Антону, направить его в правильное русло.
Но раньше я никогда не менторил разработчиков не из своей команды.
Как и для Антона, так и для меня это был вызов.
И мы оба решили действовать.
С первого же созвона я понял – у Антона есть потенциал. Просто его никто не видел. А он сам – не знал, как его раскрыть.
Что мы сделали?
– Выявили пробелы в знания
– Прокачали многопоточку и алгоритмы
– Написали пет-проект с популярными технологиями
– Переписали резюме
– Провели мок-собеседование
– Разобрали реальные собеседования
– Обсудили его страхи
И вот спустя два месяца:
– Несколько офферов
– Один из бигтех
– Уровень мидл
– Доход x2
Главное – он обрел уверенность и понятный план развития до сеньора.
И знаешь, что самое интересное?
Ты можешь так же.
Ты можешь оставаться в болоте – или пойти по пути роста.
Ты можешь бояться – или готовиться и пробовать.
Ты можешь ждать, пока настанут времена получше – или взять и вытащить себя на новый уровень уже сейчас.
Выбор только за тобой.
Как выделиться среди других кандидатов, когда спрашивают про Transactional в Spring
Рассказать про механизмы создания прокси: JDK Dynamic Proxy, CGLIB, Byte Buddy.
Согласись, ответ «спринг использует несколько вариантов создания прокси, могу рассказать про каждый» звучит более экспертно, чем «ну там прокси используются».
Поэтому разберём отличия.
JDK Dynamic Proxy
Используется, если класс реализует интерфейс.
Встроен в JDK (java.lang.reflect.Proxy), лёгкий, быстрый, прост в отладке.
Работает только с интерфейсами.
CGLIB
Используется, если класс не реализует интерфейс.
Основан на наследовании – создаёт подкласс твоего класса с переопределёнными методами.
Не требует интерфейсов.
Не работает с final классами и методами, менее прозрачен в отладке.
Byte Buddy
Используется в более гибких и низкоуровневых сценариях (Spring Actuator, Spring DevTools, Mockito, APM-агенты).
Генерирует или модифицирует байткод в рантайм, без ограничения интерфейсами или наследованием.
Безумно гибкий, работает даже с final классами и методами (если использовать javaagent).
Какой прокси используется в Transactional
В AOP используется JDK Dynamic Proxy или CGLIB.
Если есть интерфейс – JDK Dynamic Proxy.
Если нет – CGLIB.
Есть возможность всегда использовать CGLIB, указав proxyTargetClass=true.
Можно легко понять, какой механизм используется, посмотрев в дебаг:
– JDK Dynamic Proxy: com.sun.proxy.$Proxy32
– CGLIB Proxy: MyService$$EnhancerBySpringCGLIB$$abc123 хочешь узнать как застать интервьюера врасплох на вопросах про прокси, ставь 🔥 и разберем в следующем посте.
Если
Извините, но мы не готовы сделать вам предложение
Я получал это сообщение десятки раз.
Оно всегда приходит холодным текстом. Без эмоций. Без объяснений.
И каждый раз – будто удар в поддых.
⠀
Ты читаешь и замираешь.
Ощущение, как будто тебе сказали:
Ты недостаточно хорош. Мы посмотрели на тебя – и решили, что ты не подходишь.
Может, я переоценил свой уровень?
А вдруг я уже не дотягиваю до рынка?
Может, мне стоит бросить java и уйти во что-то полегче?
Как один Scheduled превратился в DDoS на наш собственный сервис
Есть у нас микросервис, который раз в секунду проверяет статусы задач.
Обычный @Scheduled(fixedRate = 1_000)
Работал год – вообще без проблем.
Вычитывал из бд задачи, проверял статусы и обновлял другие таблицы.
А потом мы немного добавили логики:
– запросы к бд стали тяжелее
– появились внешние вызовы
– и, внимание, время выполнения стало больше секунды
Что это значит?
Scheduler с fixedRate не ждёт окончания предыдущей задачи.
Он просто запускает новую копию метода каждую секунду.
Из-за того, что предыдущий метод не успел завершиться и внести изменения в бд, новый метод работал со старыми данными и делал всю ту же работу, что и первый метод.
И вот у нас уже 5 потоков, 10, 15…
Потребление cpu взлетает.
Внешние системы начинают отваливаться, потому circuit breaker мы не добавили, подумав «ну что тут может случиться».
По факту мы устроили себе локальный DDoS.
Благо обнаружили мы это очень быстро, потому что у нас есть алерты на замедления.
В итоге мы поменяли fixedRate на fixedDelay и проблема ушла.
Поэтому не доверяй слепо аннотациям – понимай, как они работают внутри.