Разберём типичный кейс.
Твой сервис — оркестратор. Он дергает несколько downstream-сервисов, чтобы обработать запрос.
Один из них проблемный: медленный и периодически отвечает 503 Service Unavailable.
1. Как сделать сервис устойчивым к ненадёжному downstream
Первое, что здесь просится » Circuit Breaker.
Идея простая: если зависимый сервис начинает фейлиться, мы перестаём его дёргать, чтобы:
- не тратить ресурсы,
- не увеличивать латентность,
- не убивать весь сервис каскадными таймаутами.
Реализация с Resilience4j:
Подключаем зависимость и оборачиваем вызов проблемного сервиса в CircuitBreaker.
Ключевые состояния:
Всё нормально. Запросы проходят. Ошибки считаются.
Порог ошибок превышен. Все вызовы сразу фейлятся, downstream даже не вызывается.
Пробный режим. Ограниченное число запросов, чтобы проверить — ожил сервис или нет.
Пример:
@CircuitBreaker(name = "inventoryService", fallbackMethod = "inventoryFallback")
public InventoryResponse getInventory(String productId) {
return inventoryClient.getInventory(productId);
}
Конфиг обычно задаётся через application.yml:
- процент ошибок
- sliding window
- waitDurationInOpenState
- permittedNumberOfCallsInHalfOpenState
2. Что делать, когда circuit открыт (fallback)
Тут нет универсального ответа » зависит от бизнеса.
Типовые варианты:
🔹Кэш
Если данные не критичны к свежести:
- Redis
- локальный cache
- stale-данные лучше, чем 503
private InventoryResponse inventoryFallback(String productId, Throwable ex) {
return cacheService.getInventory(productId);
}🔹Дефолтный ответ
Если можно вернуть «безопасное» значение:
- пустой список
- available = false
- статус UNKNOWN
🔹Очередь
Если запрос важен, но не срочный:
- кладём событие в Kafka / Rabbit
- обрабатываем асинхронно
- отвечаем клиенту 202 Accepted
На практике часто комбинируют:
кэш + деградация функциональности.
3. Глобальная обработка ошибок через
@RestControllerAdviceЧтобы не размазывать try/catch по всему коду, делаем централизованный обработчик.
Пример бизнес-исключения
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String id) {
super("Product not found: " + id);
}
}Глобальный handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(ProductNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"PRODUCT_NOT_FOUND",
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
JSON-ответ
{
"code": "PRODUCT_NOT_FOUND",
"message": "Product not found: 123"
}
Плюсы подхода:
- чистые контроллеры
- единый формат ошибок
- нормальная мапа бизнес-ошибок на HTTP-статусы
Итог
На таком вопросе проверяют не знание аннотаций, а мышление:
- понимаешь ли ты отказоустойчивость
- умеешь ли деградировать сервис
- разделяешь ли бизнес-ошибки и технические фейлы
Это уже разговор не про «Java», а про архитектуру продакшн-сервисов
👉 Java Portal
