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