Skip to main content

Command Palette

Search for a command to run...

Always Design for Failure

Designing for failure —a necessity in today’s distributed, cloud-native world.

Updated
7 min read
Always Design for Failure
R

I'm technologist in love with almost all things tech from my daily job in the Cloud to my Master's in Cybersecurity and the journey all along.


Context

In a distributed system like a microservices architecture, failures are inevitable. Networks can fail, containers can crash, and services can become unresponsive. Designing for failure means building your system to handle these failures gracefully, ensuring that your application remains resilient, available, and responsive even when things go wrong.

Here’s how you can design for failure in your microservices and containerized applications:


1. Implement Retry Mechanisms

When a service call fails, it’s often due to a transient issue (e.g., network latency or temporary unavailability). Retry mechanisms allow your application to automatically retry the failed operation after a short delay.

How to Implement:

  • Use exponential backoff to gradually increase the delay between retries.

  • Set a maximum retry count to avoid infinite retries.

Example, Powershell

$retryCount = 3
$retryDelay = 2 # Delay in seconds between retries
$success = $false

for ($attempt = 1; $attempt -le $retryCount; $attempt++) {
    try {
        Write-Host "Attempt $attempt: Calling API..."
        $response = Invoke-RestMethod -Uri "https://api.example.com/data" -Method Get
        $success = $true
        Write-Host "API call succeeded!"
        break # Exit the loop if successful
    } catch {
        Write-Host "Attempt $attempt failed: $($_.Exception.Message)"
    }
}

Explanation:

  • Retry Count: The script retries the API call up to 3 times.

  • Retry Delay: It waits 2 seconds between retries.

  • Error Handling: Uses a try-catch block to handle exceptions.

  • Success Check: Breaks the loop if the API call succeeds.

2. Use Circuit Breakers

A circuit breaker is a pattern that prevents your application from repeatedly trying to call a failing service. If a service fails multiple times, the circuit breaker "trips" and stops further requests for a specified period, allowing the failing service to recover.

How to Implement:

  • Use libraries like Polly or Hystrix to implement circuit breakers.

  • Configure thresholds for failures (e.g., trip after 5 failures in 10 seconds).

  • Provide a fallback mechanism (e.g., return cached data or a default response) when the circuit is open.

Example:

import pybreaker
import requests

# Define a custom listener to log Circuit Breaker events
class LogListener(pybreaker.CircuitBreakerListener):
    def state_change(self, cb, old_state, new_state):
        print(f"Circuit Breaker state changed from {old_state.name} to {new_state.name}")

# Create a Circuit Breaker with the following settings:
# - fail_max: Trip after 3 failures
# - reset_timeout: Stay open for 10 seconds before allowing retries
breaker = pybreaker.CircuitBreaker(
    fail_max=3,  # Trip after 3 failures
    reset_timeout=10,  # Stay open for 10 seconds
    listeners=[LogListener()]  # Add the custom listener
)

# Wrap the function that makes the HTTP request with the Circuit Breaker
@breaker
def call_api():
    print("Calling API...")
    response = requests.get("https://api.example.com/data")
    response.raise_for_status()  # Raise an exception for HTTP errors
    return response.json()

# Simulate calling the API with error handling
for _ in range(5):  # Simulate 5 attempts
    try:
        data = call_api()
        print("API call succeeded! Response:", data)
    except pybreaker.CircuitBreakerError as e:
        print("Circuit Breaker is open. Request blocked:", str(e))
    except requests.RequestException as e:
        print("API call failed:", str(e))
    except Exception as e:
        print("Unexpected error:", str(e))
    finally:
        print("---")

Explanation of the Code

1. Circuit Breaker Configuration

  • fail_max=3: The Circuit Breaker trips after 3 consecutive failures.

  • reset_timeout=10: Once tripped, the Circuit Breaker stays open for 10 seconds before allowing retries.

  • listeners=[LogListener()]: A custom listener logs state changes (e.g., from closed to open).

2. Wrapping the Function

  • The @breaker decorator wraps the call_api function, applying the Circuit Breaker logic.

  • If the function fails, the failure count increases. After 3 failures, the Circuit Breaker trips.

3. Error Handling

  • pybreaker.CircuitBreakerError: Raised when the Circuit Breaker is open and blocks the request.

  • requests.RequestException: Raised for HTTP-related errors (e.g., connection issues, timeouts).

  • General Exception Handling: Catches any unexpected errors.

4. Simulating Requests

  • The loop simulates 5 attempts to call the API.

  • If the Circuit Breaker trips, further requests are blocked for 10 seconds.

