Skip to content

Wire service.Fat with one function per operation - #59

Merged
maragubot merged 4 commits into
mainfrom
fat-per-method-wiring
Jul 31, 2026
Merged

Wire service.Fat with one function per operation#59
maragubot merged 4 commits into
mainfrom
fat-per-method-wiring

Conversation

@maragubot

@maragubot maragubot commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

service.Fat was a flat struct: every method could reach every capability, and two of the three
fields (bucket, sender) were reached by nothing at all. This gives each operation a wiring
function that hands it exactly the capabilities it uses, mirroring how handlers are registered in
http.

  • Fat keeps only ambient state (logger, tracer) and one private func field per operation.
    Capabilities leave the struct.
  • One exported wiring function per operation: service.GetUser(f, db) takes *Fat first, then
    narrow private interfaces naming exactly the methods the operation calls, and sets the func field.
    An operation cannot reach a capability it was not given, because Fat holds none.
  • Public methods stay methods, delegating to the field and panicking with the name of the wiring
    function when unwired — so *Fat keeps satisfying consumer-side interfaces like http.userGetter.
  • Wiring an operation twice panics too, so the two ends of the lifecycle are enforced rather than
    documented: an operation is callable only after its one wiring, against the capabilities that
    wiring chose.
  • service.NewFat takes global configuration only (a nil Log discards); service.Setup is
    wiring-only and called from main. servicetest.NewFat(t) constructs without wiring, with a
    t.Log-backed logger, so each test wires what it exercises.
  • TestSetup reflects over the func fields, so an operation that gets a wiring function but never a
    line in Setup fails the test instead of panicking in production.

Four things are plumbed and waiting for their first operation, which is deliberate — this is a
template, and a project that starts here should find them already flowing:

  • bucket and sender are parameters of service.Setup, passed from main. That keeps them out of
    Fat, where every operation could reach them, while leaving them one wiring function away. The
    doc on Setup says as much, so the unused parameters do not read as an oversight.
  • log and tracer are ambient state on Fat itself, the tracer from otel.Tracer("app/service")
    to match otel.Tracer("app/http") in http.AddUserToContext. Every operation gets both for free;
    the one that exists does not happen to use either yet.

cmd/app/main.go is unchanged apart from the three wiring lines.

One thing worth a look: servicetest.NewFat losing its variadic options is a small API break for
anything already built on the template — tests now build their own database and pass it to the
wiring function.

`service.Fat` no longer holds capabilities. It carries the logger and one private func field per
operation, and an exported wiring function per operation (`service.GetUser`) sets that field from
the narrow interfaces it declares. The public methods delegate and panic when unwired, so `*Fat`
keeps satisfying interfaces like `http.userGetter`.

`service.NewFat` now takes only global configuration, `service.Setup` does the wiring and is called
from `main`, and `servicetest.NewFat` constructs without wiring so each test wires what it
exercises. An internal `TestSetup` asserts `service.Setup` leaves no operation unwired.

Drops the unused `bucket` and `sender` fields, which leaves `cmd/app/main.go` with no reason to
build an S3 bucket.
Neither has an operation to wire it yet, but both stay in the composition path instead of being
dropped: `service.Setup` takes them as parameters, so the first operation that needs one finds it
already flowing from `main`. `service.Fat` still holds no capabilities.

Restores the `s3.NewBucket` setup in `cmd/app/main.go`.
`service.NewFat` sets a `trace.Tracer` from `otel.Tracer("app/service")`, matching how
`http.AddUserToContext` names its own. Like the logger, it is there for the first operation that
needs it rather than being reached for per operation.
Comment thread service/fat.go Outdated
`service.GetUser` guards its func field before setting it, so wiring happens exactly once. Together
with the delegate's not-wired panic, the two bracket the lifecycle: an operation is callable only
after its one wiring, and against the capabilities that wiring chose.

The rule was a sentence in the `service.Fat` doc before; the doc now states the panic instead.
@maragubot
maragubot merged commit f07cddf into main Jul 31, 2026
5 checks passed
@maragubot
maragubot deleted the fat-per-method-wiring branch July 31, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants