Table of contents

  1. Overview
  2. Pattern 1: Simple (most services)
  3. Pattern 2: Block in PreStart
  4. Pattern 3: Worker-managed readiness
  5. Pattern 4: Dynamic workers from DB
  6. Choosing a Pattern
  7. Related

Overview

Readiness is a service-level concern. ColdBrew provides the primitives (CBGracefulStopper, CBPreStarter, CBWorkerProvider) and your service decides when it is ready to accept traffic. Kubernetes readiness probes respect this — traffic is only routed to pods that report SERVING.

This page describes four common patterns, from simplest to most advanced.

Pattern 1: Simple (most services)

Service becomes ready during initialization. No workers, no external dependencies to wait for.

type svc struct {
    *health.Server
}

func (s *svc) InitGRPC(ctx context.Context, server *grpc.Server) error {
    pb.RegisterMyServiceServer(server, s)
    return nil
}

In the cookiecutter template, readiness is managed via SetReady() / SetNotReady():

func New(cfg config.Config) (*svc, error) {
    s := &svc{Server: GetHealthCheckServer()}
    SetReady() // service starts accepting traffic
    return s, nil
}

Graceful shutdown calls FailCheck(true), which calls SetNotReady(). Kubernetes stops routing new requests during the drain period.

When to use: Services with no background workers or external dependencies that must be established before serving.

Pattern 2: Block in PreStart

Use when a dependency (database, message broker, cache) must be ready before accepting traffic. CBPreStarter blocks server startup until PreStart returns.

var _ core.CBPreStarter = (*cbSvc)(nil)

func (s *cbSvc) PreStart(ctx context.Context) error {
    // Servers won't start until this returns.
    // Returning an error aborts startup entirely.
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        return fmt.Errorf("database connect: %w", err)
    }
    if err := db.PingContext(ctx); err != nil {
        db.Close()
        return fmt.Errorf("database ping: %w", err)
    }
    s.db = db
    return nil
}

PreStart runs before initGRPC / initHTTP, so you can also register auth interceptors or other setup here:

func (s *cbSvc) PreStart(ctx context.Context) error {
    auth.Setup(ctx, config.Get().AuthConfig)
    return nil
}

For configuration that has an environment variable equivalent (like GRPC_SERVER_DEFAULT_TIMEOUT_IN_SECONDS), prefer the env var via the Configuration Reference. Use PreStart for setup that requires code — auth interceptors, database connections, programmatic interceptor configuration via interceptors.Set*() functions.

When to use: Database connections, message broker connections, mandatory cache warmup, auth interceptor registration. If the dependency fails to connect, the service should not start.

Pattern 3: Worker-managed readiness

Use when workers can start independently and the service becomes ready once workers report in. Servers start immediately but the health check returns NOT_SERVING until all components are ready.

var (
    _ core.CBWorkerProvider  = (*svc)(nil)
    _ core.CBGracefulStopper = (*svc)(nil)
)

type svc struct {
    *health.Server
    kafkaReady atomic.Bool
    cacheReady atomic.Bool
}

func (s *svc) Workers() []*workers.Worker {
    return []*workers.Worker{
        workers.NewWorker("kafka").HandlerFunc(s.consumeKafka),
        workers.NewWorker("cache-warmer").
            HandlerFunc(s.warmCache).
            Every(5 * time.Minute),
    }
}

func (s *svc) consumeKafka(ctx context.Context, info *workers.WorkerInfo) error {
    consumer, err := kafka.Connect(ctx, s.brokers)
    if err != nil {
        return err // triggers worker restart with backoff
    }
    s.kafkaReady.Store(true)
    defer s.kafkaReady.Store(false)
    return consumer.Consume(ctx)
}

func (s *svc) warmCache(ctx context.Context, info *workers.WorkerInfo) error {
    if err := s.cache.WarmAll(ctx); err != nil {
        return err
    }
    s.cacheReady.Store(true)
    return nil
}

The health check aggregates readiness from all components:

func (s *svc) ReadyCheck(ctx context.Context, _ *emptypb.Empty) (*httpbody.HttpBody, error) {
    if s.kafkaReady.Load() && s.cacheReady.Load() {
        return readyResponse, nil
    }
    return notReadyResponse, ErrNotReady
}

During graceful shutdown, FailCheck(true) forces NOT_SERVING regardless of worker state:

func (s *svc) FailCheck(fail bool) {
    if fail {
        s.kafkaReady.Store(false)
        s.cacheReady.Store(false)
    }
}

When to use: Services that can start the gRPC/HTTP servers while background workers are still initializing. Kubernetes readiness probes hold traffic until all components report ready.

Pattern 4: Dynamic workers from DB

Use the manager pattern — one static worker reconciles dynamic children from an external source (database, config service, feature flags).

func (s *svc) Workers() []*workers.Worker {
    return []*workers.Worker{
        workers.NewWorker("reconciler").
            HandlerFunc(s.reconcileWorkers).
            Every(30 * time.Second),
    }
}

func (s *svc) reconcileWorkers(ctx context.Context, info *workers.WorkerInfo) error {
    configs, err := s.db.GetWorkerConfigs(ctx)
    if err != nil {
        return err
    }

    // Add new workers from DB
    for _, cfg := range configs {
        if _, exists := info.GetChild(cfg.Name); !exists {
            info.Add(
                workers.NewWorker(cfg.Name).
                    HandlerFunc(cfg.BuildHandler(s.deps)).
                    Every(cfg.Interval).
                    WithJitter(10),
            )
        }
    }

    // Remove workers no longer in DB
    for _, name := range info.GetChildren() {
        if !existsInConfigs(name, configs) {
            info.Remove(name)
        }
    }
    return nil
}

When the reconciler stops (parent shutdown), all dynamic children stop automatically — scoped lifecycle via the suture supervisor tree.

For readiness, combine with Pattern 3: the reconciler sets a ready flag after the first successful reconciliation.

When to use: Multi-tenant workers, feature-flagged background jobs, queue-per-customer patterns. See Dynamic Workers in the workers howto for the full API.

Choosing a Pattern

Pattern Blocks startup? Workers? Complexity
Simple No No Trivial
PreStart Yes Optional Low
Worker-managed No Yes Medium
Dynamic No Yes (dynamic) Higher

Most services start with Pattern 1 or 2. Add Pattern 3 when you introduce background workers that affect readiness. Pattern 4 is for advanced multi-tenant or config-driven scenarios.

  • Workers — full workers howto (middleware, jitter, dynamic children, metrics)
  • Shutdown Lifecycle — how ColdBrew handles SIGTERM, FailCheck, drain period
  • Production Checklist — readiness probes, resource limits, HPA configuration