skip to content

Modern tooling can strip unused code from a shipped artifact — tree shaking and dead-code elimination in bundlers, fine-grained build targets in systems like Bazel, and module descriptors in JPMS or OSGi. Does that make the Common Reuse Principle obsolete?

level: principalimportance: nice to knowfreq 14%

answer

  1. DCE removes bytes, not release coupling
  2. Resolution precedes elimination — transitives, diamonds, SBOM stay
  3. Reflection / DI / side-effectful imports defeat shaking
  4. Fine-grained targets = CRP implemented by the build, inside your repo only
  5. Across independent release boundaries, CRP still rules

basics

~20 s

No. Those tools remove unused bytes, but the Common Reuse Principle is mostly about coupling to another component's release cycle, versioning and dependency closure — none of which tree shaking touches. It reduces one symptom, not the cause.

solid answer

~50 s

Elimination tooling addresses the artifact-size symptom of a CRP violation, not the structural coupling. Even with perfect dead-code elimination you still take a dependency on the whole component: you absorb every release it publishes, including ones caused by code you don't use; you inherit its declared transitive dependencies during resolution, which is where version conflicts and CVE reports arise; its API surface and compatibility promises still apply to you; and the owning team's release discipline is still your constraint. The tooling itself is also imperfect — reflection, dynamic loading, service loaders, configuration-driven wiring and side-effectful module initialisation routinely defeat static reachability, so elimination is conservative. Fine-grained build targets and module descriptors do help genuinely, by decoupling logical packaging from physical dependency and enabling accurate, narrow build edges — but only within a build you control. Across a published-artifact boundary with independent versioning, CRP still governs.

go deeper

for a junior

Answer that the tools remove unused code from the final bundle but you still depend on the whole library, including its updates and its other dependencies.

for a middle

Separate the size symptom from the coupling cause, and name at least two things elimination cannot touch (transitive resolution, forced upgrades).

for a senior

Explain why elimination is conservative (reflection, side effects, dynamic config), and where fine-grained build targets do legitimately substitute for splitting.

for a principal

Turn it into policy: fine-grained targets internally, usage-aligned artifacts across release boundaries, side-effect-free modules plus maintained keep rules, SBOM/CVE handling under presence-based scanning, and periodic re-validation of published boundaries.

