Recipe: Library Resilience
When building a library where the domain type T is known, you can expose pipz resilience patterns to consumers via a clean With* functional options API.
The Pattern
This recipe demonstrates how zyn (an LLM orchestration library) uses pipz to give consumers composable resilience without exposing pipz internals.
Core Concept
// The Option type wraps a pipeline with additional behavior
type Option func(pipz.Chainable[*Request]) pipz.Chainable[*Request]
// Each With* function returns an Option that wraps the pipeline
func WithRetry(attempts int) Option {
return func(p pipz.Chainable[*Request]) pipz.Chainable[*Request] {
return pipz.NewRetry(retryID, p, attempts)
}
}
Consumers compose options at construction time:
client, _ := mylib.NewClient(
mylib.WithRetry(3),
mylib.WithTimeout(10*time.Second),
mylib.WithCircuitBreaker(5, 30*time.Second),
)
The result is a pipeline: CircuitBreaker(Timeout(Retry(base)))
Implementation
Define Your Request Type
// Your library's request type
type Request struct {
Input interface{}
Output interface{}
Metadata map[string]string
}
Define Resilience Identities
// Package-level identities for each resilience pattern
var (
retryID = pipz.NewIdentity("mylib:retry", "Retries failed requests")
backoffID = pipz.NewIdentity("mylib:backoff", "Retries with exponential backoff")
timeoutID = pipz.NewIdentity("mylib:timeout", "Enforces request timeout")
circuitBreakerID = pipz.NewIdentity("mylib:circuit-breaker", "Prevents cascading failures")
rateLimitID = pipz.NewIdentity("mylib:rate-limit", "Limits request rate")
fallbackID = pipz.NewIdentity("mylib:fallback", "Falls back to alternative on failure")
errorHandlerID = pipz.NewIdentity("mylib:error-handler", "Handles request errors")
)
Define the Option Type
// Option wraps a pipeline with additional behavior
type Option func(pipz.Chainable[*Request]) pipz.Chainable[*Request]
Implement With* Functions
// WithRetry retries failed requests up to maxAttempts times
func WithRetry(maxAttempts int) Option {
return func(p pipz.Chainable[*Request]) pipz.Chainable[*Request] {
return pipz.NewRetry(retryID, p, maxAttempts)
}
}
// WithBackoff retries with exponential backoff
func WithBackoff(maxAttempts int, baseDelay time.Duration) Option {
return func(p pipz.Chainable[*Request]) pipz.Chainable[*Request] {
return pipz.NewBackoff(backoffID, p, maxAttempts, baseDelay)
}
}
// WithTimeout enforces a maximum request duration
func WithTimeout(duration time.Duration) Option {
return func(p pipz.Chainable[*Request]) pipz.Chainable[*Request] {
return pipz.NewTimeout(timeoutID, p, duration)
}
}
// WithCircuitBreaker prevents cascading failures
func WithCircuitBreaker(failures int, recovery time.Duration) Option {
return func(p pipz.Chainable[*Request]) pipz.Chainable[*Request] {
return pipz.NewCircuitBreaker(circuitBreakerID, p, failures, recovery)
}
}
// WithRateLimit limits request rate
func WithRateLimit(rps float64, burst int) Option {
return func(p pipz.Chainable[*Request]) pipz.Chainable[*Request] {
return pipz.NewRateLimiter(rateLimitID, rps, burst, p)
}
}
// WithFallback uses an alternative client on failure
func WithFallback(fallback Client) Option {
return func(p pipz.Chainable[*Request]) pipz.Chainable[*Request] {
return pipz.NewFallback(fallbackID, p, fallback.pipeline())
}
}
// WithErrorHandler provides custom error handling
func WithErrorHandler(handler pipz.Chainable[*pipz.Error[*Request]]) Option {
return func(p pipz.Chainable[*Request]) pipz.Chainable[*Request] {
return pipz.NewHandle(errorHandlerID, p, handler)
}
}
Apply Options in Constructor
// Client is your library's main type
type Client struct {
pipe pipz.Chainable[*Request]
}
// NewClient creates a client with optional resilience configuration
func NewClient(backend Backend, opts ...Option) *Client {
// Create the base pipeline (your core logic)
base := newTerminal(backend)
// Apply each option, wrapping the pipeline
pipe := pipz.Chainable[*Request](base)
for _, opt := range opts {
pipe = opt(pipe)
}
return &Client{pipe: pipe}
}
// Execute processes a request through the configured pipeline
func (c *Client) Execute(ctx context.Context, req *Request) (*Request, error) {
return c.pipe.Process(ctx, req)
}
// pipeline exposes the internal pipeline for composition (e.g., WithFallback)
func (c *Client) pipeline() pipz.Chainable[*Request] {
return c.pipe
}
Create the Base Terminal
var terminalID = pipz.NewIdentity("mylib:terminal", "Executes request against backend")
func newTerminal(backend Backend) pipz.Chainable[*Request] {
return pipz.Apply(terminalID, func(ctx context.Context, req *Request) (*Request, error) {
result, err := backend.Call(ctx, req.Input)
if err != nil {
return req, err
}
req.Output = result
return req, nil
})
}
Usage Examples
Basic Retry
client := mylib.NewClient(backend,
mylib.WithRetry(3),
)
Production Configuration
client := mylib.NewClient(backend,
mylib.WithRetry(3),
mylib.WithTimeout(10*time.Second),
mylib.WithCircuitBreaker(5, 30*time.Second),
mylib.WithRateLimit(100, 10),
)
Options are applied in order, creating: RateLimit(CircuitBreaker(Timeout(Retry(terminal))))
With Fallback
primary := mylib.NewClient(primaryBackend,
mylib.WithTimeout(5*time.Second),
)
fallback := mylib.NewClient(fallbackBackend,
mylib.WithTimeout(10*time.Second),
)
resilient := mylib.NewClient(primaryBackend,
mylib.WithTimeout(5*time.Second),
mylib.WithFallback(fallback),
)
Real-World Example: zyn
zyn implements this pattern for LLM operations:
// From zyn - creating a classifier with resilience
classifier, _ := zyn.Classification(
"What type of email is this?",
[]string{"spam", "urgent", "newsletter", "personal"},
provider,
zyn.WithRetry(3),
zyn.WithTimeout(10*time.Second),
zyn.WithCircuitBreaker(5, 30*time.Second),
)
// Execute with full resilience
category, _ := classifier.Fire(ctx, session, "URGENT: Your account suspended!")
zyn provides 8 synapse types (Binary, Classification, Extraction, Transform, etc.), each accepting the same ...Option parameter. The resilience layer is completely orthogonal to the LLM logic.
Benefits
- Clean API — Consumers see
WithRetry(3), notpipz.NewRetry(id, p, 3) - Composable — Options combine naturally in any order
- Encapsulated — pipz is an implementation detail, not a public dependency
- Type-Safe — The generic
Tis fixed to your domain type - Testable — Each option can be tested in isolation
Key Insight
By fixing pipz.Chainable[T] to your domain type (*Request, *SynapseRequest, etc.), you create a resilience vocabulary specific to your library. Consumers get production-grade resilience without learning pipz.
See Also
- zyn source — Complete implementation of this pattern
- Building Pipelines — Application-level pipeline construction
- Extensible Vocabulary — Creating domain-specific APIs