Business Layer Extensions
The business layer layers cross-cutting concerns onto core domain logic with a
decorator pattern: each extension wraps an ExtBusiness and delegates to it.
Wrap, never modify the core Business. An extension that adds tracing to one
method must still implement every other method as a plain pass-through.
It comes in three coordinated pieces: the seam on the bus (Piece 1), the extension package itself (Piece 2), and the wiring that registers it (Piece 3).
Checklist
- Seam exists? Confirm the target
<x>bus.gohasExtBusiness,Extension, and the reverse-apply loop inNewBusiness. If not, add them (piece 1). - Create
extensions/<x><concern>/<x><concern>.go(piece 2). - Implement EVERY
ExtBusinessmethod — wrap the relevant ones, pass the rest straight through toext.bus. Missing one fails to satisfy the interface and won't compile. - Wire into
api/services/*/main.goin the right order (piece 3).
Piece 1 — the seam (in <x>bus/<x>bus.go)
// ExtBusiness lists every public method an extension can wrap.
type ExtBusiness interface {
Create(ctx context.Context, input CreateInput) (Model, error)
// ... every other public business method
}
// Extension wraps a new layer of business logic around the existing logic.
type Extension func(ExtBusiness) ExtBusiness
func NewBusiness(log *logger.Logger, delegate *delegate.Delegate, storer Storer, extensions ...Extension) ExtBusiness {
b := ExtBusiness(&Business{ /* ... */ })
for i := len(extensions) - 1; i >= 0; i-- { // reverse: first-listed = outermost
if ext := extensions[i]; ext != nil {
b = ext(b)
}
}
return b
}
Piece 2 — the extension (extensions/<x><concern>/<x><concern>.go)
Package name is <x><concern> (e.g. widgetotel, widgetlog). Struct holds the
wrapped bus plus any deps; NewExtension returns the closure.
// Package widgetotel provides an extension for WidgetBus to add OTEL related
// code on top of it without changing the existing logic.
package widgetotel
type Extension struct {
bus widgetbus.ExtBusiness
}
func NewExtension() widgetbus.Extension {
return func(bus widgetbus.ExtBusiness) widgetbus.ExtBusiness {
return &Extension{bus: bus}
}
}
// Wrapped method — adds the concern, then delegates.
func (ext *Extension) Create(ctx context.Context, input widgetbus.CreateWidgetInput) (widgetbus.Widget, error) {
ctx, span := otel.AddSpan(ctx, "business.widgetbus.create")
defer span.End()
w, err := ext.bus.Create(ctx, input)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return w, err
}
span.SetStatus(codes.Ok, "")
return w, nil
}
// Pass-through method — NOT relevant to this concern, but REQUIRED for the interface.
func (ext *Extension) Delete(ctx context.Context, w widgetbus.Widget) error {
return ext.bus.Delete(ctx, w)
}
Metrics variant
For a Prometheus metrics extension, follow the repo house style: a package-level
singleton built in init() with promauto, _total counter naming, and a status
label. Record after delegating, then return the original result.
var metrics = struct {
created *prometheus.CounterVec
}{
created: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "widgetbus_created_total",
Help: "The total number of widget create operations.",
}, []string{"status"}),
}
func status(err error) string {
if err != nil {
return "error"
}
return "success"
}
func (ext *Extension) Create(ctx context.Context, input widgetbus.CreateWidgetInput) (widgetbus.Widget, error) {
w, err := ext.bus.Create(ctx, input)
metrics.created.WithLabelValues(status(err)).Inc()
return w, err
}
Piece 3 — wiring (api/services/*/main.go)
Construct each extension, then pass them as the trailing variadic args to NewBusiness
in outermost-first order:
widgetOtelExt := widgetotel.NewExtension() // first = outermost wrapper
widgetLogExt := widgetlog.NewExtension(log)
widgetBus := widgetbus.NewBusiness(log, delegate, widgetStorage, widgetOtelExt, widgetLogExt)
Order matters. NewBusiness applies extensions in reverse, so the first one passed is
the outermost layer (runs first on the way in, last on the way out). The repo convention
is OTEL first (outermost span), then logging/metrics inside it.
Common mistakes
- Forgetting a pass-through method → interface not satisfied, compile error. List
every
ExtBusinessmethod first, then fill them in. - Guessing the wrap order — first in the slice is outermost, not innermost.
- Modifying core
Businessto add the concern instead of wrapping it. The whole point is to leave core logic untouched. - These extensions forward already-typed Business values, so layer-boundary converters
usually don't apply — but if you touch primitives or DB rows, follow
layered-architecture-types.
Use use-modern-go for syntax.