Buffalo — Architecture#

Architectural style#

Layered Framework Library (Ports-and-Adapters)

Buffalo is a web framework delivered as a pure Go library — there is no buffalo binary. User applications call buffalo.New(opts) in their own main.go to obtain an *App and then call app.Serve(). Internally, the App sits at the centre of a ports-and-adapters layout: render, servers, worker, binding, and plugins are independent subsystems (adapters) that App orchestrates. None of the sibling packages import each other; all coupling flows inward toward App.

Component diagram (textual)#

User application (main.go)
        │
        ▼ buffalo.New(Options)
┌───────────────────────────────────────┐
│              App (root pkg)           │
│  ┌──────────┐  ┌────────────────────┐ │
│  │ Options  │  │       Home         │ │
│  │ (config) │  │ (routing group:    │ │
│  └──────────┘  │  Middleware,       │ │
│                │  ErrorHandlers,    │ │
│                │  gorilla/mux router│ │
│                └────────────────────┘ │
│                                       │
│  adapters (no cross-imports)          │
│  ┌──────────┐  ┌──────────────────┐   │
│  │ binding/ │  │    render/       │   │
│  │ (decode  │  │ (HTML/JSON/XML/  │   │
│  │  bodies) │  │  SSE/MD/DL)      │   │
│  └──────────┘  └──────────────────┘   │
│  ┌──────────┐  ┌──────────────────┐   │
│  │ servers/ │  │     worker/      │   │
│  │ (HTTP/   │  │ (background jobs)│   │
│  │  TLS/    │  └──────────────────┘   │
│  │  Unix)   │                         │
│  └──────────┘                         │
│  ┌──────────────────────────────────┐ │
│  │ plugins/ (external binary IPC)   │ │
│  └──────────────────────────────────┘ │
│  ┌──────────────────────────────────┐ │
│  │ internal/ (env, httpx, meta,     │ │
│  │           nulls, templates)      │ │
│  └──────────────────────────────────┘ │
└───────────────────────────────────────┘
         │ app.Serve()
         ▼
  goroutine: servers.Server.Start(ctx, app)
  goroutine: worker.Worker.Start(ctx)
  goroutine: graceful shutdown watcher

Core components#

App#

  • Package: github.com/gobuffalo/buffalo (app.go)
  • Responsibility: Central orchestrator; owns the HTTP lifecycle, routing, middleware, error handling, session management, workers, and event emission.
  • Key types: App (struct embedding Options + Home)
  • Dependencies: Home, Options, servers, worker, plugins, gorilla/mux, gobuffalo/events

Home#

  • Package: github.com/gobuffalo/buffalo (home.go)
  • Responsibility: Routing group container; holds MiddlewareStack, ErrorHandlers, the underlying *mux.Router, and static file paths. Being extracted from App as part of the road-to-v1 refactor to separate application lifecycle from routing concerns.
  • Key types: Home (struct)
  • Dependencies: gorilla/mux, MiddlewareStack

Context / DefaultContext#

  • Package: github.com/gobuffalo/buffalo (context.go, default_context.go)
  • Responsibility: Per-request state bag. Wraps http.ResponseWriter, *http.Request, session, cookies, flash, params, logger, and template data. Passed to every Handler and middleware.
  • Key types: Context (interface), DefaultContext (concrete struct)
  • Dependencies: binding, render, gorilla/mux, gorilla/sessions

Handler / MiddlewareStack#

  • Package: github.com/gobuffalo/buffalo (handler.go, middleware.go)
  • Responsibility: Handler is the fundamental unit of request processing (func(Context) error). MiddlewareStack manages an ordered slice of MiddlewareFunc wrappers with per-handler skip, remove, replace, and clone operations.
  • Key types: Handler (type alias), MiddlewareFunc (type alias), MiddlewareStack (struct)
  • Dependencies: stdlib reflect + runtime for pointer-based function identity

render.Engine#

  • Package: github.com/gobuffalo/buffalo/render (render.go)
  • Responsibility: Unified rendering subsystem. Dispatches to format-specific Renderer implementations: HTML via plush templating, JSON, XML, plain text, Server-Sent Events, file download, markdown, JS. Configurable template helpers and custom template engines.
  • Key types: Engine (struct), Renderer (interface), Options
  • Dependencies: gobuffalo/plush, gobuffalo/helpers

servers.Server#

  • Package: github.com/gobuffalo/buffalo/servers (servers.go)
  • Responsibility: Abstracts the HTTP serving layer. Simple wraps *http.Server, TLS adds certificate loading, Listener wraps a pre-created net.Listener. Provides WrapXxx factory functions for user customisation.
  • Key types: Server (interface), Simple, TLS, Listener
  • Dependencies: stdlib net/http, net

worker.Worker#

  • Package: github.com/gobuffalo/buffalo/worker (worker.go)
  • Responsibility: Background job queue abstraction. Defines an interface for Perform, PerformAt, PerformIn, Register, Start, Stop. The built-in Simple implementation runs jobs in goroutines; third-party adapters (e.g. gocraft/work) implement the interface.
  • Key types: Worker (interface), Handler (type alias), Job
  • Dependencies: stdlib context, time

plugins#

  • Package: github.com/gobuffalo/buffalo/plugins (plugins.go)
  • Responsibility: Discovers and dispatches to CLI plugins installed as external binaries. On LoadPlugins(), runs buffalo-plugins available as a subprocess and parses JSON output to learn what commands are available. Dispatches lifecycle events (e.g. buffalo build) to registered plugin binaries via JSON IPC. Entirely process-boundary coupling — no Go import of third-party plugins.
  • Key types: Plugin, Plugins, Command
  • Dependencies: stdlib os/exec, encoding/json, gobuffalo/events

Data flow#

A typical HTTP request flows as follows:

1. net/http → App.ServeHTTP(w, r)
2.   → processPreHandlers (optional pre-middleware: http.Handler or PreWare chain)
3.   → method override (MethodOverride handler rewrites POST _method field)
4.   → gorilla/mux router matches path/method → RouteInfo
5.   → MiddlewareStack.handler(info) wraps Handler with all middleware in reverse order
       (assertMiddleware always innermost, then RequestLogger, PanicHandler, defaultErrorMiddleware,
        then user-defined middleware)
6.   → App.newContext(info, w, r) creates DefaultContext:
       - merges mux path vars + query/form params
       - attaches session, flash, logger, template data
7.   → User Handler(ctx Context) error
       - reads ctx.Params(), ctx.Bind() for input
       - returns ctx.Render(200, render.JSON/HTML/...) or ctx.Error(code, err)
8.   → render.Renderer writes Content-Type header + encoded body to response
9.   ← Response flushed to net/http

Background jobs are enqueued via app.Worker.Perform(job) from any handler and processed asynchronously by the worker goroutine started in Serve().

Initialization / Bootstrap#

buffalo.New(opts Options) *App:

  1. LoadPlugins() — discovers external buffalo CLI plugins (subprocess call)
  2. env.Load() — loads .env file via godotenv if present
  3. optionsWithDefaults(opts) — fills in GO_ENV, ADDR, PORT, HOST, SESSION_SECRET, LOG_LEVEL from env vars using cmp.Or; creates default gorilla cookie session store, logger, and simple worker
  4. Constructs App with Home{router: mux.NewRouter(), ErrorHandlers: {...}} and a default MiddlewareStack containing RequestLogger, defaultErrorMiddleware, PanicHandler
  5. Returns *App ready for route registration

app.Serve(srvs ...servers.Server):

  1. Emits EvtAppStart lifecycle event
  2. Selects default server (TCP or UNIX socket based on Addr)
  3. Sets up signal.NotifyContext for SIGTERM/SIGINT
  4. Launches goroutines via sync.WaitGroup:
    • Shutdown watcher (listens for context cancellation → shuts down servers then worker)
    • Worker goroutine (worker.Start(ctx)) unless WorkerOff
    • One goroutine per servers.Server calling server.Start(ctx, app) — passes app as http.Handler
  5. wg.Wait() blocks until all goroutines return

There is no dependency injection framework. All wiring is manual via the Options struct. Consumers pass implementations (custom servers.Server, worker.Worker, sessions.Store) directly into Options fields.

Configuration#

Buffalo is configured entirely through the Options struct passed to buffalo.New(). Defaults are populated from environment variables in optionsWithDefaults():

OptionEnv varDefault
EnvGO_ENV"development"
AddrADDR, PORT127.0.0.1:3000 (dev) / 0.0.0.0:3000 (prod)
HostHOSThttp://127.0.0.1:3000
LogLvlLOG_LEVELDebugLevel
SessionStoreSESSION_SECRETgorilla cookie store
SessionName"_buffalo_session"
TimeoutSecondShutdown60

No Viper, no config files, no etcd. Configuration is intentionally minimal: env vars for deployment, direct struct fields for programmatic control. Uses Go 1.21’s cmp.Or for precedence-ordered defaults.

Key design decisions#

  1. Library over framework binary. Buffalo provides zero executables. Users own main.go, which means the framework never controls the program entry point. The trade-off: easier to embed and customise; harder to provide scaffolding and live-reload without a separate CLI tool (buffalo CLI, which is a separate repo).

  2. Handler func(Context) error over http.Handler. The single-method function type is far simpler than the stdlib interface and enables middleware written as higher-order functions wrapping Handler. Error return values propagate cleanly to centralized error handlers rather than requiring http.Error calls scattered across handlers.

  3. Middleware skip-by-handler-identity using reflection pointers. The MiddlewareStack identifies functions by their runtime pointer address (via reflect.ValueOf(f).Pointer()), allowing per-handler middleware skipping without any decorator types or build-time annotation. This is a pragmatic but fragile approach that breaks with closures and anonymous functions.

  4. Process-boundary plugin architecture. Third-party tools integrate by installing an executable named buffalo-* on $PATH. Buffalo discovers capabilities via JSON over stdout IPC, not Go plugin imports or shared objects. This avoids Go’s plugin ABI fragility and version coupling entirely, at the cost of requiring external binaries.

  5. Home struct extraction (in-flight, road-to-v1). The routing group concerns (Middleware, ErrorHandlers, router, filepaths) are actively being separated from application lifecycle concerns (Serve, Stop, Worker, Logger) into a Home struct. This refactor makes explicit what was implicit: an App.Group() is a routing scope, not a full application, and should not expose Serve()/Stop(). Code shows the transition is incomplete — App still has bridging fields (root, appSelf, children) and TODOs.