Java Backend Interview Cheatsheet

Java Backend / Tech Lead Interview Cheatsheet

2026

Темы: Collections, Concurrency, GC, SQL/PostgreSQL, Hibernate/JPA, Spring/Spring Boot, Kafka, Kubernetes

Формат: основной вопрос → ответ. Где уместно — блок ↺ Уточняющий вопрос с возможным продолжением интервьюера и ответом.


1. Collections

HashMap internals

Массив бакетов. Индекс бакета = hash(key) & (n-1). При коллизии — связный список, который при длине ≥8 и размере таблицы ≥64 превращается в красно-чёрное дерево (O(log n) вместо O(n)). При заполнении выше load factor (0.75) — resize: удвоение массива и рехеширование.

↺ Зачем дерево с Java 8?
Длинная цепочка вырождала поиск в O(n), что эксплуатировалось в hash-collision DoS-атаках. Дерево ограничивает худший случай до O(log n).

↺ Почему именно степень двойки как размер?
Чтобы вычислять бакет дешёвой битовой операцией hash & (n-1) вместо деления по модулю. Дополнительно Java перемешивает старшие биты хэша (h ^ (h >>> 16)), чтобы они влияли на индекс.


HashMap vs LinkedHashMap vs TreeMap


equals и hashCode

Нарушение контракта (равные объекты — разные хэши) → объект «теряется»: положили в один бакет, ищем в другом. Правило: переопределяешь equals — обязан переопределить hashCode.

↺ Что будет, если hashCode всегда возвращает константу?
Всё попадёт в один бакет → HashMap выродится в список (или дерево) → O(n)/O(log n) вместо O(1). Работать будет, но медленно.


ArrayList vs LinkedList

ArrayList — массив, O(1) доступ по индексу, вставка/удаление в середину O(n). LinkedList — O(1) вставка по известному узлу, O(n) доступ по индексу. На практике ArrayList почти всегда быстрее (локальность кэша); LinkedList редко оправдан.


ConcurrentHashMap vs synchronizedMap vs Hashtable

synchronizedMap/Hashtable блокируют всю структуру на каждую операцию. ConcurrentHashMap блокирует только нужный бакет (Java 8+: CAS + synchronized на узле), чтение почти без блокировок. Не допускает null ключей/значений.

↺ Почему ConcurrentHashMap запрещает null?
В конкурентной среде get()==null неоднозначно: «нет ключа» или «значение null»? Без блокировки это не различить через containsKey. Запрет убирает неоднозначность.


fail-fast vs fail-safe итераторы

Fail-fast (ArrayList, HashMap) — бросают ConcurrentModificationException при изменении во время итерации (через modCount). Fail-safe (CopyOnWriteArrayList, ConcurrentHashMap) — итерируют по снимку / без исключения.


CopyOnWriteArrayList

Много чтений, мало записей (список слушателей). Каждая запись копирует весь массив — дорого, зато чтение без блокировок.


2. Concurrency

happens-before

Гарантия JMM: если A happens-before B, результаты A видны B. Устанавливается через: synchronized (выход из монитора → вход), volatile (запись → чтение), Thread.start(), Thread.join(), операции с Lock, инициализация final-полей в конструкторе.


volatile vs synchronized

volatile — только видимость одиночного поля, без атомарности составных операций (i++ не потокобезопасен). synchronized — и видимость, и взаимное исключение блока. Для счётчика — AtomicInteger или synchronized, не volatile.

↺ Почему i++ не атомарен даже с volatile?
i++ — три операции: read, increment, write. volatile гарантирует видимость каждой, но не атомарность их связки. Между read и write другой поток может вклиниться.


ReentrantLock vs synchronized

ReentrantLock гибче: tryLock() (с таймаутом), lockInterruptibly(), честная очередь, несколько Condition. Требует ручного unlock() в finally. synchronized проще и достаточен в большинстве случаев.


CAS и ABA-проблема

Compare-And-Swap — атомарно «если значение == ожидаемому, замени». Основа атомиков, без блокировок. ABA: значение менялось A→B→A, CAS не заметит подмену. Решение — AtomicStampedReference (счётчик версий).


ExecutorService и типы пулов

В проде — явный ThreadPoolExecutor с ограниченной очередью и понятным RejectedExecutionHandler.

