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
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  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