There are sixteen directories in go-tool-base’s pkg/ today, and that’s after the extraction. I counted them twice, on a Sunday afternoon, because the first count didn’t seem right.
The part that doesn’t get written up
Decomposition stories are always told through what came out. Here’s the module, here’s the import path, here’s the tidy little go.mod with three lines in it. Fine, but I’d scored twenty-seven packages before any of that started, and the scoring was the easy bit.
The hard calls were the ones where the answer came back no. There were four of them, and no two of them had the same reason, which is the thing I took away from it: “extract or don’t” turned out to be nowhere near a big enough vocabulary. Ask a binary question and most packages eventually become modules, because “no” tends to look like you couldn’t be bothered.
Delete it, don’t extract it
pkg/forms was the wizard layer, the prompts you get when you run gtb generate project and it asks what you’d like it to build. It scored ten out of ten for ease of decoupling: trivial to lift out, no framework coupling worth the name, straight into wave two alongside the other easy wins. And then, before touching it, the obvious question (the one I should perhaps have asked first)… does this still earn its keep at all?
It didn’t. huh had moved on to v2 and absorbed the entire purpose of the thing, so what forms was doing in 2024 was papering over gaps that no longer existed, and I’d have been extracting a wrapper around a library that had learned to do the job itself.
So it went. Commit 8b5732c0 rewrites the wizards on native huh v2 and deletes the package in one go, which is a very satisfying shape for a commit to have.
The most valuable extraction decision I made that week was an rm -rf.
Keep it, because the world has enough of these
logger also scored ten for ease, also went into wave two, and also never left. Completely different reason, and it’s the call I’m surest about of the four. I feel like there are too many loggers out there at the moment and adding another package to that pile becomes its own problem, this implementation is very specific to gtb, and all the packages I’d already extracted use slog as their intersection.
That second half is the bit that matters. If half a dozen extracted modules all talk to slog, then the standard library is already the shared interface, and there is nothing left for a go/logger module to do except sit in the middle of a relationship that was working fine without it.
Extracting it wouldn’t have shared anything! It would have manufactured a dependency, on me, in every module that adopted it, in exchange for an interface Go already ships. The framework keeps its own logger because the framework has opinions about how a CLI should look when it talks. That’s a product decision, and I’m not sure a product decision is something you should hand to other people as a library.
Keep it, because it really is framework-shaped
telemetry is the opposite case, and it got the opposite treatment. I suspected it was too tangled to be worth extracting, but a suspicion is not a verdict, so it went off to be examined properly: dig into the design limitations, work out whether it even warrants extraction, don’t assume.
Verdict: staying, and definitely staying.
How I got there matters rather more than the answer did, though, because if I’d just trusted the hunch I’d have got the same result and learned nothing, and the next hunch would have been every bit as unexamined. The rubric exists so these calls aren’t vibes, including the ones where the vibe was right all along.
Or widen it, which I didn’t see coming
The fourth one refused both options. go-tool-base carried a wide GitHub client, and a lot of that code was left over from an earlier incarnation of the framework, written while I was at a very GitHub-centric employer. Extracting it would have shipped one vendor’s API as though it were an abstraction; deleting it would have thrown away work that did something useful.
So it got widened instead: the whole thing became the go/forge contract with per-provider adapters underneath, and today there are four of them. That has its own story and I’ll write it up properly another time, because the interesting part isn’t the refactor, it’s carrying a previous job’s code around for years without noticing what shape it had left you in.
Five words, not two
So here’s the vocabulary the programme actually needed.
- Extract. Real reuse value, and the coupling can come out.
- Keep, too specific. It’s framework-shaped. Someone else would have to bend it to fit.
- Keep, too generic. The ecosystem has this covered, or the standard library already is the interface.
- Delete. Something else grew into the job while you weren’t looking.
- Widen. It’s the right idea, dressed for one vendor.
Only the first is what people mean when they say “extract”. Two of the others leave the package where it was and neither is a shrug, one kills it outright, and one keeps the idea while throwing the implementation away. Five different answers, and a yes/no question can only ever reach two of them.
What’s left is what it is
After the waves, pkg/ holds sixteen things: the CLI wiring, props, setup, docs, the transports, the logger, the telemetry, version, chat. That’s not a leftovers pile so much as the answer to “what is go-tool-base, actually”, written out as a directory listing, and I couldn’t have told you it before I started. I suspect most frameworks have a list like this, and never find out what’s on it, because nothing ever forces the question.
I do think the whole exercise was worth it, and said so at the time in rather more exclamation marks than were strictly necessary, but the modules aren’t the thing I’d point at.
Sixteen directories, and now I know why each one is still there.