↺ Что произойдёт при переполнении очереди пула?
Сработает RejectedExecutionHandler. Политики: AbortPolicy (default, бросает исключение), CallerRunsPolicy (задачу выполнит вызывающий поток — естественный backpressure), DiscardPolicy, DiscardOldestPolicy.


Future vs CompletableFuture

Future — блокирующий get(), без колбэков и композиции. CompletableFuture — цепочки (thenApply, thenCompose), комбинирование (allOf/anyOf), обработка ошибок (exceptionally), свой executor.


Virtual Threads (Java 21)

Лёгкие потоки под управлением JVM, не привязаны 1:1 к OS-потокам. Можно миллионы. При блокирующем I/O не держат OS-поток. Идеальны для I/O-bound сервисов. Не дают выигрыша для CPU-bound.

↺ Что такое pinning?
Если virtual thread входит в synchronized-блок или нативный вызов, он «пиннится» к carrier-потоку и не может отцепиться при блокировке — теряется преимущество. Рекомендация: заменять synchronized на ReentrantLock в горячем коде (в новых версиях JDK pinning на synchronized частично устранён).


deadlock / livelock / starvation


ThreadLocal и риск утечки

Хранит данные per-thread (контекст транзакции, SecurityContext). В пуле потоков поток не умирает → значение остаётся → утечка памяти и протечка данных между запросами. Всегда remove() в finally.


CountDownLatch vs CyclicBarrier vs Semaphore


3. Garbage Collection

Memory layout

Heap (Young = Eden + 2 Survivor, Old Gen), Metaspace (метаданные классов, нативная память), Stack (фреймы, per-thread), Code Cache (JIT).


Generational GC

Гипотеза: большинство объектов умирают молодыми. Новые — в Eden. Minor GC чистит Young (быстро), выжившие мигрируют между Survivor и в Old Gen. Major/Full GC чистит Old (дорого, длинные паузы).


Stop-the-World

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


G1 vs ZGC vs Shenandoah

Для low-latency сервиса под нагрузкой — аргумент за ZGC/Shenandoah; для типового сервиса хватает G1.


GC roots и определение мусора

Достижимость от GC roots (стек потоков, статические поля, JNI-ссылки). Недостижимый объект — мусор. Циклические ссылки между мусором собираются (это не reference counting).


OutOfMemoryError

Java heap space (нехватка/утечка), Metaspace (слишком много классов), GC overhead limit exceeded (GC >98% времени). Диагностика: -XX:+HeapDumpOnOutOfMemoryError, анализ дампа (MAT/VisualVM), GC-логи, поиск растущих коллекций / незакрытых ресурсов / ThreadLocal.


Reference types


JVM flags for highload

-Xms = -Xmx (без ресайза heap), выбор сборщика (-XX:+UseZGC), -XX:MaxGCPauseMillis (цель для G1), heap dump on OOM.

↺ Особенность JVM в контейнере (Kubernetes)?
Старые JVM не видели cgroup-лимиты контейнера и брали память хоста → OOMKill. Решение: -XX:MaxRAMPercentage вместо фиксированного -Xmx (современные JDK по умолчанию учитывают лимиты контейнера). limit памяти пода должен быть выше heap (есть ещё Metaspace, стеки, off-heap).


4. SQL / PostgreSQL

Index types


Index not used?

Функция над колонкой (LOWER(name)=... — нужен функциональный индекс), LIKE '%suffix', неявное приведение типов, низкая селективность (Seq Scan дешевле), устаревшая статистика (нужен ANALYZE).


EXPLAIN ANALYZE

Seq Scan где ждёшь Index Scan; расхождение rows (оценка) и actual rows (факт) → плохая статистика; actual time; loops (узел в цикле — признак N+1 на уровне БД); тяжёлые Sort/Hash Join с уходом на диск.


Isolation levels

LevelDirtyNon-repeatablePhantom
READ UNCOMMITTEDyes*yesyes
READ COMMITTEDnoyesyes
REPEATABLE READnonoyes*
SERIALIZABLEnonono

PostgreSQL default — READ COMMITTED.

↺ Особенность PostgreSQL?
READ UNCOMMITTED работает как READ COMMITTED (грязного чтения нет вообще). REPEATABLE READ через snapshot isolation блокирует и phantom. SERIALIZABLE использует SSI (Serializable Snapshot Isolation) и может откатывать транзакции с serialization failure — приложение должно уметь ретраить.


