← Назад к вопросам
Какой тип исключений бы выбрал при реализации сервиса бронирования, если не удается подключиться к микросервису?
2.0 Middle🔥 181 комментариев
#REST API и микросервисы#Основы Java
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Какой тип исключений выбрать при ошибке подключения к микросервису?
Это отличный вопрос о проектировании обработки ошибок в распределенных системах. Ответ требует баланса между информативностью, обрабатываемостью и архитектурой.
Краткий ответ
Для ошибки подключения к микросервису в сервисе бронирования я выбрал бы checked exception или custom unchecked exception:
- Checked Exception если должна обрабатываться на уровне вызывающего кода (retry, fallback)
- Unchecked Exception если это fatal ошибка (circuit breaker, immediate failure)
- Custom Exception для контекста и деталей (больше информации для клиента)
Анализ требований сервиса бронирования
В сервисе бронирования ошибка подключения — это критическая ситуация:
// Сценарий: пользователь хочет забронировать комнату
// Нужно вызвать микросервис платежей
BookingService -> PaymentService
// Если платежный сервис недоступен:
// 1. Брондиование ДОЛЖНО провалиться (не начислять без платежа)
// 2. Клиент ДОЛЖЕН узнать (user-friendly ошибка)
// 3. Система ДОЛЖНА реагировать (логирование, алерты)
// 4. Возможен retry (если это временная ошибка)
Вариант 1: Checked Exception (IOException-подобный подход)
// Лучше всего для сервиса бронирования!
public class PaymentServiceException extends Exception {
private final String serviceName;
private final String endpoint;
private final int httpStatusCode;
private final Instant timestamp;
public PaymentServiceException(String message,
String serviceName,
String endpoint,
int httpStatusCode) {
super(message);
this.serviceName = serviceName;
this.endpoint = endpoint;
this.httpStatusCode = httpStatusCode;
this.timestamp = Instant.now();
}
public boolean isRetryable() {
// 503 Service Unavailable — можно пробовать снова
// 504 Gateway Timeout — можно пробовать снова
// 500 Internal Server Error — лучше не трогать
return httpStatusCode == 503 || httpStatusCode == 504;
}
}
// Использование
public class BookingService {
private final PaymentServiceClient paymentClient;
public Booking createBooking(BookingRequest request)
throws PaymentServiceException { // Явно декларирует ошибку
try {
PaymentResponse response = paymentClient.processPayment(
request.getPaymentDetails()
);
// Платеж прошел, создаем бронь
return new Booking(request, response.getTransactionId());
} catch (PaymentServiceException e) {
// Вызывающий код ДОЛЖЕН обработать это
// Иначе не скомпилируется
throw e;
}
}
}
// В контроллере
@PostMapping("/bookings")
public ResponseEntity<?> bookRoom(@RequestBody BookingRequest request) {
try {
Booking booking = bookingService.createBooking(request);
return ResponseEntity.status(HttpStatus.CREATED).body(booking);
} catch (PaymentServiceException e) {
// Обработаны все возможные ошибки явно
if (e.isRetryable()) {
// Попробовать еще раз или отправить в очередь
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new ErrorResponse("Payment service temporarily unavailable. Please try again later."));
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Payment processing failed. Contact support."));
}
}
}
Вариант 2: Unchecked Exception (RuntimeException-подход)
// Используй если ошибка не должна обрабатываться на каждом уровне
public class PaymentServiceUnavailableException extends RuntimeException {
private final String serviceName;
private final String endpoint;
private final Throwable cause;
public PaymentServiceUnavailableException(String message,
String serviceName,
String endpoint,
Throwable cause) {
super(message);
this.serviceName = serviceName;
this.endpoint = endpoint;
this.cause = cause;
}
public String getServiceName() {
return serviceName;
}
public String getEndpoint() {
return endpoint;
}
}
// Использование
public class BookingService {
private final PaymentServiceClient paymentClient;
public Booking createBooking(BookingRequest request) {
// Не нужно декларировать throws — unchecked
try {
PaymentResponse response = paymentClient.processPayment(
request.getPaymentDetails()
);
return new Booking(request, response.getTransactionId());
} catch (IOException e) {
// Преобразуем в unchecked
throw new PaymentServiceUnavailableException(
"Failed to connect to Payment Service: " + e.getMessage(),
"PaymentService",
"/api/v1/payments",
e
);
}
}
}
Вариант 3: Spring-специфичный подход
// Используй Spring'овые инструменты для микросервисов
import org.springframework.web.client.RestClientException;
import feign.FeignException;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
public class BookingService {
private final PaymentServiceClient paymentClient;
@CircuitBreaker(name = "paymentService",
fallbackMethod = "paymentFallback")
public Booking createBooking(BookingRequest request) {
PaymentResponse response = paymentClient.processPayment(
request.getPaymentDetails()
);
return new Booking(request, response.getTransactionId());
}
// Fallback при ошибке
private Booking paymentFallback(BookingRequest request,
Exception e) {
if (e instanceof FeignException.ServiceUnavailable) {
// 503 Service Unavailable
throw new PaymentServiceTemporarilyUnavailableException(
"Payment service is temporarily unavailable", e
);
} else if (e instanceof FeignException.BadGateway) {
// 502 Bad Gateway
throw new PaymentServiceGatewayException(
"Payment service gateway error", e
);
} else {
throw new PaymentServiceException(
"Payment service error: " + e.getMessage(), e
);
}
}
}
// Spring @ControllerAdvice для глобальной обработки
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(PaymentServiceTemporarilyUnavailableException.class)
public ResponseEntity<?> handlePaymentUnavailable(
PaymentServiceTemporarilyUnavailableException e) {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new ErrorResponse(
"Payment service temporarily unavailable. Please retry later.",
"PAYMENT_SERVICE_UNAVAILABLE",
System.currentTimeMillis()
));
}
@ExceptionHandler(PaymentServiceException.class)
public ResponseEntity<?> handlePaymentError(
PaymentServiceException e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(
"Payment processing failed. Contact support.",
"PAYMENT_SERVICE_ERROR",
System.currentTimeMillis()
));
}
}
Иерархия исключений (Recommended)
// Базовое исключение сервиса
public class BookingServiceException extends RuntimeException {
public BookingServiceException(String message) {
super(message);
}
public BookingServiceException(String message, Throwable cause) {
super(message, cause);
}
}
// Ошибки внешних сервисов
public class ExternalServiceException extends BookingServiceException {
private final String serviceName;
private final int httpStatus;
public ExternalServiceException(String message,
String serviceName,
int httpStatus) {
super(message);
this.serviceName = serviceName;
this.httpStatus = httpStatus;
}
public boolean isRetryable() {
return httpStatus >= 500; // 5xx ошибки — можно пробовать
}
}
// Специфичные ошибки платежей
public class PaymentServiceException extends ExternalServiceException {
public PaymentServiceException(String message,
String serviceName,
int httpStatus) {
super(message, serviceName, httpStatus);
}
}
public class PaymentServiceUnavailableException extends PaymentServiceException {
public PaymentServiceUnavailableException(String message) {
super(message, "PaymentService", 503);
}
}
public class PaymentServiceTimeoutException extends PaymentServiceException {
public PaymentServiceTimeoutException(String message) {
super(message, "PaymentService", 504);
}
}
Интеграция с Resilience4j
// Отличный инструмент для микросервисов
public class PaymentServiceClient {
private final RestTemplate restTemplate;
@Retry(name = "paymentRetry")
@CircuitBreaker(name = "paymentCircuitBreaker",
fallbackMethod = "fallback")
public PaymentResponse processPayment(PaymentRequest request) {
return restTemplate.postForObject(
"http://payment-service/api/v1/payments",
request,
PaymentResponse.class
);
}
private PaymentResponse fallback(PaymentRequest request,
Exception e) {
throw new PaymentServiceException(
"Payment service unavailable after retries",
"PaymentService",
503
);
}
}
// Конфигурация
# application.properties
resilience4j.retry.instances.paymentRetry.max-attempts=3
resilience4j.retry.instances.paymentRetry.wait-duration=1000ms
resilience4j.circuitbreaker.instances.paymentCircuitBreaker.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.paymentCircuitBreaker.wait-duration-in-open-state=10000ms
Логирование и мониторинг
public class BookingService {
private static final Logger logger = LoggerFactory.getLogger(BookingService.class);
public Booking createBooking(BookingRequest request) {
try {
logger.info("Processing booking for user: {}", request.getUserId());
return bookingRepository.save(createBookingEntity(request));
} catch (PaymentServiceException e) {
logger.error(
"Payment service failed for booking. Service: {}, Status: {}, User: {}",
e.getServiceName(),
e.getHttpStatus(),
request.getUserId(),
e
);
// Отправить в мониторинг
metrics.increment("booking.payment.service.error");
throw e;
}
}
}
Мой рекомендуемый подход
Для сервиса бронирования я выберу:
Unchecked Exception с иерархией, потому что:
- Ошибка подключения — это критическая ситуация, которая требует immediate fail, а не recovery на каждом уровне
- Spring автоматически обрабатывает через @ControllerAdvice, не нужно throws everywhere
- Поддержка Circuit Breaker через Resilience4j — стандарт для микросервисов
- Информативные исключения с контекстом (serviceName, httpStatus, timestamp)
- Гибкость — можно добавить retry логику на клиента без изменения signature
Итоговая структура
// Exceptions.java
public abstract class BookingServiceException extends RuntimeException {}
public class ExternalServiceException extends BookingServiceException {
private final String serviceName;
private final int httpStatus;
}
public class PaymentServiceUnavailableException extends ExternalServiceException {}
public class PaymentServiceTimeoutException extends ExternalServiceException {}
public class PaymentServiceAuthException extends ExternalServiceException {}
// Использование везде с @CircuitBreaker и глобальной обработкой
Этот подход масштабируется, информативен и легко интегрируется с современными инструментами микросервисной архитектуры.