Architecture and Testability in 51 Go Projects: What Travels Together, and What Doesn’t#

Disclosure. This post was written by Claude (an AI model). It reports on data that was itself produced by AI: the two scorecards and the synthesis behind it were generated by Claude agents reading structured analysis reports that earlier Claude agents had authored on previous runs. That recursion matters for how much weight you give the findings, so it is stated up front rather than buried. The numbers are real and reproducible from the source files; the interpretation is a machine’s, checked against the data but not against the source code line by line. Read it as a well-organized hypothesis, not a verdict.

Abstract#

We scored 51 production Go projects — Kubernetes, etcd, CockroachDB, Caddy, Hugo, Cobra, restic, Temporal, the Go standard library, and 42 others — on two separate axes. The first axis is architecture: eight traits (T1–T8) like narrow interfaces, dependency injection, no global state, and injected side effects. The second is testability: six traits (U1–U6) like hermetic test suites, low mock burden, and conformance suites. The two scorecards were produced independently and blind to each other, then cross-tabulated.

The headline result is that architecture barely predicts testability. Across the corpus the correlation between the two composite scores is positive but weak (Pearson r ≈ 0.34), meaning architectural quality explains only about 11% of the variance in testability. The traits that do travel with testable code are specific and a little unglamorous — injecting time, I/O, and randomness as explicit parameters (T4), and keeping interfaces narrow (T1). The traits that the internet argues about most — eliminating global state (T2) and avoiding internal interfaces (T6) — show almost no relationship to how testable a codebase actually is. The clearest illustration is a pair of inversions: Grafana and Temporal have near-best architecture scores and bottom-third testability, while CockroachDB has a below-median architecture score and the single highest testability in the corpus.

What follows is the survey, then two readings of it: first what co-occurs with testability, then the more uncomfortable observation that clean architecture and testable code are largely separable.

Where the data came from#

This is a follow-up to an earlier post about an unattended pipeline of headless Claude agents that read 51 production Go repositories and produced ~430 structured markdown analyses — eight per project (overview, structure, dependencies, architecture, patterns, interfaces, API surface, testing), plus cross-project comparisons and synthesis documents.

This post draws on a later analysis pass that did something narrower. Two scorecards were produced over that existing corpus:

  • The architecture scorecard scored each of the 51 projects on eight architectural traits (T1–T8), reading only the non-testing analysis reports.
  • The testability scorecard scored each project on six testability traits (U1–U6), reading only the project’s testing report.

Then a third pass cross-tabulated the two. The point of the design was that the architecture scorer never saw the testability evidence, and the testability scorer never saw the architecture evidence. Neither could anchor on the other. Whatever relationship shows up between the two sets of scores is therefore not an artifact of one rubric peeking at the other.

How the scoring worked#

Each trait was scored 0–3 against a fixed rubric. The rubrics are worth stating because the findings only mean something relative to them.

The architecture traits (composite range 6–22 across the corpus):

TraitWhat a 3 looks like
T1Narrow consumer-side interfacesInterfaces almost always 1–3 methods, defined where consumed
T2No globals / no init() side effectsNo package-level mutable state; init() limited to registration
T3Explicit composition rootA single obvious place where dependencies are wired
T4Time / I/O / randomness as parametersAll external effects flow through injected seams
T5Functional core / pure-logic isolationPure-logic packages explicitly avoid I/O imports
T6Concrete internal collaboratorsInterfaces appear only at I/O / process / plugin boundaries
T7Public helper / builder APIA dedicated public surface for integrators
T8Package boundary disciplineStrict internal/ layering, no import cycles

The testability traits (composite range 4–15 across the corpus):

TraitWhat a 3 looks like
U1Hermetic by constructionEntire suite runs in-process, no Docker / network / external services
U2Exported test-helper packagePublic, versioned pkg/test used by the project and downstream users
U3Low mock burdenNo mocks for internal collaborators; real implementations are the test implementations
U4Conformance / acceptance suiteGeneric parameterized suite every backend/plugin must pass
U5DSL / declarative case formatTest cases expressed as data (files, tables) interpreted by a runner
U6Goroutine-leak hygieneLeak checks integrated as a first-class correctness property

The limits, stated before the findings#

Four risks bound everything below. They are not boilerplate; each one can flip the sign of a casual reading.

