Service Clients
go.putnami.dev/client provides a service client builder with retry, circuit breaker, and interceptor chain for resilient inter-service communication.
Building a client
import "go.putnami.dev/client"
c, err := client.NewBuilder().
BaseURL("http://users-api:3000").
ClientID("my-service").
Timeout(10 * time.Second).
Build()
if err != nil {
log.Fatal(err)
}
defer c.Close()Builder methods
| Method | Description |
|---|---|
BaseURL(url) |
Service base URL |
ClientID(id) |
Client identity (sent as X-Client-Id header) |
Timeout(d) |
Per-request timeout |
WithRetry(config) |
Enable retry with exponential backoff |
WithCircuitBreaker(config) |
Enable circuit breaker |
WithInterceptors(...) |
Add custom interceptors |
WithTransport(t) |
Use a custom transport |
Build() |
Build the client |
Making requests
resp, err := c.Do(ctx, &client.Request{
Method: "GET",
Path: "/api/users/123",
Headers: map[string]string{"Accept": "application/json"},
Query: map[string]string{"include": "orders"},
})
if err != nil {
return err
}
if resp.IsSuccess() {
var user User
json.Unmarshal(resp.Body, &user)
}Request
type Request struct {
Method string
Path string
Headers map[string]string
Query map[string]string
Body []byte
}Response
type Response struct {
StatusCode int
Headers map[string]string
Body []byte
}
resp.IsSuccess() // true for 2xx
resp.IsRetryable() // true for 408, 429, 500, 502, 503, 504Retry
Enable automatic retry with exponential backoff for transient failures:
c, _ := client.NewBuilder().
BaseURL("http://users-api:3000").
WithRetry(client.RetryConfig{
MaxRetries: 3,
BaseDelay: 100 * time.Millisecond,
MaxDelay: 5 * time.Second,
}).
Build()Retry configuration
| Field | Type | Default | Description |
|---|---|---|---|
MaxRetries |
int |
— | Maximum retry attempts |
BaseDelay |
time.Duration |
— | Initial delay between retries |
MaxDelay |
time.Duration |
— | Maximum delay cap |
RetryableFunc |
func(*Response, error) bool |
— | Custom retry logic |
By default, the following status codes trigger retries: 408, 429, 500, 502, 503, 504.
Custom retry logic
client.RetryConfig{
MaxRetries: 3,
BaseDelay: time.Second,
RetryableFunc: func(resp *client.Response, err error) bool {
if err != nil {
return true // retry on network errors
}
return resp.StatusCode == 503 // only retry 503
},
}Circuit breaker
Protect against cascading failures by opening the circuit after repeated errors:
c, _ := client.NewBuilder().
BaseURL("http://users-api:3000").
WithCircuitBreaker(client.CircuitBreakerConfig{
FailureThreshold: 5,
ResetTimeout: 30 * time.Second,
SuccessThreshold: 2,
}).
Build()Circuit breaker configuration
| Field | Type | Default | Description |
|---|---|---|---|
FailureThreshold |
int |
— | Consecutive failures before opening |
ResetTimeout |
time.Duration |
— | Time before transitioning to half-open |
SuccessThreshold |
int |
— | Successes in half-open to close |
FailureStatuses |
[]int |
— | Status codes counted as failures |
Circuit breaker states
Closed → (failures reach threshold) → Open
Open → (reset timeout expires) → Half-Open
Half-Open → (successes reach threshold) → Closed
Half-Open → (any failure) → Open- Closed — requests flow normally, failures are counted
- Open — requests fail immediately without calling the downstream service
- Half-Open — limited requests are allowed through to test recovery
Interceptors
Add custom logic to the request/response pipeline:
type Interceptor func(req *Request, next func(*Request) (*Response, error)) (*Response, error)// Logging interceptor
loggingInterceptor := func(req *client.Request, next func(*client.Request) (*client.Response, error)) (*client.Response, error) {
start := time.Now()
resp, err := next(req)
duration := time.Since(start)
fmt.Printf("%s %s → %d (%dms)\n", req.Method, req.Path, resp.StatusCode, duration.Milliseconds())
return resp, err
}
c, _ := client.NewBuilder().
BaseURL("http://users-api:3000").
WithInterceptors(loggingInterceptor).
Build()The interceptor chain handles client ID headers, retry with exponential backoff, and circuit breaker state management.
Transports
HTTP transport (default)
client.NewBuilder().
WithTransport(client.NewHTTPTransport(client.HTTPTransportConfig{})).
Build()Connect transport
For calling gRPC services via the Connect protocol:
client.NewBuilder().
WithTransport(client.NewConnectTransport(client.ConnectTransportConfig{})).
Build()Custom transport
Implement the Transport interface for custom protocols:
type Transport interface {
RoundTrip(ctx context.Context, req *Request) (*Response, error)
Close() error
}Full example
c, err := client.NewBuilder().
BaseURL("http://users-api:3000").
ClientID("order-service").
Timeout(10 * time.Second).
WithRetry(client.RetryConfig{
MaxRetries: 3,
BaseDelay: 200 * time.Millisecond,
MaxDelay: 5 * time.Second,
}).
WithCircuitBreaker(client.CircuitBreakerConfig{
FailureThreshold: 5,
ResetTimeout: 30 * time.Second,
SuccessThreshold: 2,
}).
Build()
if err != nil {
log.Fatal(err)
}
defer c.Close()
resp, err := c.Do(ctx, &client.Request{
Method: "GET",
Path: "/api/users/123",
})
if err != nil {
// Network error or circuit breaker open
return err
}
if !resp.IsSuccess() {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}Related guides
- HTTP & Middleware — server-side HTTP
- gRPC & Connect — gRPC services
- Telemetry — client tracing