Dapr — Patterns#

Concurrency patterns#

Structured concurrency via RunnerCloserManager#

  • Usage: Every binary (daprd, placement, sentry, scheduler, operator, injector) uses concurrency.NewRunnerManager or concurrency.NewRunnerCloserManager from dapr/kit to compose long-running subsystems.
  • Example: cmd/daprd/app/app.go:161 — runtime, HTTP server, gRPC servers all registered as func(ctx context.Context) error runners; pkg/actors/actors.go:268 — actor subsystems similarly composed
  • Assessment: Sophisticated. This is the project’s most distinctive concurrency discipline. All goroutines are structured: they receive a context, return an error, and closers execute in reverse order. Prevents goroutine leaks and makes shutdown sequencing explicit. Far superior to ad-hoc go func() for long-running services.

Goroutine spawning (368 occurrences)#

  • Usage: Widespread anonymous goroutine spawning for one-off background work. 368 go func calls outside vendor.
  • Example: pkg/runtime/processor/ — component init fanout; pkg/resiliency/policy.go:122 — background timeout goroutine with make(chan doneCh[T], 1) for result handoff
  • Assessment: Idiomatic and appropriate given Dapr’s event-driven nature. The higher-risk long-lived goroutines are managed via RunnerCloserManager; the short-lived fire-and-forward ones are typical for callback and event delivery.