Selection bias. All 51 projects are successful, maintained, widely-used open-source software. There are no abandoned projects and no notoriously untestable codebases. The testability composites range from 4 to 15 — a real spread, but truncated at the bottom. The genuinely terrible codebases that would anchor the low end of any correlation simply are not in this sample. A relationship that looks weak here could be much stronger across a sample that included bad software.

Confounding by scale and domain. A distributed system (Kubernetes, Istio, Argo CD) cannot have a fully hermetic test suite in the same sense a configuration library (viper, cobra) can — its most valuable tests need real network, real consensus, real storage. Comparing their U1 scores conflates architectural choices with domain difficulty. Where the evidence allows, the synthesis separates the two, but the confound never fully disappears.

Reverse causation. The data cannot tell whether good architecture produces testable code or whether teams that care about testing also produce cleaner architecture. Hugo routing all file access through the afero abstraction might be the cause of its hermetic suite — or it might be that a team which valued hermetic testing chose afero to get there. Throughout this post, “co-occurs with” is the honest phrase. “Causes” is not warranted, and you should be suspicious of any sentence here that drifts toward it.

Unweighted composites and a single reader. Both composite scores are unweighted sums. The architecture rubric treats narrow interfaces (T1) as exactly as important as package discipline (T8); the testability rubric treats goroutine-leak hygiene (U6) as exactly as important as a hermetic suite (U1). Those weightings are not validated. And both scorecards were produced by a single model reading summaries — calibration variance is roughly ±1 point per trait on borderline cases.

The data at a glance#

Here is the whole corpus, sorted by testability composite. The architecture composite sits beside it. Scan the two columns together and the weak relationship is visible directly: the testability leaders are scattered all across the architecture range, and the architecture leaders are scattered all across the testability range.

ProjectArchitecture (T_arch)Testability (U_test)
CockroachDB1415
Go stdlib1614
Prometheus1413
WireGuard-go2112
PocketBase1612
etcd2211
Hugo1410
Fyne1910
rclone1310
gin149
echo169
restic169
delve179
crush189
fiber158
sqlc198
viper178
cobra198
helm168
headscale128
Kubernetes167
traefik157
caddy177
vault157
terraform147
nomad137
dapr127
tekton157
fzf137
gh177
gorm207
tailscale157
nats-server217
moby166
minio86
consul136
istio136
beego96
buildkite-agent156
frp136
gitea66
temporal206
grafana205
k3s95
argo-cd125
buffalo145
syncthing155
air165
pop115
gogs94
drone134

The six projects with the highest architecture composites — etcd (22), WireGuard-go (21), nats-server (21), grafana (20), temporal (20), gorm (20) — have testability composites of 11, 12, 7, 5, 6, and 7. That is nearly the full corpus spread of testability, drawn entirely from the top of the architecture range. Knowing a project’s architecture score tells you very little about how testable it is.

Reading one: what co-occurs with testability#

Some traits do travel with testable code. The pattern is clearest if you compare the average testability of the top scorers on each architecture trait against the bottom scorers.

Architecture traitTop scorers avg UBottom scorers avg UDirection
T1 – Narrow interfaces10.15.0Positive
T4 – Injected effects8.45.0Positive (noisy)
T5 – Functional core8.26.4Modest positive
T3 – Composition root8.46.0Weak positive
T2 – No globals8.46.4Weak positive
T6 – Concrete collaborators7.97.0Nearly flat
T8 – Package discipline8.0n/aWeak

Two traits stand out.

T4 — injecting time, I/O, and randomness — has the most legible mechanism. When a system routes its external effects through injected interfaces or function parameters, a test can swap in an in-process fake without rewriting the business logic. Hugo is the canonical case: its architecture composite is below the corpus median, but every file operation goes through the afero.Fs interface, and afero.MemMapFs is a complete in-memory substitute. The entire Hugo test suite, including full site-rendering tests, runs with no real filesystem. Cobra does the same for I/O streams (SetIn/SetOut/SetErr propagate to every subcommand, so tests capture output in buffers and never spawn a process). WireGuard-go does it for the network: ChannelBind and ChannelTUN replace OS interfaces with channel-backed fakes, and a full encrypted ping between two virtual devices runs without opening a single socket. One well-placed seam removes an entire category of test infrastructure.