MVCC

Multi-Version Concurrency Control: каждая транзакция видит снимок данных на её начало. Читатели не блокируют писателей и наоборот. Старые версии (dead tuples) удаляет VACUUM/autovacuum.

↺ Чем грозит отсутствие/отставание VACUUM?
Раздувание таблиц (table bloat), деградация производительности, в пределе — wraparound transaction ID (катастрофа). Autovacuum должен успевать; под высокой нагрузкой его тюнят.


Optimistic vs Pessimistic locking

Пессимистичная — SELECT ... FOR UPDATE, лочим строку сразу (высокая вероятность конфликта). Оптимистичная — version-колонка, проверка при сохранении, конфликт → ретрай (низкая вероятность, лучше масштабируется).


Window functions

Агрегаты без схлопывания строк: ROW_NUMBER(), RANK(), LAG/LEAD, SUM() OVER (PARTITION BY ...). Пример — нумерация заявок в рамках клиента, скользящие суммы.


Read scaling

Read-replica (стриминговая репликация), отчёты/аналитика на реплику. Помнить про репликационный лаг (eventual consistency).


5. Hibernate / JPA

N+1 problem

Загрузили N сущностей → на каждую связь отдельный запрос → 1 + N запросов. Решения: JOIN FETCH в JPQL, @EntityGraph, @BatchSize (пачками через IN), DTO-проекция. Обнаружение — лог SQL / Hibernate statistics / p6spy.


LAZY vs EAGER

LAZY — связь грузится при первом обращении (default для коллекций). EAGER — сразу (default для @ManyToOne/@OneToOne). Правило: всё LAZY, грузить осознанно через fetch. EAGER порождает N+1 и тянет лишнее.


LazyInitializationException

Обращение к LAZY-связи после закрытия сессии/транзакции (в контроллере). Лечение: грузить нужное в транзакции (JOIN FETCH/@EntityGraph), маппить в DTO внутри транзакции.

↺ Почему OpenSessionInView — антипаттерн?
Держит сессию открытой на весь HTTP-запрос (включая рендеринг) → маскирует N+1, удерживает соединение из пула дольше нужного, ленивые запросы летят неконтролируемо. Лучше явно грузить данные в сервисном слое.


Entity lifecycle

Transient (новый, не в контексте) → Managed/Persistent (в persistence context, изменения трекаются) → Detached (контекст закрыт) → Removed. merge возвращает detached в managed (создаёт копию!), persist сохраняет transient.


Persistence Context и dirty checking

Кэш первого уровня в рамках транзакции. Hibernate трекает managed-сущности и при flush сам генерирует UPDATE для изменённых полей — без явного save(). First-level cache: повторный find по тому же ID не идёт в БД.


flush vs commit

flush — синхронизация изменений в БД (SQL отправлен), транзакция ещё открыта (можно откатить). commit — фиксация (внутри сначала flush).


Batch save

Накопление сущностей в context → рост памяти, медленный flush. Решение: периодический flush() + clear(), hibernate.jdbc.batch_size, упорядочивание insert/update.


Optimistic locking in JPA

@Version-поле. Hibernate добавляет WHERE version = ? в UPDATE; 0 обновлённых строк → OptimisticLockException. Для конкурентного редактирования — естественный выбор.


Hibernate vs native SQL / Spring Data JDBC

Hibernate хорош для CRUD по графу объектов. Для сложной аналитики, оконных функций, тяжёлых отчётов — нативный запрос или отдельный read-слой (CQRS). Trade-off JPA vs Spring Data JDBC: последний проще и предсказуемее по SQL, без ленивой загрузки и магии прокси.


6. Spring / Spring Boot

IoC и DI

IoC — управление созданием/жизненным циклом объектов передано контейнеру. DI — способ реализации: зависимости передаются извне, а не создаются объектом.


Injection types

При одном конструкторе @Autowired не нужен.

↺ Как Spring разрешает несколько кандидатов одного типа?
@Primary (по умолчанию), @Qualifier("name") (явно), или по имени параметра. Иначе NoUniqueBeanDefinitionException.


Bean lifecycle

1. Инстанцирование → 2. DI → 3. *Aware → 4. postProcessBeforeInitialization → 5. @PostConstruct/afterPropertiesSet → 6. postProcessAfterInitialization (здесь оборачиваются прокси — AOP, @Transactional) → 7. готов → 8. @PreDestroy/destroy.