## What the tools actually do - **Tree shaking / dead-code elimination (DCE):** a bundler or compiler statically determines which exported symbols are reachable from your entry points and omits the rest from the emitted output. Requires statically analysable module structure (ES modules, section-GC linking, Proguard/R8-style shrinking, .NET IL trimming, Go's linker dropping unreferenced symbols). - **Fine-grained build targets (Bazel, Buck, Pants, Gradle subprojects):** a target declares narrowly what it depends on; the build graph rebuilds and links only what is reachable. This makes *build-time* dependency granularity finer than any published artifact. - **Module descriptors (JPMS `module-info`, OSGi bundles):** declare exports/requires at package granularity, enabling strong encapsulation and tools like `jlink` to assemble a minimal runtime image. All three attack **shipped size and build scope**. Now check them against the actual costs of a CRP violation. ## The costs they do NOT remove 1. **Release coupling.** If the component releases 3.1 because of a subsystem you don't use, you still choose between upgrading (rebuild, re-test, re-certify, redeploy) and pinning (drift, then a painful catch-up). No amount of DCE changes this — it operates *after* you've already decided which version to consume. 2. **Resolution-time transitive closure.** Dependency resolvers work from declared metadata, not from reachability. The unused subsystem's declared dependencies still enter your resolution graph, still participate in conflict resolution, still appear in your lockfile and SBOM. DCE runs later and cannot un-declare them. 3. **Version-conflict / diamond problems.** These are decided during resolution — again, before any elimination. 4. **Security and licence reporting.** Scanners and SBOM tooling are presence-based. Reachability-aware scanning exists but is not universal, is defeated by dynamic dispatch and reflection, and many compliance regimes require remediation or a documented exception regardless of reachability. 5. **API and compatibility surface.** The component's public contract, its deprecations and its breaking-change policy apply to the whole artifact you depend on. 6. **Organisational coupling.** You depend on the owning team's cadence, support level and deprecation policy for the entire component. 7. **Cognitive surface.** Consumers still see the whole API in docs and autocomplete. ## Why the elimination itself is unreliable Static reachability is conservative by necessity: - **Reflection / dynamic loading:** looking a class up by name, service-loader mechanisms, dependency-injection containers scanning annotations, plugin registries — all make code reachable in ways the analyser cannot see. Tools then need hand-written keep rules, and a missed rule is a production class-not-found failure rather than a compile error. - **Side effects at module load:** a module whose import registers something (a polyfill, a codec, a metrics collector) cannot be dropped even if no symbol is referenced. Bundlers use side-effect hints precisely because they cannot infer this. - **Re-export barrels / index modules** that eagerly pull in everything can defeat shaking unless carefully authored. - **Dynamic configuration:** which implementation is used may be decided at runtime from config, so everything must be kept. So the size benefit is real but partial and fragile, and requires ongoing maintenance of keep rules and side-effect metadata. ## Where the tools genuinely change the calculus Be fair to the counter-argument: - **Inside a monorepo with fine-grained targets and atomic builds**, the physical artifact boundary is decoupled from the dependency edge. A target can depend on exactly the sub-library it needs, and everything is released together, so release coupling nearly vanishes. Here CRP pressure is legitimately much lower — the build system is *implementing* CRP for you at target granularity. - **Bundle-size-critical client code** does get the size benefit, provided authors keep modules side-effect-free. - **JPMS/OSGi** improve encapsulation and let you ship trimmed runtime images, reducing attack surface at deployment even if the build-time dependency remains coarse. The honest conclusion: **the finer your dependency edges can be, the more of CRP you get for free — but only within the boundary where you control the build and the release.** The moment a component crosses an independent-release boundary (published artifact, external consumer, separate team cadence), CRP is back in force because the coupling it targets is release coupling, not byte coupling. ## The principal-level framing Treat elimination tooling as a *mitigation* in your dependency-hygiene strategy, not a substitute for boundary design. The policy that follows: fine-grained targets internally; usage-aligned published artifacts externally; side-effect-free modules and maintained keep rules where shaking matters; SBOM and CVE policy that acknowledges presence-based reporting; and periodic review of whether published boundaries still match observed joint usage.

  • Name two situations where tree shaking silently fails to remove genuinely unused code.
    When code is reached only reflectively — dependency-injection containers scanning annotations, service loaders, class lookup by name — the analyser cannot prove it unreachable and it must be kept. And when importing a module has side effects (registering a codec, installing a polyfill, initialising a metrics collector), the module must be retained even if no exported symbol is referenced, which is why bundlers rely on explicit side-effect metadata.
  • Why doesn't dead-code elimination help with version conflicts or CVE reports?
    Both are decided at dependency-resolution time from declared metadata, which happens before any elimination pass. The unused subsystem's declared dependencies still enter the graph, still participate in conflict resolution, and still appear in the lockfile and SBOM that scanners and auditors read.
  • Where does modern tooling legitimately reduce the need for CRP-driven splitting?
    Inside a monorepo with fine-grained build targets and atomic release, where a target can depend on exactly the sub-library it needs and everything ships together. There the build system implements CRP at target granularity and the release-coupling cost largely disappears — but this does not extend across published-artifact boundaries with independent versioning.

Vacuum-packing luggage. Everything fits in the case now, but you are still travelling with someone else's belongings: if they are recalled at customs, or the owner repacks, your trip changes regardless of how little space the items take.

saying these in an interview costs you the question

  • "Tree shaking removes it, so the dependency is free" — conflates shipped bytes with release and resolution coupling.
  • Assuming dead-code elimination is complete and safe by default, ignoring reflection, service loaders and side-effectful module initialisation.
  • Believing vulnerability scanners and SBOMs are reachability-based; almost all are presence-based.
  • Claiming module systems like JPMS or OSGi let a consumer depend on part of a published artifact's release cycle — they scope visibility, not versioning.
  • The opposite error: denying that fine-grained build targets in a monorepo genuinely reduce CRP pressure.

context