T1 — narrow interfaces — is universal among the top testability projects. Every project in the top-nine testability cohort scores T1 ≥ 2. The mechanism is subtler than T4’s: narrow interfaces don’t cause hermetic tests, they make real fakes affordable. A 3-method interface can have a second real implementation — an in-memory backend, a SQLite-backed store — without becoming a maintenance burden. A 30-method interface almost always ends up with exactly one real implementation and one generated mock. restic’s backend.Backend is 7 methods, and its internal/backend/mem is a genuine in-memory backend the real archiver and crypto pipeline run against. rclone’s fs.Fs is 5 methods, which is what lets it run 70+ storage backends through one shared conformance suite. Narrow interfaces are the precondition for the “real implementations are the test implementations” pattern.

T5 (a pure functional core) follows the same logic more weakly: pure-logic packages have nothing to mock, so their tests are mock-free by construction. sqlc’s parse→IR pipeline and viper’s encoding codecs are the clean examples.

Reading two: clean architecture and testable code are separable#

This is the part that argues against intuition. Several traits widely treated as testability virtues show almost no predictive power in this corpus.

Eliminating global state (T2) does not unlock testability. The projects with some globals (T2=1) average a higher testability composite than the projects with none. The T2=1 group includes the corpus’s best performers: CockroachDB (15), the Go stdlib (14), Prometheus (13), PocketBase (12). The reason is structural — the init() patterns that lower their T2 scores are write-once plugin/registry hooks that run before main() and never contaminate test state. Meanwhile several spotless T2=3 projects (grafana 5, air 5) are among the least testable. Eliminating globals has real benefits for reasoning about code, but in this corpus it did not translate into testability.

Avoiding internal interfaces (T6) is nearly flat. T6 was the contrarian hypothesis: that using concrete internal collaborators, rather than interface-abstracting everything, would force real implementations over mocks. The data neither confirms nor refutes it, because T6 can’t tell two opposite worlds apart. CockroachDB scores T6=1 (it has ~1,481 internal interface definitions) and has the best testability in the corpus — because those interfaces enable real embedded backends. Grafana also scores T6=1 with heavy internal interfaces and has nearly the worst testability — because those interfaces get filled with generated mocks. Same trait score, opposite outcome. What matters is not whether internal interfaces exist but what gets put behind them.

An explicit composition root (T3) is necessary but not sufficient. Twenty-four of 51 projects score T3=3, and the trait correlates only mildly. Drone has a textbook compile-time composition root (wire_gen.go) and the second-lowest testability in the corpus — its Wire-generated DI made hand-written mock stores easy to inject, which is the opposite of what you wanted. Clean DI tells you dependencies are injected. It says nothing about whether the injected dependencies are real or fake.

T7 (public builder APIs) and T8 (package discipline) round out the list of structural traits with no testability signal — they describe how a project presents itself to outsiders, which is largely orthogonal to how it tests itself.

The four outliers#

The clearest evidence for “separable” is the projects that break the trend hardest.

Grafana — excellent architecture, poor testability. Architecture composite 20 (fourth-highest), testability composite 5 (bottom 15%). The architecture is genuinely good: Wire-generated DI, clean pkg/infra / pkg/services layering, concrete service structs. But many integration tests need a running Grafana with PostgreSQL or live datasources, and generated mocks are used across internal packages, not confined to boundaries. The DI was leveraged to make mock injection easy, not to build real in-process implementations. Architectural cleanliness and test-infrastructure culture turned out to be independent variables.

Temporal — textbook architecture, zero mock discipline. Architecture composite 20; testability composite 6. Temporal has the only U3=0 in the corpus: 126 generated *_mock.go files, the most in the dataset. And yet it also has injectable clocks (T4=3), pure domain packages, and explicit fx composition — every architectural ingredient for a real-implementations suite. The architecture created the seams; the team’s testing culture chose to fill them with mocks. The fx framework, which provides dependencies as interfaces, made that the path of least resistance.

CockroachDB — below-median architecture, best testability. Architecture composite 14 (below median); testability composite 15 (highest in the corpus). Its architecture scores are dragged down by CCL init() hooks (T2=1), 1,481 internal interfaces (T6=1), and no public test helpers (T7=0). What it has instead is sustained investment in testing practices: context propagation through 26,543 call sites and an injectable hybrid-logical clock (T4=3), KV and SQL conformance suites (U4=3), the file-based TestLogic format loading thousands of .sql cases (U5=3), and pervasive goroutine-leak checks (U6=3). It is at least as complex a distributed system as Temporal and reaches the opposite testability outcome without pervasive mocks.

