Chain Composition
NewChain
The Chain struct composes multiple invokers into a single execution pipeline:
chain := invoker.NewChain(
telemetry.NewTracerInvoker(tracer),
invoker.NewMetrics(metrics),
invoker.NewRateLimiter(store, cfg, metrics),
invoker.NewIdempotency(store, cfg, metrics),
invoker.NewRetry(policy, metrics),
invoker.NewCircuitBreaker(cfg, metrics),
invoker.NewDLQ(publisher, metrics),
)
Recursion Model
The chain executes via recursive function calls. Each invoker receives a next function that triggers the next invoker in the sequence:
func (c *Chain) Invoke(
ctx context.Context,
evt event.Event,
handlerName string,
handle func(context.Context) error,
) error {
var next func(i int, ctx context.Context) error
next = func(i int, ctx context.Context) error {
if i == len(c.invokers) {
return handle(ctx)
}
return c.invokers[i].Invoke(ctx, evt, handlerName, func(ctx context.Context) error {
return next(i+1, ctx)
})
}
return next(0, ctx)
}
This means:
- Each invoker controls whether
nextis called - Context can be enriched at each layer
- Errors propagate back through the chain
- An invoker can call
nextmultiple times (e.g., retry)
Recommended Ordering
- Tracing - Creates spans, must wrap everything
- Metrics - Records latency and outcomes
- Rate Limiting - Cheap rejection, fail fast
- Idempotency - Cheap rejection, prevent duplicates
- Retry - Handle transient failures
- Circuit Breaker - Protect downstream services
- DLQ - Terminal handler for permanent failures
Why this order?
- Observability wraps everything for complete visibility
- Cheap rejections happen early to save resources
- Retries occur before circuit breaker health decisions
- DLQ is terminal and isolated as the last resort