Channel-based signalling (552 select {} occurrences, ~10 make(chan patterns)#

  • Usage: Channels are used for lifecycle coordination (ready signals, shutdown signals, error propagation) rather than data streaming.
  • Examples:
    • cmd/placement/app/app.go:82placeReady := make(chan struct{}) for readiness handoff
    • cmd/sentry/app/app.go:66issuerEvent = make(chan struct{}) for cert renewal coordination
    • pkg/resiliency/policy.go:122make(chan doneCh[T], 1) for returning generic result from timeout-wrapped goroutine
  • Assessment: Correct. Channels used for signal/rendezvous, not for throughput. Unbuffered or capacity-1 channels dominate.

errgroup for parallel fan-out#

  • Usage: Used selectively for structured parallel operations.
  • Example: pkg/runtime/pubsub/default_bulkpub.go:49 — parallel bulk publish to multiple topics using errgroup.Group; integration test tests/integration/suite/daprd/workflow/listener/multi.go:82
  • Assessment: Used appropriately and sparingly — only where coordinated parallel work with error aggregation is genuinely needed.

sync.Pool for object reuse#

  • Usage: Object pooling in hot paths — scheduler stream event processing and actor workflow factories.
  • Examples:
    • pkg/scheduler/server/internal/pool/loops/stream/stream.go:34 — pools event stream structs to avoid per-event allocation
    • pkg/actors/targets/workflow/orchestrator/factory.go:36, activity/factory.go:32 — pools workflow actor execution objects
    • pkg/actors/internal/placement/loops/disseminator/inflight/inflight.go:35 — pools in-flight placement dissemination records
  • Assessment: Targeted and justified — applied in tight event-processing loops, not speculatively.

Context propagation (4702 context.Context references)#

  • Usage: Pervasive. Every operation, component call, gRPC invocation, and actor interaction passes a context.Context. Context cancellation drives the entire shutdown sequence.
  • Example: pkg/resiliency/policy.go:119context.WithTimeout(ctx, def.t) wraps each retried operation; context.WithCancelCause used in scheduler pool for structured error cause propagation
  • Assessment: Exemplary. Context is the backbone of all cancellation. context.WithCancelCause (Go 1.20) appears in newer subsystems, allowing richer shutdown diagnostics.

Graceful shutdown#

  • Usage: SIGHUP triggers in-process restart (not full process exit) in daprd; SIGTERM triggers RunnerCloserManager teardown. DaprGracefulShutdownSeconds is a configurable flag.
  • Example: cmd/daprd/app/app.go:123 — comment distinguishes SIGHUP (restart) from SIGTERM (exit); cmd/daprd/options/options.go:161 — configurable dapr-block-shutdown-duration
  • Assessment: Production-grade. SIGHUP restart without pod restart is a Kubernetes optimization that avoids connection disruption during rolling updates.

Worker pool for scheduler#

  • Usage: Scheduler service uses a configurable number of workers (--workers, default 2048) to process job events in parallel.
  • Example: cmd/scheduler/options/options.go:136 — detailed flag description explaining tuning tradeoffs
  • Assessment: Explicit and well-documented. Tunable to I/O vs CPU bottleneck.

Error handling#

  • Style: Mixed — sentinel errors for conditions, fmt.Errorf with %w for wrapping, custom struct types for rich API errors. Overall pragmatic rather than dogmatic.
  • Error types defined:
    • pkg/resiliency/retry.go:39CodeError struct for gRPC status code propagation through retry policies
    • pkg/api/errors/pubsub.go:27–37PubSubError, PubSubMetadataError, PubSubTopicError with component name and topic context
    • pkg/api/errors/state.go:28StateStoreError for state operation failures
    • pkg/actors/errors/errors.go:32ActorError for actor-specific failures with actor type/ID metadata
    • pkg/api/http/errors.go:23ErrorResponse struct for HTTP JSON error body serialization
  • Wrapping approach: fmt.Errorf("...: %w", err) throughout. No pkg/errors dependency. Stdlib errors.Is / errors.As used for unwrapping.
  • Examples:
    • cmd/scheduler/options/options.go:162errors.New("kubeconfig flag is only valid in --mode=kubernetes") — validation at boundary
    • pkg/api/errors/pubsub.go:221 — structured PubSubError.Build() that produces a gRPC status.Status with metadata — errors are protocol-aware
    • pkg/resiliency/policy.go:211retry.NotifyRecoverWithData callback on retry state transitions for observability

Configuration pattern#

  • Approach: Options struct per binary, populated via stdlib flag package. No functional options at the top level, no Viper, no YAML config file for the binary itself.
  • Example: cmd/daprd/options/options.goOptions struct with ~40 fields bound to flag.FlagSet. cmd/sentry/options/options.go — nested sub-option structs (X509Options, JWTOptions, OIDCOptions).
  • Internal DI: Subsystem constructors accept explicit Options structs with their dependencies. Example: pkg/scheduler/server/internal/pool/pool.go:32type Options struct { ... } with all dependencies injected.
  • Assessment: Consistent. The choice to avoid Viper keeps the configuration observable and testable — no magic lookup, no global state. Nested Options structs for complex subsystems (sentry OIDC options) keep concerns separated.

Dependency injection#

  • Approach: Manual wiring via Options structs. No DI framework (no wire, dig, or fx).
  • Evidence: cmd/daprd/app/app.go — composition root that wires all subsystems by constructing each with its dependencies. pkg/runtime/runtime.goDaprRuntime struct with all subsystem fields populated in newDaprRuntime().
  • Pattern: Constructor functions accept Options structs; nested structs carry transitive deps. Every package exposes a New(opts Options) *T or New(opts Options) (Interface, error) constructor.
  • Assessment: Explicit and testable. The lack of a framework means the wiring is straightforward Go and always greppable. Downside: composition root (newDaprRuntime) is large — hundreds of lines of wiring code. Acceptable at this scale.

Other notable patterns#

Generics for protocol-agnostic handlers (pkg/api/http/universal.go:30)#

One of Dapr’s most architecturally interesting generic uses: UniversalHTTPHandler[T proto.Message, U proto.Message] wraps any Universal handler function into an http.HandlerFunc. The generic adapter handles JSON/protobuf decoding, InModifier and OutModifier hooks, and status code selection — eliminating per-endpoint boilerplate. This is a generics-as-adapter pattern: the type parameters enforce proto-compatibility while the runtime uses reflection to allocate zero values.

func UniversalHTTPHandler[T proto.Message, U proto.Message](
    handler func(ctx context.Context, in T) (U, error),
    opts UniversalHTTPHandlerOpts[T, U],
) http.HandlerFunc

Generic resiliency Runner (pkg/resiliency/policy.go:34)#

Runner[T any] / Operation[T any] express retry/circuit-breaker/timeout policies generically. A single NewRunner[T](ctx, def) call returns a closure that applies all policies to any operation returning (T, error). The RunnerOpts[T] struct carries a Disposer func(T) — called on timeout-leaked results — and an Accumulator func(T) for batched intermediate results. Pre-generics this would have required interface{} casts or per-type codegen.

Registry + blank-import init() for component loading#

Each building-block type has its own typed Registry with a DefaultRegistry singleton (pkg/components/pubsub/registry.go:32). Component implementations register themselves via init() functions, triggered by blank imports in cmd/daprd/components/. Build tags select between allcomponents.go and stablecomponents.go to produce different binary flavors without conditional code in core paths.

This is one of the cleanest examples of the “self-registering plugin” pattern in Go — zero interface overhead, zero config file, fully determined at link time.

Type switches for protobuf oneofs#

Protobuf oneof fields surface as Go interface values. Type switches are used idiomatically throughout the scheduler pool and actor placement:

// pkg/scheduler/server/internal/pool/loops/connections/connections.go:162
switch t := meta.GetTarget(); t.GetType().(type) {
case *schedulerv1pb.JobTargetMetadata_Actor:
    ...
case *schedulerv1pb.JobTargetMetadata_Job:
    ...
}

Assessment: correct and necessary when consuming generated protobuf. Dapr uses this consistently and safely (no unhandled default in critical dispatch paths).

Interface segregation with dual implementations (disk vs. operator)#

The hotreload.Reloader interface has two implementations selected at startup: DiskReloader (watches filesystem via inotify for standalone mode) and OperatorReloader (consumes gRPC streaming events from Kubernetes operator). Similarly, channel.AppChannel has http.Channel and grpc.Channel implementations. These are textbook examples of Go interface segregation — the interface captures exactly the operation the runtime needs, not the full feature set of either implementation.

gRPC interceptor chains for cross-cutting concerns#

Both unary and streaming gRPC servers use grpc.ChainUnaryInterceptor / grpc.ChainStreamInterceptor for composable middleware:

// pkg/api/grpc/server.go:328-329
grpcGo.UnaryInterceptor(grpcMiddleware.ChainUnaryServer(intr...)),
grpcGo.StreamInterceptor(grpcMiddleware.ChainStreamServer(intrStream...)),

Interceptors handle OTel tracing, metrics, metadata injection, and auth. This is the gRPC idiomatic equivalent of HTTP middleware stacks.

Table-driven tests (247 occurrences)#

Heavy use of table-driven tests throughout, using anonymous structs with descriptive names. Standard Go style, nothing unusual — but the volume signals a culture of parameterized testing over individual test functions.

Broadcaster (event fan-out) in scheduler#

The scheduler uses dapr/kit/events/broadcaster.Broadcaster to fan out host-list updates to all watching subscribers:

// pkg/scheduler/server/internal/cron/leadership.go:29
hostBroadcaster *broadcaster.Broadcaster[[]*schedulerv1pb.Host]

This is an observer/pub-sub pattern scoped to internal service topology changes — not message-queue fan-out.