There's a test I run on every codebase I build or inherit. I call it the Deletion Test.
It works like this:
> Pick any feature directory. Delete it. Does the rest of the system still compile, build, and pass tests?
If yes — the architecture is sound. If no — you have a problem that will compound every time you add something new.
This sounds almost too simple. But in practice, it catches more design problems than any formal architecture review I've seen.
Why Deletion Is a Better Signal Than Addition
When engineers think about module quality, they usually think about *adding things*: adding tests, adding documentation, adding abstraction layers. But addition is a lagging indicator. You can add tests to poorly-coupled code and still have a mess.
Deletion is a *leading* indicator. If a module resists deletion, it means something outside it knows too much about its internals. That coupling is a latent liability — invisible on green builds, but devastating when requirements change.
The deletion test forces you to verify that your module boundaries are real, not aspirational.
What the Deletion Test Catches in Practice
1. Cross-domain imports from internal files
The most common violation I find:
// ❌ apps/website/src/components/home/SomeWidget.tsx
import { SuggestionsRepository } from '@domains/suggestions/suggestions.repository';
// This import reaches *inside* the domain — past the public index.ts.
// If you delete the suggestions directory, this widget breaks.The fix is always a public API contract:
// ✅ domains/suggestions/index.ts — the ONLY file other domains import from
export { SuggestionsService } from './suggestions.service';
export type { Suggestion, SuggestionStatus } from './suggestions.types';
// DO NOT export: repository, policy, dto internalsNow the widget imports from @domains/suggestions — not suggestions.repository. Delete the entire directory, and the import error is localized to one line in index.ts, not scattered across the codebase.
2. Shared utilities that aren't really shared
Another common case:
// ❌ shared/utils/suggestions-formatter.ts
// This utility only formats suggestion data.
// It has no business being "shared."Shared directories attract code that doesn't belong anywhere obvious. The result: modules that can't be deleted because their logic leaked sideways into shared/.
My rule: if a utility is only used by one domain, it lives inside that domain. Shared means *genuinely used by two or more unrelated domains.*
3. Database schema coupling
This one is subtle but important:
// ❌ domains/admin/admin.repository.ts
const result = await db.suggestion.findMany({
where: { status: 'PENDING_MODERATION' }
});
// The admin domain is directly querying the suggestions table.
// If you ever delete the suggestions domain, admin's DB query breaks.The fix: cross-domain data access goes through the owning domain's service interface, not raw Prisma queries:
// ✅ admin domain calls the suggestions service, not the DB directly
import { SuggestionsService } from '@domains/suggestions';
class AdminSuggestionsController {
constructor(private readonly suggestionsService: SuggestionsService) {}
async getPending() {
return this.suggestionsService.getPendingModeration();
}
}Now the admin domain is coupled to the *interface*, not the implementation. If the suggestions domain changes its storage layer, admin doesn't care.
How to Run the Deletion Test in a Real Project
Here's the actual process I follow:
# 1. Back up current state
git stash
# 2. Delete the target domain directory
rm -rf apps/api/src/domains/suggestions
# 3. Run the compiler — not the tests, just the compiler first
pnpm tsc --noEmit 2>&1 | head -50
# 4. Count the errors
pnpm tsc --noEmit 2>&1 | grep "error TS" | wc -l
# 5. For each error: is this legitimate (the deleted feature was actually used here)?
# Or is this a leaking internal import that should have gone through index.ts?
# 6. Restore
git stash popA clean domain should produce zero TypeScript errors outside its own directory when deleted. External errors mean external coupling.
The 5-File Rule Corollary
The Deletion Test has a partner: the 5-File Rule.
> Any typical feature modification should touch no more than 5 files — all within the same domain directory.
This isn't a hard limit. It's a smell detector. If adding a new field to an entity requires touching 8 files across 4 directories, the architecture is resisting the change. That resistance is a design problem masquerading as a scope problem.
When I see a PR touching domains/suggestions/, shared/utils/, infrastructure/database/, app.ts, and a component in the website — I slow down and ask: what coupling allowed this simple change to spread this far?
Applying This to Legacy Codebases
Most codebases aren't greenfield. They're inherited, patched, and organically grown. The deletion test still applies — it just surfaces problems rather than preventing them.
My triage process for a legacy codebase:
- Map the
importgraph — usemadge --circular ./srcto find circular dependencies first - Identify the highest-coupling files — these will have the most incoming imports
- Extract a public interface — for each high-coupling module, create an
index.tsthat exports only what external callers should see - Run the deletion test per domain — even if deletion fails, count the errors. That count is your coupling debt metric
- Reduce coupling iteratively — each PR should reduce the deletion error count, not increase it
The Real Value
The deletion test isn't about deleting features. You'll rarely actually delete a whole domain.
It's about building with the expectation of change — which is the only reasonable expectation in software.
Requirements change. Features are removed. Business pivots happen. A codebase that passes the deletion test survives these events because its modules are genuinely independent. A codebase that fails the test turns every pivot into a rewrite.
The test is cheap to run. The discipline it enforces is priceless.
*This architecture principle is applied throughout the Portfolio Nexus codebase — you can see the domain boundary enforcement in the ESLint boundaries plugin configuration and the index.ts public API contracts.*