← Назад к вопросам

Какой тип исключений бы выбрал при реализации сервиса бронирования, если не удается подключиться к микросервису?

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 с иерархией, потому что:

  1. Ошибка подключения — это критическая ситуация, которая требует immediate fail, а не recovery на каждом уровне
  2. Spring автоматически обрабатывает через @ControllerAdvice, не нужно throws everywhere
  3. Поддержка Circuit Breaker через Resilience4j — стандарт для микросервисов
  4. Информативные исключения с контекстом (serviceName, httpStatus, timestamp)
  5. Гибкость — можно добавить 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 и глобальной обработкой

Этот подход масштабируется, информативен и легко интегрируется с современными инструментами микросервисной архитектуры.

Какой тип исключений бы выбрал при реализации сервиса бронирования, если не удается подключиться к микросервису? | PrepBro