Graceful Degradation
Degradation Strategies
// Service unavailable - fallback to cache
class ProductPriceService
{
public function getPrice(ProductId $productId): Money
{
try {
return $this->priceRepository->get($productId);
} catch (ServiceUnavailableException $e) {
// Fallback to cached price
return $this->cache->get('price_' . $productId->getValue()) ?? new Money(0, 'USD');
}
}
}
Feature Degradation
// Disable non-critical features during failure
class RecommendationService
{
public function getRecommendations(ProductId $productId): array
{
try {
return $this->mlService->getRecommendations($productId);
} catch (Exception $e) {
// Degrade: return empty recommendations
$this->logger->warning('ML service unavailable, degrading recommendations');
return [];
}
}
}
Content Degradation
// Show cached/static content when dynamic fails
class ProductPageController
{
public function execute()
{
try {
$product = $this->productService->getProduct($id);
return $this->renderDynamic($product);
} catch (Exception $e) {
// Fallback to cached page
$cached = $this->cache->get('product_page_' . $id);
if ($cached) {
return $this->renderCached($cached);
}
throw $e;
}
}
}
Key Takeaway
Graceful degradation provides reduced functionality when dependencies fail. Use cache fallbacks, disable non-critical features, and show cached content.
Error Boundaries
Module Error Boundaries
// Isolate failures by module
class OrderModule
{
public function processOrder(Order $order): Result
{
try {
$result = $this->orderService->process($order);
return Result::success($result);
} catch (Exception $e) {
$this->logger->error('Order processing failed', [
'order_id' => $order->getId(),
'error' => $e->getMessage()
]);
return Result::failure('Order processing temporarily unavailable');
}
}
}
Queue Error Boundaries
// Message queue error isolation
class OrderMessageHandler
{
public function handle(Message $message): void
{
try {
$order = $this->serializer->unserialize($message->getBody());
$this->orderProcessor->process($order);
} catch (DeserializationException $e) {
// Message format error - don't retry
$this->deadLetterQueue->send($message);
} catch (BusinessException $e) {
// Business error - don't retry
$this->logger->warning('Business error', ['error' => $e->getMessage()]);
} catch (Exception $e) {
// Transient error - retry
throw $e;
}
}
}
API Error Boundaries
// REST API error isolation
class ApiErrorHandler
{
public function handleException(Exception $e): JsonResponse
{
return match(true) {
$e instanceof AuthenticationException =>
new JsonResponse(['error' => 'Unauthorized'], 401),
$e instanceof AuthorizationException =>
new JsonResponse(['error' => 'Forbidden'], 403),
$e instanceof NotFoundException =>
new JsonResponse(['error' => 'Not Found'], 404),
$e instanceof ValidationException =>
new JsonResponse(['error' => $e->getErrors()], 422),
default =>
new JsonResponse(['error' => 'Internal Server Error'], 500),
};
}
}
Key Takeaway
Error boundaries isolate failures to specific modules or services. Differentiate between transient and permanent errors for appropriate handling.
Retry Strategies
Exponential Backoff
// Exponential backoff retry
class RetryService
{
public function retry(callable $fn, int $maxRetries = 3): mixed
{
$attempt = 0;
while ($attempt < $maxRetries) {
try {
return $fn();
} catch (Exception $e) {
$attempt++;
if ($attempt >= $maxRetries) {
throw $e;
}
// Exponential backoff with jitter
$delay = (2 ** $attempt) * 1000; // 2s, 4s, 8s
$jitter = random_int(0, 1000);
usleep(($delay + $jitter) * 1000);
}
}
}
}
Retry Configuration
// Retry strategy configuration
$retryConfig = [
'payment_service' => [
'max_retries' => 3,
'base_delay' => 1000, // ms
'max_delay' => 30000, // ms
'backoff_multiplier' => 2,
'retryable_exceptions' => [
ConnectionException::class,
TimeoutException::class,
],
],
'inventory_service' => [
'max_retries' => 2,
'base_delay' => 500,
'retryable_exceptions' => [
ServiceUnavailableException::class,
],
],
];
Idempotent Retries
// Ensure idempotency for retries
class IdempotentPaymentService
{
public function processPayment(PaymentRequest $request): PaymentResult
{
$idempotencyKey = $request->getIdempotencyKey();
// Check if already processed
$existing = $this->paymentRepository->getByIdempotencyKey($idempotencyKey);
if ($existing) {
return $existing;
}
// Process payment
$result = $this->paymentGateway->charge($request);
// Store with idempotency key
$this->paymentRepository->save($result, $idempotencyKey);
return $result;
}
}
Key Takeaway
Use exponential backoff with jitter for retries. Configure retryable exceptions. Ensure idempotency for safe retries.
Circuit Breaker Pattern
Circuit Breaker States
Closed (Normal) -> Open (Failing) -> Half-Open (Testing)
| |
+------------------------------------+
(Success)
Circuit Breaker Implementation
class CircuitBreaker
{
private int $failureCount = 0;
private int $successCount = 0;
private string $state = 'closed';
private \DateTimeImmutable $lastFailureTime;
private const FAILURE_THRESHOLD = 5;
private const SUCCESS_THRESHOLD = 3;
private const TIMEOUT = 60; // seconds
public function call(callable $fn): mixed
{
if ($this->state === 'open') {
if ($this->shouldTryReset()) {
$this->state = 'half-open';
} else {
throw new CircuitOpenException('Service unavailable');
}
}
try {
$result = $fn();
$this->onSuccess();
return $result;
} catch (Exception $e) {
$this->onFailure();
throw $e;
}
}
private function onSuccess(): void
{
$this->failureCount = 0;
$this->successCount++;
if ($this->state === 'half-open' && $this->successCount >= self::SUCCESS_THRESHOLD) {
$this->state = 'closed';
}
}
private function onFailure(): void
{
$this->failureCount++;
$this->successCount = 0;
$this->lastFailureTime = new \DateTimeImmutable();
if ($this->failureCount >= self::FAILURE_THRESHOLD) {
$this->state = 'open';
}
}
private function shouldTryReset(): bool
{
return (time() - $this->lastFailureTime->getTimestamp()) >= self::TIMEOUT;
}
}
Circuit Breaker Usage
class PaymentService
{
private CircuitBreaker $circuitBreaker;
public function processPayment(PaymentRequest $request): PaymentResult
{
return $this->circuitBreaker->call(function () use ($request) {
return $this->paymentGateway->charge($request);
});
}
}
Key Takeaway
Circuit breaker prevents cascading failures. Open state fails fast. Half-open tests recovery. Closed state is normal operation.
Quiz
1. What is graceful degradation?
2. What is exponential backoff?
3. What states does a circuit breaker have?
4. Why ensure idempotency for retries?
5. What is an error boundary?
Flashcards
Question
What is graceful degradation?
Click to reveal answer
Answer
Reduced functionality when dependencies fail, using cache fallbacks
Question
What is exponential backoff?
Click to reveal answer
Answer
Increasing delay between retries: 2s, 4s, 8s with jitter
Question
Circuit breaker states?
Click to reveal answer
Answer
Closed (normal), Open (failing), Half-Open (testing)
Question
Why idempotency?
Click to reveal answer
Answer
Prevent duplicate operations when retrying
Question
What is an error boundary?
Click to reveal answer
Answer
Isolates failures to specific modules to prevent cascading
Question
When to use circuit breaker?
Click to reveal answer
Answer
For external service calls that may fail or timeout
Revision Notes
Key Takeaways
- 1. Graceful degradation provides reduced functionality during failures
- 2. Error boundaries isolate failures to specific modules
- 3. Exponential backoff with jitter for retry strategies
- 4. Circuit breaker prevents cascading failures
- 5. Ensure idempotency for safe retries
Interview Tips
- • Explain graceful degradation strategies
- • Describe circuit breaker pattern implementation
- • Discuss retry strategies with backoff
- • Explain error boundary design
Cheat Sheet
Failure Handling
Graceful Degradation:
Cache fallbacks
Disable non-critical features
Show cached content
Error Boundaries:
Isolate per module
Differentiate error types
Prevent cascading
Retry Strategies:
Exponential backoff
Jitter for randomness
Idempotent operations
Circuit Breaker:
Closed: Normal operation
Open: Failing, fail fast
Half-Open: Testing recovery