Bean scopes

singleton (default), prototype, request/session (web).

↺ Что будет, если инжектить prototype в singleton?
Prototype создастся один раз (при создании singleton) и переиспользуется — обычно не то, что хотели. Решения: ObjectProvider, @Lookup, Provider<T>.


Spring Boot autoconfiguration

@SpringBootApplication@EnableAutoConfiguration. Сканирует classpath, через ...AutoConfiguration.imports подключает конфиги по условиям @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty. Convention over configuration: объявил свой бин → автоконфиг отступает.

↺ Как переопределить/отладить?
Свой бин побеждает авто; можно исключить через exclude. Запуск с --debug даёт отчёт о сработавших/несработавших условиях.


Configuration hierarchy

Аргументы CLI → env-переменные → application-{profile}.ymlapplication.yml → дефолты. Relaxed binding: SPRING_DATASOURCE_PASSWORD (env) → spring.datasource.password — именно так инжектятся секреты из k8s Secret.


Spring AOP и прокси

Spring оборачивает бин прокси, перехватывающим вызовы (транзакции, кэш, лог). JDK Dynamic Proxy (есть интерфейс) или CGLIB (наследование, default в Boot).

↺ Почему @Transactional не работает при self-invocation?
Вызов this.method() изнутри того же бина идёт напрямую, минуя прокси → аспект не срабатывает. То же для @Cacheable, @Async. Также не работает на private/final-методах. Решения: вынести в отдельный бин, self-ссылка, AopContext.currentProxy().


@Transactional internals

Прокси открывает транзакцию, коммитит при успехе, откатывает при unchecked-исключении.

↺ Что откатывает транзакцию по умолчанию?
Только RuntimeException и Error. Checked-исключения НЕ откатывают — частый баг. Переопределяется: @Transactional(rollbackFor = Exception.class).

Propagation: REQUIRED (default), REQUIRES_NEW (новая, текущая приостановлена — аудит/лог, переживающий откат), NESTED (savepoint), MANDATORY/NEVER/SUPPORTS/NOT_SUPPORTED.


Web layer

DispatcherServlet — front controller: HandlerMapping → контроллер → интерсепторы → HttpMessageConverter (JSON через Jackson). @RestController = @Controller + @ResponseBody. Централизованная обработка ошибок — @RestControllerAdvice + @ExceptionHandler. Валидация — @Valid + Bean Validation.

↺ Фильтр vs интерсептор?
Filter — уровень сервлет-контейнера, до Spring MVC (аутентификация, CORS). Interceptor — внутри MVC, видит handler. Spring Security работает через цепочку фильтров.


Testing

@SpringBootTest — весь контекст (интеграционные, медленно). Срезы быстрее: @WebMvcTest (web + MockMvc), @DataJpaTest (JPA + тестовая БД), @JsonTest. @MockBean/@MockitoBean — подмена бина моком. Testcontainers — реальная PostgreSQL/Kafka в Docker для интеграционных (ближе к проду, чем H2).


Spring MVC vs WebFlux

MVC — thread-per-request, блокирующий, прост, хорош с JPA. WebFlux — реактивный event-loop, неблокирующий (Mono/Flux), меньше потоков при массе I/O.

↺ Когда WebFlux оправдан и при чём тут virtual threads?
WebFlux — много медленных I/O-вызовов, высокая конкурентность при ограниченных потоках; минус — сложность, нужен R2DBC (JPA блокирует). С Java 21 virtual threads закрывают исходную мотивацию WebFlux: блокирующий простой код без платы за поток на запрос. Senior-поинт: для нового сервиса с блокирующими зависимостями — MVC на virtual threads вместо реактивщины.


Actuator

Эндпоинты эксплуатации: /health (liveness/readiness → k8s-пробы), /metrics (Micrometer → Prometheus → Grafana), /loggers (смена уровня на лету). Безопасность: закрывать /env, /heapdump от внешнего доступа.


7. Apache Kafka

Topic, Partition, Broker, Offset

Топик — категория сообщений, разбит на партиции. Партиция — упорядоченный неизменяемый лог на одном брокере (+реплики), единица параллелизма. Брокер — сервер кластера. Offset — порядковый номер сообщения в партиции; consumer хранит свой offset в __consumer_offsets.