Key Features of the Circuit Breaker

  1. Failure Detection: Detects repeated failures and trips the Circuit Breaker.

  2. State Management:

    • Closed: Normal operation; requests are allowed.

    • Open: Requests are blocked for a specified timeout period.

    • Half-Open: After the timeout, the Circuit Breaker allows a few requests to test if the service has recovered.

  3. Fallback Mechanism: You can implement a fallback (e.g., return cached data or a default response) when the Circuit Breaker is open.

3. Implement Fallback Mechanisms

When a service fails, a fallback mechanism ensures that your application can still provide a meaningful response to the user. This could be cached data, a default value, or a simplified version of the service.

How to Implement:

  • Use caching (e.g., Azure Redis Cache) to store frequently accessed data.

  • Provide default responses for critical services.

  • Use degraded functionality (e.g., disable non-essential features) during outages.


4. Use Timeouts

Timeouts prevent your application from waiting indefinitely for a response from a service. If a service doesn’t respond within the specified time, the request is aborted, and the application can take appropriate action (e.g., retry or fallback).

How to Implement:

  • Set reasonable timeout values for service calls.

  • Use libraries like Polly to enforce timeouts.

Example:

import requests

try:
    # Set a timeout of 5 seconds (connect and read timeout)
    response = requests.get("https://api.example.com/data", timeout=(3.05, 5))
    response.raise_for_status()  # Raise an exception for HTTP errors
    print("API call succeeded! Response:", response.json())
except requests.exceptions.Timeout:
    print("The request timed out.")
except requests.exceptions.RequestException as e:
    print("An error occurred:", str(e))

Explanation:

  • timeout=(3.05, 5):

    • The first value (3.05) is the connection timeout (time to establish a connection).

    • The second value (5) is the read timeout (time to wait for the server to send data).

  • Error Handling:

    • requests.exceptions.Timeout: Raised if the request times out.

    • requests.exceptions.RequestException: Catches other HTTP-related errors.

Key Considerations for Timeouts

  1. Appropriate Timeout Values: Choose timeout values based on the expected response time of the operation.

  2. Error Handling: Always handle timeout exceptions to avoid crashing your application.

  3. Resource Cleanup: Ensure resources are properly cleaned up if a timeout occurs.

  4. Fallback Mechanisms: Implement fallback logic (e.g., retries or default responses) for timed-out operations.

5. Monitor and Alert

Proactively monitoring your system helps you detect and respond to failures before they impact users. Use monitoring tools to track the health and performance of your services.

How to Implement:

  • Use Azure Monitor and Application Insights to collect telemetry data.

  • Set up alerts for key metrics (e.g., high error rates, slow response times).

  • Use dashboards to visualize the health of your system.

Example Metrics to Monitor:

  • Error rates.

  • Latency.

  • Request throughput.

  • Circuit breaker state.


6. Test for Failure

Regularly test your system’s resilience by simulating failures. This helps you identify weaknesses and ensure that your failure-handling mechanisms work as expected.

How to Implement:

  • Use chaos engineering tools like Azure Chaos Studio to inject failures (e.g., kill containers, simulate network latency).

  • Conduct failure drills to test your team’s response to outages.

Example Scenarios to Test:

  • Service unavailability.

  • Network latency or partition.

  • High CPU or memory usage.


7. Design for Redundancy

Ensure that your system can continue operating even if individual components fail. This involves deploying multiple instances of your services and using load balancing to distribute traffic.

How to Implement:

  • Use Azure Kubernetes Service (AKS) to deploy multiple replicas of your containers.

  • Use Azure Load Balancer or Traffic Manager to distribute traffic across instances.

  • Deploy services across multiple regions for geographic redundancy.


8. Use Asynchronous Communication

Synchronous communication between services can create tight coupling and increase the risk of cascading failures. Asynchronous communication (e.g., using message queues) decouples services and improves resilience.

How to Implement:

  • Use Azure Service Bus or Event Grid for asynchronous messaging.

  • Implement event-driven architectures to handle failures gracefully.

Example:

  • A service publishes an event to a message queue.

  • Another service processes the event when it’s ready, without waiting for a response.


Conclusion

  • Improves Resilience: Your application can handle failures without crashing.

  • Enhances User Experience: Users experience minimal disruption during outages.

  • Reduces Downtime: Failures are isolated, preventing cascading failures.

  • Supports Scalability: Resilient systems can scale more effectively.


More from this blog

U

Understand. Build. Conquer the Cloud

70 posts

No time for a novel? Here are my my Cloud Architect field notes: Distilling my complex cloud adventures into digestible TL;DRs.