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:48—signal.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-servercontext.WithTimeout) and finally stops the worker. Async.WaitGrouptracks all goroutines. - Assessment: Idiomatic and clean. Uses the Go 1.16
signal.NotifyContextAPI rather than the oldersignal.Notifychannel 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— theSimpleworker wraps a parentcontext.Contextwithcontext.WithCanceland stores the cancel func. - Example:
worker/simple.go:182— the worker’s goroutine polls<-w.ctx.Done()in aselectto exit cleanly whenStop()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:17andplugins/plugins.go:30; new in Go 1.21. - Example:
plugins.go:17—var 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.OnceValueAPI (Go 1.21). Clean, race-safe, and requires no init() trickery. Also avoids the oldvar once sync.Once; once.Do(func(){...})boilerplate.
sync.Once for Build-info Caching#
- Usage:
runtime/build.go:51,102— twosync.Onceguards around reading embedded build metadata. - Assessment: Fine, but slightly redundant given
sync.OnceValueis already used elsewhere. Likely pre-dates the OnceValue addition.
Channel Signaling in Tests#
- Usage:
plugins/plugins_test.go:45—make(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.
Simpleworker 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 customHTTPErrorstruct, anderrors.Is/errors.Asfor unwrapping. The%wverb is used sparingly (2 occurrences found). - Error types defined:
HTTPError(errors.go:33) — carries an HTTPStatus intandCause error. ImplementsUnwrap()soerrors.Is/errors.Aswork through it.ErrorHandler(errors.go:53) — a function typefunc(int, error, Context) errorused 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 therender.Autoengine 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%wverb appears inrender/template.go:70(fmt.Errorf("%s: %w", name, err)) and in a test; not used consistently across the codebase. Nopkg/errorsdependency. - Examples:
errors.go:39—HTTPError.Unwrap()delegates toh.Cause, enabling callers to useerrors.As(err, &HTTPError{})to extract status codes.errors.go:70—ErrorHandlers.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 returnc.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 tobuffalo.New(). Defaults are populated byoptionsWithDefaults()using Go 1.21’scmp.Orfor precedence chains. - Example (
options.go):Interfaces (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")Worker,sessions.Store,Logger) are fields inOptions, making dependency injection entirely explicit: pass your implementation in the struct; no framework magic. - Assessment: Simple and readable.
cmp.Oris a clean Go 1.21 replacement forif x == "" { x = default }chains. The struct fields are well-annotated withjsontags 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
Optionsstruct. - Evidence:
options.go—Worker worker.Worker,SessionStore sessions.Store,Logger Loggerare all interface fields; callers pass concrete implementations.app.go:37—buffalo.New(opts Options)reads directly fromopts; no container, no reflection, no code generation.- No use of
google/wire,uber-go/dig, oruber-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-230—funcKey()andptrName(). - How: To support
MiddlewareStack.Skip(mw, handlers...)andRemove(mws...), the stack identifies functions by their runtimeuintptraddress viareflect.ValueOf(f).Pointer(). A package-levelmap[uintptr]string(guarded bysync.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 viaevents.EmitPayloadandevents.EmitError. This is a publish-subscribe bus from thegobuffalo/eventslibrary, 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-29—devErrorTmpl,prodErrorTmpl,prodNotFoundTmplare 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.,Bindablefor custom deserialization,paginablefor 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 thegobuffalo/httptesthelper.
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/RUnlockfor reads andLock/Unlockfor writes.
No Generics#
- Assessment: The codebase targets Go 1.21 but does not use generics. The
Contextdata bag usesany(interface{}), andmap[string]anyis 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.