Consumer groups

Партиции распределяются между consumer'ами группы: каждую партицию читает ровно один consumer в группе. Consumer'ов больше партиций → лишние простаивают. Механизм горизонтального масштабирования.


Message ordering

Гарантирован только внутри партиции. Связанные сообщения → одинаковый partition key → одна партиция. Глобального порядка по топику нет.

↺ Что сломается при увеличении числа партиций?
hash(key) % partitions изменится → сообщения с одним ключом могут попасть в разные партиции → порядок нарушится. Партиции закладывают с запасом и не меняют на лету.


Delivery guarantees

↺ Как бороться с дублями на стороне consumer?
Идемпотентная обработка: дедуп по идемпотентному ключу (ID сообщения) в БД, проверка перед выполнением. Настоящий exactly-once при записи во внешние системы недостижим без идемпотентности приёмника.


Replication и ISR

У партиции лидер + followers. Чтение/запись через лидера, followers догоняют. ISR (In-Sync Replicas) — реплики, не отстающие от лидера. При падении лидера новый из ISR.

acks: 0 (не ждём, быстро/рискованно), 1 (ждём лидера, баланс), all (ждём все ISR, надёжно). В связке с min.insync.replicas гарантирует запись на N реплик.

↺ Как не потерять сообщения?
Producer: acks=all, retries>0, enable.idempotence=true. Брокер: min.insync.replicas=2 при RF=3. Consumer: commit только после успешной обработки.


Consumer lag

Отставание consumer'а от последнего сообщения. Решения: добавить consumer'ов (≤ числа партиций), ускорить обработку (батчинг/параллелизм), увеличить max.poll.records, вынести тяжёлую логику.


Retention vs compaction

Retention — удаление по времени/размеру (топик как поток событий). Log compaction — хранит последнее значение по ключу (топик как «текущее состояние», напр. актуальный профиль).


Rebalancing

Перераспределение партиций при изменении состава группы. Во время — обработка останавливается (stop-the-world). Смягчают: session.timeout.ms, heartbeat.interval.ms, cooperative rebalancing.


DLQ (Dead Letter Queue)

Сообщения, не обработанные после N ретраев, — в отдельный топик, чтобы не блокировать остальные и разобрать позже.


Kafka in Spring Boot

application.yml

spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      acks: all
      properties:
        enable.idempotence: true
    consumer:
      group-id: subsidy-service
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      enable-auto-commit: false
      properties:
        spring.json.trusted.packages: "*"
    listener:
      ack-mode: manual

Зависимость spring-kafka. Автоконфигурация поднимает KafkaTemplate и фабрики.

Producer

@Service
public class SubsidyEventProducer {
    private final KafkaTemplate<String, SubsidyEvent> kafkaTemplate;

    public SubsidyEventProducer(KafkaTemplate<String, SubsidyEvent> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void send(SubsidyEvent event) {
        // key = applicationId — all events for one application go to same partition (ordering)
        kafkaTemplate.send("subsidy-events", event.applicationId(), event);
    }
}

Consumer

@Component
public class SubsidyEventConsumer {

