PATTERN Cited by 1 source
Single-function forced package split¶
Intent¶
When a single function in an otherwise-shared package drags an entire dependency tree into binaries that don't need it, move that function into its own package so only binaries that actually want it pull in its transitive dependencies.
Motivation¶
Go's import model
is package-scoped: any binary that directly or transitively
imports package P brings in every package P imports (in
files not excluded by build tags). A single helper in P that
depends on a heavy library turns the helper's dependency into
every P-consumer's dependency.
Mechanism¶
- Use goda
reach(main, heavy-package)to identify the edge responsible for pulling in the heavy dep tree. Typically a single function call site. - Move the offending function (and any tightly-coupled symbols)
into a new package
P/heavyfunc/. - Update every caller that genuinely needs the function to
import
P/heavyfuncdirectly. - Leave
Pitself dep-free; binaries importingPfor unrelated reasons stop pulling the heavy tree.
Canonical wiki instance: Datadog trace-agent¶
Trace Agent binary, 2025:
go listreported 526 packages fromk8s.io/*.- systems/go-size-analyzer attributed ≥30 MiB to them.
- systems/goda
reach(trace-agent, k8s.io/api)traced the whole graph back to one function in one package of the Agent codebase, imported bytrace-agentfor a completely unrelated reason. The function didn't use k8s; the package did.
Fix: DataDog/datadog-agent#32174
moved the function into its own package, updated the relevant
imports. Result: 570 packages removed from trace-agent,
≥36 MiB binary-size cut — "more than half of the binary".
The 2026-02-18 post notes this was an extreme case but not a unique one — "we found many similar cases, although with smaller impacts". Transitive-dependency reachability tends to be one edge wide in practice.
Relation to build-tag isolation¶
Both prune at the import edge, but pick different fulcrum points:
- patterns/build-tag-dependency-isolation — file-level, for optional behaviour within a package.
- This pattern — package-level, for one symbol in a shared package.
Rule of thumb: if the optionality can't be expressed as a tag without breaking the package's API contract, split the package.
Forces¶
- + Root-cause fix: the shared package really does become dep-free for everyone except new direct importers.
- + Transparent to binaries that don't need the function — they just stop seeing the dep tree.
- − API break for any external consumer still using the old path (exported symbols moved).
- − Requires an up-to-date dependency graph (goda) to pick the right function confidently — blind restructuring can miss the actual culprit.
- − Doesn't solve the class of linker-level pessimism where even correctly-pruned code hangs onto too many symbols; a different fix (patching / forking / upstream) is needed there.
Seen in¶
- sources/2026-02-18-datadog-how-we-reduced-agent-go-binaries-up-to-77-percent — canonical 570-package / 36-MiB trace-agent instance + explicit note that many smaller-magnitude cases also matched this shape.