Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Service Clients

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, 504

Retry

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

On this page

  • Service Clients
  • Building a client
  • Builder methods
  • Making requests
  • Request
  • Response
  • Retry
  • Retry configuration
  • Custom retry logic
  • Circuit breaker
  • Circuit breaker configuration
  • Circuit breaker states
  • Interceptors
  • Transports
  • HTTP transport (default)
  • Connect transport
  • Custom transport
  • Full example
  • Related guides