    @KafkaListener(topics = "subsidy-events", groupId = "subsidy-service")
    public void listen(SubsidyEvent event, Acknowledgment ack) {
        try {
            process(event);     // should be idempotent
            ack.acknowledge();  // commit offset ONLY after successful processing
        } catch (Exception e) {
            throw e;            // offset not committed — will be re-read
        }
    }
}

Key points:


8. Kubernetes

Pod, Deployment, Service, Ingress

↺ Deployment vs StatefulSet?
Deployment — stateless, поды взаимозаменяемы. StatefulSet — stateful (БД, Kafka): стабильные имена, упорядоченный старт/стоп, привязка к своему хранилищу.


Probes: liveness vs readiness vs startup

↺ Чем грозит неправильная настройка?
Агрессивный liveness → бесконечные перезапуски под нагрузкой. Нет readiness → трафик на непрогретый под → ошибки при деплое. liveness вместо readiness для зависимостей (БД недоступна) → каскадные перезапуски вместо вывода из балансировки.


Requests vs Limits

Request — гарантированный минимум, для планирования. Limit — потолок: CPU → throttling, память → OOMKill. Рекомендация: requests = типичное потребление, limits — с запасом, без перерасхода памяти.

↺ QoS-классы?
Guaranteed (requests=limits), Burstable (requests<limits), BestEffort (ничего). При нехватке на ноде первыми вытесняются BestEffort, затем Burstable.


HPA

Horizontal Pod Autoscaler меняет число реплик по метрикам (CPU/память/кастомные — RPS, Kafka lag). Для кастомных нужен metrics adapter (Prometheus Adapter). VPA — вертикально; с HPA по CPU/памяти не сочетают (конфликт).


Rolling Update vs Recreate

Rolling (default) — постепенно, без простоя (maxSurge, maxUnavailable). Recreate — гасим все, потом поднимаем (простой, но нет двух версий — важно при несовместимой схеме БД).

↺ Blue-green vs canary?
Blue-green — две полные среды, переключение разом (быстрый откат, двойные ресурсы). Canary — новая версия на малую долю трафика, постепенно (ловит проблемы на части пользователей).

Zero-downtime deploy: readiness + graceful shutdown — SIGTERM → вывод из Service → дочитать запросы (preStop, terminationGracePeriodSeconds) → завершение.


ConfigMap vs Secret

ConfigMap — несекретные конфиги. Secret — чувствительные данные, в base64 (НЕ шифрование). Реальная защита: encryption at rest для etcd + внешнее хранилище (Vault).


PersistentVolume vs PersistentVolumeClaim

PV — ресурс хранилища в кластере. PVC — запрос пода на хранилище нужного размера/класса. Связывание часто динамическое через StorageClass.


Service discovery

Внутренний DNS (CoreDNS): service.namespace.svc.cluster.local. Типы: ClusterIP (внутри кластера, default), NodePort, LoadBalancer (внешний балансировщик облака), Headless (без балансировки, для StatefulSet).


PodDisruptionBudget

Минимум доступных подов при добровольных нарушениях (обновление/drain нод). Защита от случайного падения всех реплик сразу.


ArgoCD — GitOps CD

Инструмент GitOps continuous delivery. Git — единственный источник истины о желаемом состоянии кластера. Манифесты/Helm/Kustomize лежат в Git, ArgoCD непрерывно сравнивает их с live-состоянием.

Что даёт: декларативность и аудит (история в Git), откат = git revert, нет ручного kubectl apply в прод, визуализация дерева ресурсов (Synced/OutOfSync, Healthy/Degraded).

↺ ArgoCD vs Jenkins?
Jenkins — CI, push-модель (пайплайн пушит в кластер). ArgoCD — CD, pull-модель (кластер подтягивает из Git). Связка: CI собирает образ и обновляет тег в Git-манифесте → ArgoCD деплоит.


Vault через External Secrets Operator (ESO)

Проблема: секреты нельзя в Git открыто, k8s Secret — лишь base64. Решение: секреты в Vault, в кластер подтягиваются автоматически. ESO синхронизирует секреты из Vault в нативные k8s Secret; в Git — только ссылка (путь в Vault), не значение.

SecretStore

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: subsidy
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "subsidy-role"
          serviceAccountRef:
            name: "subsidy-sa"

ExternalSecret

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: subsidy-db-secret
  namespace: subsidy
spec:
  refreshInterval: "1h"
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: subsidy-db-credentials
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: subsidy/database
        property: password
    - secretKey: DB_USERNAME
      remoteRef:
        key: subsidy/database
        property: username

ESO периодически читает Vault → создаёт/обновляет k8s Secret. Изменился секрет в Vault → обновится k8s Secret.

Inject into pod env

spec:
  template:
    spec:
      serviceAccountName: subsidy-sa
      containers:
        - name: app
          image: registry.internal/subsidy:1.2.0
          env:
            - name: SPRING_DATASOURCE_USERNAME
              valueFrom:
                secretKeyRef:
                  name: subsidy-db-credentials
                  key: DB_USERNAME
            - name: SPRING_DATASOURCE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: subsidy-db-credentials
                  key: DB_PASSWORD

Spring Boot подхватит SPRING_DATASOURCE_* через relaxed binding.

Full flow: Vault → ESO authenticates via ServiceAccount → reads path → creates k8s Secret → pod mounts as env → Spring Boot reads env. In Git — only SecretStore/ExternalSecret/Deployment, no passwords → GitOps/ArgoCD compatible.

↺ ESO vs Vault Agent Injector?
ESO материализует секрет в k8s Secret (прозрачно для приложения — обычный env; но секрет существует в etcd). Vault Agent Injector (sidecar) монтирует секрет прямо в файл пода, минуя k8s Secret (не материализуется в etcd; приложение читает из файла). Выбор по требованиям ИБ.


Secrets in env — security considerations

Env — наименее безопасный из распространённых способов:

More secure (ascending):

