Skip to content

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

  1. Use goda reach(main, heavy-package) to identify the edge responsible for pulling in the heavy dep tree. Typically a single function call site.
  2. Move the offending function (and any tightly-coupled symbols) into a new package P/heavyfunc/.
  3. Update every caller that genuinely needs the function to import P/heavyfunc directly.
  4. Leave P itself dep-free; binaries importing P for unrelated reasons stop pulling the heavy tree.

Canonical wiki instance: Datadog trace-agent

Trace Agent binary, 2025:

  • go list reported 526 packages from k8s.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 by trace-agent for 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:

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

Last updated · 200 distilled / 1,178 read