Two projects expose similar helpers. One has no project references. It is missing from the solution most developers build every day. The obvious reaction is understandable: share the code, add the reference, and bring the lonely project back into the fold.
That cleanup can be exactly wrong.
A recent architecture correction I reviewed was a useful reminder that repository shape is not the whole system. Build agents, installed workloads, runtime origins, signing steps, and release pipelines are part of the architecture too. An isolated .NET MAUI head looked neglected when viewed only from the solution file. Viewed from its release path, the isolation was load-bearing.
Start With the Delivery Graph
Before deduplicating across project boundaries, ask how each project is actually built and shipped.
In the case I reviewed, a server-focused solution deliberately excluded the mobile head. That kept routine server builds from requiring mobile workloads on every developer machine and CI runner. The mobile app was still real software: a separate pipeline restored, built, signed, and released it independently.
Adding a shared project reference would therefore do more than remove a few repeated lines. It would expand the mobile restore graph and couple its release path to a project the mobile pipeline had never needed. Adding the mobile project to the server solution would impose a different cost in the other direction: machines building the server product would now need tooling they did not otherwise use.
Neither consequence is visible in a tidy dependency diagram. Both matter in production delivery.
Similar Code Can Encode Different Assumptions
The same review contained two link resolvers that appeared ripe for consolidation. The browser implementation derived an external base address from its hosting origin. That was valid because the browser app was served by the web host.
The mobile app ran inside an embedded web view. Its internal origin did not identify the external service it needed to reach. Reusing the browser resolver would not create elegant sharing; it would import an invalid environment assumption.
This is a useful test for any proposed abstraction: are the implementations merely similar in syntax, or are they equivalent in authority and context?
Imagine a browser client and a desktop client that both produce a download URL. The browser can safely resolve a relative path against the page origin. The desktop client may need an explicitly configured service endpoint. A common interface can still be useful, but a common implementation may be actively misleading.
Duplication is not automatically good. It is sometimes the smallest honest representation of two different worlds.
Guard the Constraint—and Its Reason
Documentation corrected the misleading story, but the stronger improvement was an architecture test suite.
The tests did not merely assert that the mobile head had no project references and stayed outside the server solution. They also checked that the separate mobile build path still existed. That second half changes the character of the rule.
Without it, the test says: “keep this isolated forever.”
With it, the test says: “keep this isolated while this independent delivery path remains true.”
That is a healthier architectural guard. It protects a decision without pretending the decision is timeless. If the separate pipeline disappears, the guard fails and asks maintainers to reconsider the boundary. The test becomes executable rationale rather than fossilized preference.
This pattern generalizes well. If a component must stay dependency-free because it is loaded by a restricted host, test both the dependency rule and the existence of that host. If a migration assembly must remain separate because tooling discovers it independently, guard both the assembly boundary and the discovery path.
The Trade-Off Is Still Real
Deliberate isolation has costs.
Separate helpers can drift. A visual shell copied for a second head may receive fixes later. Architecture tests add maintenance, and source-shape checks can become brittle if they assert incidental text instead of meaningful structure.
The answer is not to ignore those costs. It is to compare them with the coupling being avoided.
Prefer the smallest duplication that preserves the boundary. Keep behavioural contracts aligned where the behaviour truly is shared. Document the assumptions that must differ. Review the boundary when release topology changes. And avoid broad claims such as “this project shares nothing” when the important truth is more precise: it ships independently and intentionally owns a narrow set of platform-specific choices.
A Practical Review Checklist
Before merging apparently duplicate code across application heads, ask:
- Which pipeline builds and releases each head?
- What workloads and SDKs does each build environment guarantee?
- Will a new reference expand the restore or signing boundary?
- Do the implementations rely on different origins, storage, identity, or lifecycle rules?
- Can we test the constraint and the continuing fact that justifies it?
- What event should trigger reconsideration of the decision?
These questions turn “DRY versus duplication” into a more useful discussion about operational ownership.
Architecture Includes the Way Software Leaves the Repository
The most valuable lesson for me was a leadership one: do not label unusual structure before tracing its history and delivery path.
An isolated project may be forgotten. It may also be a carefully bounded product whose release assumptions are invisible from the main solution. We should investigate before simplifying, explain the decision in language that cannot be mistaken for neglect, and encode the rationale where future changes will encounter it.
Clean architecture is not the fewest projects or the fewest repeated lines. It is a system whose dependencies match the realities of how it runs and ships—and whose exceptions are specific, reviewable, and allowed to expire.
Top comments (0)