  1. Files instead of env (volume mount / Vault Agent Injector в tmpfs) — не наследуются дочерними, не светят в env/дампах, права файла, ротация без рестарта.
  2. Runtime fetch from Vault — секрет только в памяти приложения, нигде не материализуется.
  3. Dynamic secrets — Vault выдаёт короткоживущие креды к БД с TTL; утекли → протухли за минуты.

Bottom line: env + k8s Secret — приемлемый baseline, но для финтеха правильнее двигаться к файловому монтированию и/или динамическим секретам, плюс обязательно encryption at rest для etcd и жёсткий RBAC на kubectl exec.


9. Liquibase

What is Liquibase?

Инструмент версионирования схемы БД: изменения описываются как упорядоченные changeset'ы в changelog'е и применяются автоматически при деплое. Решает проблему «у всех разные версии схемы»: схема меняется только через миграции, история — в служебной таблице DATABASECHANGELOG.


Changelog structure

databaseChangeLog:
  - changeSet:
      id: 001-create-application
      author: dev
      changes:
        - createTable:
            tableName: application
            columns:
              - column: { name: id, type: bigint, autoIncrement: true,
                          constraints: { primaryKey: true } }
              - column: { name: status, type: varchar(32),
                          constraints: { nullable: false } }
      rollback:
        - dropTable: { tableName: application }

Tracking applied changesets

Таблица DATABASECHANGELOG: id, author, путь, checksum, дата применения. При старте сравнивает changelog с таблицей: новые changeset'ы применяет, применённые проверяет по checksum.

↺ Что будет, если изменить уже применённый changeset?
Ошибка checksum mismatch — changeset'ы неизменяемы. Правильно: новый changeset с исправлением. Обходные пути (осознанно): validCheckSum в changeset или clearCheckSums — но это исключение, не практика.


Multiple nodes (Kubernetes)

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

↺ Под упал во время миграции — что с локом?
Лок может остаться висеть → следующие старты ждут бесконечно. Лечение: liquibase release-locks или ручной UPDATE DATABASECHANGELOGLOCK SET LOCKED=false. Best practice для k8s — выносить миграции из старта приложения в init-контейнер или отдельный Job, чтобы реплики приложения не гонялись за локом и rolling update не зависал.


Rollback

Откат changeset'а. Для простых изменений (createTable) генерируется автоматически, для сложных (изменение данных, dropColumn) — пишется вручную в блоке rollback. Команды: rollback-count N, rollback <tag>, rollback-to-date. Главное преимущество над бесплатным Flyway, где undo-миграции платные.


Preconditions и contexts


Liquibase vs Flyway

FlywayLiquibase
FormatSQL files V1__name.sqlXML/YAML/JSON/SQL changelog
IdentificationVersion in filenameid + author + checksum
RollbackPaidBuilt-in
Conditional logicNo (Java migrations as workaround)Preconditions, contexts, labels
Diff/generateNodiff, generate-changelog
Learning curveLower (just SQL)Higher, but more flexible

Spring Boot integration: зависимость liquibase-core + spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml. Миграции применяются при старте контекста — так же, как Flyway.


10. SOAP

SOAP message structure

Протокол обмена структурированными XML-сообщениями. Сообщение — Envelope, внутри:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>...</soap:Header>
  <soap:Body>
    <getSubsidyStatus xmlns="http://example.com/subsidy">
      <applicationId>12345</applicationId>
    </getSubsidyStatus>
  </soap:Body>
</soap:Envelope>

WSDL

Web Services Description Language — XML-контракт сервиса: какие операции есть, какие сообщения они принимают/возвращают, типы данных (XSD-схема), binding (как кодировать, обычно SOAP over HTTP) и endpoint (адрес). По WSDL генерируется клиентский код.

↺ Contract-first vs code-first?
Contract-first: сначала WSDL/XSD, из него генерируются классы (wsimport, jaxb-плагины). Code-first: пишем Java, WSDL генерируется из аннотаций. Для интеграций с внешними системами (госорганы, банки) — всегда contract-first: контракт диктует владелец сервиса.


Calling SOAP from Java/Spring

Два основных пути:

