Circuit Breaker for Resilient External API Calls
Wraps outbound HTTP calls with a Hystrix-like circuit breaker to stop hammering unhealthy dependencies.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
package main
import ( "context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/afex/hystrix-go/hystrix")
// ExternalAPIClient represents a client for calling an external API
type ExternalAPIClient struct {
baseURL string
timeout time.Duration
circuitName string // Unique name for the circuit breaker
}
// NewExternalAPIClient creates a new client with circuit breaker configuration
func NewExternalAPIClient(baseURL string, timeout time.Duration, circuitName string) *ExternalAPIClient {
// Configure circuit breaker settings
hystrix.ConfigureCommand(circuitName, hystrix.CommandConfig{
Timeout: int(timeout.Milliseconds()), // Request timeout
MaxConcurrentRequests: 100, // Max concurrent calls
RequestVolumeThreshold: 20, // Min requests to trigger circuit check
SleepWindow: 5000, // Time to wait before trying again after open
ErrorPercentThreshold: 50, // Error % to trigger circuit open
})
return &ExternalAPIClient{
baseURL: baseURL,
timeout: timeout,
circuitName: circuitName,
}
}
// APIResponse represents a sample response from the external API
type APIResponse struct {
Data string `json:"data"`
Status string `json:"status"`
}
// CallAPI makes a resilient API call using the circuit breaker
func (c *ExternalAPIClient) CallAPI(endpoint string) (*APIResponse, error) {
var response *APIResponse
// Wrap API call with circuit breaker
err := hystrix.Do(c.circuitName, func() error {
// Create HTTP request with timeout
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+endpoint, nil)
if err != nil {
return fmt.Errorf("failed to create request: %v", err)
}
// Send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %v", err)
}
defer resp.Body.Close()
// Check status code
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("api returned non-200 status: %d", resp.StatusCode)
}
// Parse response
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return fmt.Errorf("failed to parse response: %v", err)
}
return nil
}, func(err error) error {
// Fallback function: execute when circuit is open or request fails
log.Printf("Circuit breaker fallback triggered: %v", err)
// Return a default response or custom error
response = &APIResponse{
Data: "fallback data - service unavailable",
Status: "fallback",
}
return nil // Return nil to indicate fallback succeeded
})
if err != nil {
return nil, fmt.Errorf("circuit breaker error: %v", err)
}
return response, nil
}
// Example Usage
func main() {
// Create client for external API (e.g., a weather service)
client := NewExternalAPIClient(
"https://api.example.com",
3*time.Second,
"weather-api-circuit",
)
// Simulate repeated API calls to demonstrate circuit breaker behavior
for i := 0; i < 50; i++ {
resp, err := client.CallAPI("/weather?city=London")
if err != nil {
log.Printf("Call %d failed: %v", i+1, err)
} else {
log.Printf("Call %d succeeded: %+v", i+1, resp)
}
time.Sleep(500 * time.Millisecond)
}
}How It Works
Wraps outbound HTTP calls with a Hystrix-like circuit breaker to stop hammering unhealthy dependencies.
Defines a Hystrix command with timeout, error threshold, and request volume settings; executes HTTP requests through the command; logs fallback behavior and breaker state transitions.
Key Concepts
- 1Opens the circuit after consecutive failures or timeouts.
- 2Half-open probes allow recovery when the dependency heals.
- 3Timeouts and concurrency limits protect local resources.
When to Use This Pattern
- Calling flaky third-party APIs safely.
- Protecting microservices from cascading failures.
- Feature flagging risky integrations with a controllable breaker.
Best Practices
- Tune thresholds based on real latency and error metrics.
- Provide fallbacks or cached responses when the breaker is open.
- Instrument breaker state for dashboards and alerts.
Go Version1.16
Difficultyintermediate
Production ReadyYes
Lines of Code114