Buffalo — Patterns#

Concurrency patterns#

Graceful Shutdown via signal.NotifyContext + sync.WaitGroup#

  • Usage: app.Serve() (server.go:48); the primary concurrency entry point for the whole framework.
  • Example: server.go:48signal.NotifyContext(a.Context, syscall.SIGTERM, os.Interrupt) creates a cancellable context. A dedicated shutdown goroutine waits on <-ctx.Done() then sequentially stops each server (with a per-server context.WithTimeout) and finally stops the worker. A sync.WaitGroup tracks all goroutines.
  • Assessment: Idiomatic and clean. Uses the Go 1.16 signal.NotifyContext API rather than the older signal.Notify channel pattern. Shutdown is sequential (servers first, worker second), which makes sense: drain HTTP in-flight requests before draining the job queue.

Context Cancellation in Worker#

  • Usage: worker/simple.go:27,74 — the Simple worker wraps a parent context.Context with context.WithCancel and stores the cancel func.
  • Example: worker/simple.go:182 — the worker’s goroutine polls <-w.ctx.Done() in a select to exit cleanly when Stop() is called.
  • Assessment: Standard pattern, correctly implemented. The worker isolates its own cancellation from the app-level context, giving Stop() a clean shutdown path without cancelling the request context.

sync.OnceValue for Lazy Singleton Initialization#

  • Usage: plugins.go:17 and plugins/plugins.go:30; new in Go 1.21.
  • Example: plugins.go:17var LoadPlugins = sync.OnceValue(func() error { ... }). The plugin discovery subprocess (buffalo-plugins available) runs exactly once per process, with the result cached for all subsequent calls.
  • Assessment: Excellent use of the new sync.OnceValue API (Go 1.21). Clean, race-safe, and requires no init() trickery. Also avoids the old var once sync.Once; once.Do(func(){...}) boilerplate.

sync.Once for Build-info Caching#

  • Usage: runtime/build.go:51,102 — two sync.Once guards around reading embedded build metadata.
  • Assessment: Fine, but slightly redundant given sync.OnceValue is already used elsewhere. Likely pre-dates the OnceValue addition.

Channel Signaling in Tests#

  • Usage: plugins/plugins_test.go:45make(chan struct{}) used to signal a goroutine that a test subprocess has finished.
  • Assessment: Minimal usage. Buffalo’s concurrency is orchestration-level (goroutines for server + worker lifetime), not data-pipeline-level. No fan-out/fan-in or worker pools beyond the simple job queue.

Categories not present#

  • Worker pools: Not implemented. Simple worker runs each job in its own goroutine with no pool cap; this is called out as a known limitation.
  • Fan-out/fan-in: Not present.
  • Pipeline processing: Not present.
  • Rate limiting: Not present in the core; expected from user-defined middleware.

Error handling#

  • Style: Mixed — fmt.Errorf (bare strings), a custom HTTPError struct, and errors.Is/errors.As for unwrapping. The %w verb is used sparingly (2 occurrences found).
  • Error types defined:
    • HTTPError (errors.go:33) — carries an HTTP Status int and Cause error. Implements Unwrap() so errors.Is/errors.As work through it.
    • ErrorHandler (errors.go:53) — a function type func(int, error, Context) error used as a per-status handler callback.
    • ErrorHandlers (errors.go:65) — map[int]ErrorHandler; a registry keyed by HTTP status code.
    • ErrRedirect (render/auto.go:20) — used by the render.Auto engine to signal a redirect should replace rendering.
    • sendError (mail/smtp_errors.go:10), startTLSUnsupportedError (mail/dialer.go:157) — internal mail subsystem errors.
  • Wrapping approach: Mostly bare fmt.Errorf("message: %v", err). The %w verb appears in render/template.go:70 (fmt.Errorf("%s: %w", name, err)) and in a test; not used consistently across the codebase. No pkg/errors dependency.
  • Examples:
    • errors.go:39HTTPError.Unwrap() delegates to h.Cause, enabling callers to use errors.As(err, &HTTPError{}) to extract status codes.
    • errors.go:70ErrorHandlers.Get(status) falls back to a chain of parent-status defaults (e.g., 422 → 400 → generic default), implementing a status-hierarchy lookup.
    • resource.go:61-81 — resource methods return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) as default stubs.

Assessment: Error handling is workable but not fully modernised. The Unwrap() support on HTTPError is good; the inconsistent %w usage means callers cannot rely on error chain traversal everywhere. A future v1 cleanup would standardise on %w.