  1. JAX-WS — стандарт Java: wsimport генерирует прокси-классы из WSDL, вызов как обычного Java-метода.
  2. Spring WSWebServiceTemplate (аналог RestTemplate): JAXB-маршалинг объектов в XML, перехватчики для WS-Security и логирования.
// Spring WS client
public class SubsidyClient extends WebServiceGatewaySupport {
    public GetStatusResponse getStatus(long id) {
        GetStatusRequest request = new GetStatusRequest();
        request.setApplicationId(id);
        return (GetStatusResponse) getWebServiceTemplate()
            .marshalSendAndReceive("https://gov.example.com/subsidy", request);
    }
}

SOAP vs REST

SOAPREST
StyleOperations (RPC)Resources (HTTP semantics)
FormatXML onlyUsually JSON
ContractWSDL, mandatory, strictOpenAPI, optional
SecurityWS-Security (message-level sign/encrypt)TLS + OAuth2/JWT (transport-level)
TransportAny (HTTP, JMS)HTTP
Where aliveGov systems, banks, EDI, legacyModern APIs

↺ Что такое WS-Security? Чем отличается от TLS?
TLS защищает канал (точка-точка, расшифровывается на каждом промежуточном узле). WS-Security защищает само сообщение: XML-подпись и шифрование частей Envelope, токены в Header. Сообщение остаётся защищённым при передаче через посредников (брокеры, шины) — end-to-end.


11. gRPC

What is gRPC?

RPC-фреймворк от Google: вызовы удалённых методов поверх HTTP/2 с бинарной сериализацией Protocol Buffers. Contract-first: API описывается в .proto, из него генерируется код клиента и сервера для любого языка.

Отличия от REST: бинарный компактный формат (быстрее JSON), HTTP/2 (мультиплексирование запросов в одном соединении, server push), строгая типизация контракта, стриминг из коробки, deadlines/cancellation как часть протокола.

syntax = "proto3";

service SubsidyService {
  rpc GetStatus (StatusRequest) returns (StatusResponse);
  rpc WatchStatus (StatusRequest) returns (stream StatusResponse); // server streaming
}

message StatusRequest {
  int64 application_id = 1;
}
message StatusResponse {
  string status = 1;
  string updated_at = 2;
}

Four call types

  1. Unary — запрос → ответ (аналог REST).
  2. Server streaming — запрос → поток ответов (подписка на изменение статуса).
  3. Client streaming — поток запросов → один ответ (загрузка чанков файла).
  4. Bidirectional streaming — двусторонний поток (чат, realtime-синхронизация).

Protocol Buffers

Бинарный формат сериализации со схемой. Поля кодируются по номерам (= 1, = 2), а не по именам → компактность; нет парсинга текста → скорость. Схема обеспечивает типобезопасность и кодогенерацию.

↺ Как обеспечивается обратная совместимость в proto?
Правила эволюции: не менять номера существующих полей, не переиспользовать номера удалённых (помечать reserved), новые поля — только с новыми номерами и опциональной семантикой. Старый клиент просто игнорирует незнакомые поля.


gRPC drawbacks

↺ Как балансировать gRPC в Kubernetes?
Обычный ClusterIP Service балансирует соединения, а не вызовы — gRPC-клиент откроет одно HTTP/2-соединение и все запросы уйдут в один под. Решения: headless service + client-side load balancing (round_robin в gRPC-клиенте), либо L7-прокси / service mesh (Envoy, Istio, Linkerd), который балансирует на уровне HTTP/2-стримов.


gRPC in architecture

Синхронное межсервисное взаимодействие внутри периметра, где важна низкая задержка и строгий контракт: расчёт графика платежей, проверка лимитов, валидация заявки. Наружу (фронт, партнёры) — REST через API Gateway.

↺ gRPC vs Kafka — когда что?
Это не конкуренты, а разные паттерны. gRPC — синхронный запрос-ответ, когда вызывающему нужен результат сейчас (проверить лимит до подтверждения заявки). Kafka — асинхронные события: развязка сервисов, гарантии доставки, переживание недоступности потребителя.


Deadlines и errors


Quick reminders