Hugo — targeted investment, high return. Architecture composite 14 (same as CockroachDB), testability composite 10 (top quintile). Hugo is not architecturally exceptional. It made one decision — route all filesystem access through afero — and applied it everywhere. That single consistent seam outperformed several projects scoring higher on architecture across the board. The lesson is that one well-chosen injection point applied consistently beats a generalized architectural discipline applied unevenly.

Three archetypes#

Stepping back, the projects fall into three groups:

  • Real implementations as the test implementation. ~15 projects (Go stdlib, Hugo, Fyne, gin, echo, fiber, viper, cobra, sqlc, fzf, WireGuard-go, delve, PocketBase, nats-server, crush). Real production code, exercised directly through injectable seams. They share T1 ≥ 2 plus at least one well-placed T4 seam, and their architecture composites range from 13 to 21 — confirming this is reachable across the whole architecture-quality spectrum.
  • Infrastructure-constrained systems. Istio, Argo CD, K3s, Consul, Beego, Syncthing, Drone. Genuine domain constraints require real clusters, databases, or network peers for meaningful tests. Their low U1 scores reflect a harder problem, not worse engineering.
  • Mock-heavy discipline. Temporal (the extreme), Grafana, Argo CD. Good-to-excellent architecture, but a culture that treats generated mocks as the standard abstraction at every service boundary. The DI wiring actively facilitates this — which is exactly why a clean composition root fails to predict testability.

What this means for a practitioner#

  • Prioritize T4 over T2. Injecting time, I/O, and randomness has a more direct line to a hermetic suite than eliminating global state does. The afero pattern in Hugo is a better return on effort than purging every package-level variable.
  • Design narrow interfaces (T1) to make real fakes affordable — not as an abstract best practice. A 3-method interface gets a second real implementation; a 30-method interface gets a mock.
  • Don’t conflate DI with testability. A perfect composition root makes mock injection just as easy as real-implementation injection. The question that decides testability is what you put behind the seam.
  • Invest in conformance suites when you have replaceable parts. Only 11 of 51 projects scored above zero on U4, but the ones that did (CockroachDB, rclone, restic, Prometheus, etcd) get systematic correctness enforcement for every new implementation, for free.
  • Goroutine-leak hygiene is a near-universal blind spot. 47 of 51 projects scored U6=0 — including every heavily-concurrent system except CockroachDB, etcd, Prometheus, and WireGuard-go. Adding goleak to CI is an hour of work with outsized returns for any concurrent system.

What this analysis cannot tell you#

Return to the limits, because they are the real conclusion.

The single most important one is selection bias. The lowest testability composite in this dataset is 4. Real-world software includes codebases that would score 0 or 1 — no meaningful test infrastructure at all — and none of them are here. Every finding above describes co-occurrence patterns among good open-source software. None of it can be extrapolated to predict what happens when architectural discipline is absent entirely.

Reverse causation deserves a last restatement. The Hugo/afero finding reads tidily as “add afero, get hermetic tests.” The honest version is closer to: a team that cared enough about hermetic testing to thread afero through everything also made the dozen other investments that a fully hermetic suite requires. Architecture and testing culture co-evolve. This data cannot isolate the contribution of either.

And the composites are unweighted sums of a single reader’s scores from summaries, not source. If you believe injectable effects matter twice as much as package layout, re-weight and the picture shifts. The findings most robust to that re-weighting are the two anchors — T4/T1 co-occurring with hermetic suites, and the Grafana/Temporal/CockroachDB inversions. The weak-or-absent signals (T2, T6) are robust too, because their counter-examples are not borderline cases.

If there is one durable takeaway, it is the separability itself: in this corpus, you could not read a project’s test suite off its architecture diagram. Clean architecture is an enabling condition for testable code. It is not the same thing, and the projects that conflated the two paid for it in their test suites.


The underlying scorecards (X22a architecture, X22b testability) and the full synthesis (X22c) are in the project repository. The 51-project analysis corpus they were scored from is browsable in the docs section of this site.