Configuration pattern#

  • Approach: Plain config struct (Options) passed by value to buffalo.New(). Defaults are populated by optionsWithDefaults() using Go 1.21’s cmp.Or for precedence chains.
  • Example (options.go):
    type Options struct {
        Name    string
        Addr    string
        Env     string
        Worker  worker.Worker  // inject custom implementation
        SessionStore sessions.Store
        // ...
    }
    opts.Addr = cmp.Or(env.Get("ADDR", ""), ":3000")
    Interfaces (Worker, sessions.Store, Logger) are fields in Options, making dependency injection entirely explicit: pass your implementation in the struct; no framework magic.
  • Assessment: Simple and readable. cmp.Or is a clean Go 1.21 replacement for if x == "" { x = default } chains. The struct fields are well-annotated with json tags and doc comments. No builder pattern, no functional options — just a value struct. Slightly verbose to construct fully, but very easy to understand.

Dependency injection#

  • Approach: Manual wiring via the Options struct.
  • Evidence:
    • options.goWorker worker.Worker, SessionStore sessions.Store, Logger Logger are all interface fields; callers pass concrete implementations.
    • app.go:37buffalo.New(opts Options) reads directly from opts; no container, no reflection, no code generation.
    • No use of google/wire, uber-go/dig, or uber-go/fx.
  • Assessment: Appropriate for a framework of this size. The options-struct pattern is the idiomatic Go equivalent of constructor injection. The trade-off is verbosity when all defaults are acceptable, which NewOptions() addresses by providing a ready-to-use zero-value struct.

Other notable patterns#

Reflection-based Function Identity (pointer-keyed middleware registry)#

  • Where: middleware.go:183-230funcKey() and ptrName().
  • How: To support MiddlewareStack.Skip(mw, handlers...) and Remove(mws...), the stack identifies functions by their runtime uintptr address via reflect.ValueOf(f).Pointer(). A package-level map[uintptr]string (guarded by sync.Mutex) caches pointer-to-name mappings. runtime.FuncForPC(ptr).Name() resolves the human-readable name.
  • Assessment: Pragmatic but fragile. Works reliably for top-level named functions and method values. Breaks for closures (each closure instance has a distinct address), which is a real footgun when users try to skip middleware defined inline. The architecture document calls this out. This is the single most unusual pattern in the codebase.

Compile-time Interface Satisfaction Checks#

  • Where: default_context.go:23-24, worker/simple.go:13.
  • Examples:
    var _ Context = &DefaultContext{}
    var _ context.Context = &DefaultContext{}
    var _ Worker = &Simple{}
  • Assessment: Good practice. These blank-identifier assignments catch interface drift at compile time without runtime cost.

Observer / Event System (gobuffalo/events)#

  • Where: events.go, server.go, route_info.go, errors.go.
  • How: Named string constants (e.g., EvtAppStart = "buffalo:app:start") are emitted via events.EmitPayload and events.EmitError. This is a publish-subscribe bus from the gobuffalo/events library, not a built-in Go channel pattern.
  • Assessment: Provides a clean lifecycle hook mechanism for plugins and tooling (e.g., the CLI live-reload tool can listen for EvtRouteStarted). Decouples the core framework from plugin reactions.

Embedded Templates via //go:embed#

  • Where: errors.go:22-29devErrorTmpl, prodErrorTmpl, prodNotFoundTmpl are embedded HTML strings.
  • Assessment: Modern idiomatic Go (1.16+). Avoids shipping separate template files; the binary is self-contained.

Type Assertions for Interface Narrowing#

  • Where: binding/request_binder.go:39, binding/file_request_type_binder.go:69, default_context.go:83,162, middleware.go:186.
  • Pattern: if v, ok := x.(SomeInterface); ok { ... } used to optionally invoke richer behaviour (e.g., Bindable for custom deserialization, paginable for pagination helpers).
  • Assessment: Appropriate use of the “optional interface” pattern — a clean way to add opt-in capabilities without breaking the base interface. Used sparingly and with the comma-ok idiom throughout.

Table-driven Tests (light usage)#

  • Count: 9 occurrences of test-table markers.
  • Assessment: Modest. Buffalo’s tests tend toward integration-style tests (start a real *App, make HTTP requests) rather than extensive unit-test tables. The framework’s own testing story is mostly covered by the gobuffalo/httptest helper.

sync.RWMutex for Reader-writer Protected Maps#

  • Where: binding/request_binder.go:20, mail/mail.go:29, mail/message.go:25, app.go:19.
  • Assessment: Appropriate for maps that are read far more frequently than written (binder registry, mail headers). Correctly uses RLock/RUnlock for reads and Lock/Unlock for writes.

No Generics#

  • Assessment: The codebase targets Go 1.21 but does not use generics. The Context data bag uses any (interface{}), and map[string]any is the template data type. Given the framework’s API surface (request-scoped data bags, middleware chains), generics would add complexity without clear benefit for the core use case.