The Common Module
common/, shared/, utils/ — where code goes when nobody will own it. The cause is missing ownership, not laziness, and the fix has to address the cause.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
Why does every codebase grow a folder named for having no name, and what should go there instead?
A function is needed that turns an order into the line items a tax service expects. Orders should not know about tax, tax should not know about orders, and the deadline is Thursday.
Put it in common/. It is genuinely common to two modules, the name is accurate, and the alternative is a design argument nobody has budget for on a Tuesday.
The name is the problem: "common" describes who uses it, not what it is, so there is no criterion for what may be added and therefore no criterion for refusal (Naming).
- The name is the problem: "common" describes who uses it, not what it is, so there is no criterion for what may be added and therefore no criterion for refusal (Naming).
- It has no owner, because it was created precisely to avoid the ownership question. Unowned code is not maintained, only added to (Code Ownership).
- It becomes a dependency of everything, so it has the fan-in of a shared library with none of the discipline — no versioning, no contract, no compatibility obligation, and no way to know who depends on a given function (Fan-in and Fan-out).
- And it collects domain knowledge. The tax mapping goes in, and six months later
common/contains business rules that two modules depend on and neither maintains, which is the point at which a schema change breaks something nobody expected (Shotgun Surgery).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- Both plausible owners have reasons to refuse, and both reasons are good (Dependency Direction).
- There is a folder called
common/and putting it there requires no discussion with anyone. - Nobody has time this week for a conversation about where a concept belongs.
- Every piece of domain knowledge has exactly one owner who is responsible for it changing correctly (Designing by Responsibility).
- No module acquires a dependency on another module's rules by accident (Kinds of Coupling).
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Every function must have a module that owns it, meaning a team that would be asked before it changes and would notice if it broke.
- A genuinely generic, dependency-free utility — no domain knowledge, no I/O — can live in a narrow, named package with an owner. That is a real category and it is small (Shared Libraries).
- Somebody has to own the concepts that fall between modules, and the usual right answer is that a missing concept has been discovered and needs naming, not filing (Choosing the Model).
- Nobody should own a folder defined by usage, because a definition like that admits everything.
- The test for whether something may go in a shared location is whether it has any domain knowledge. A date formatter does not; a function that knows what an order line means does (Information Hiding).
- The second test is direction: a shared package must not import any module. If it needs to, the concept belongs in a module (Dependency Cycles).
- The third is naming. If the package can be given a name describing what it contains —
money,pagination,ids— it is a real module. If the only honest name is "things", it is a dumping ground with a euphemism (The Utility Dumping Ground).
The smell, and the case where it is genuinely fine
This is worth stating as a smell rather than a rule, because the shape has a legitimate version and treating every shared folder as a defect produces the opposite over-correction — a package per function, each with a build step.
The distinguishing property is not size and not the name. It is whether the folder has a criterion that can be used to refuse something. money can refuse a date formatter. common cannot refuse anything, and a container that cannot refuse will eventually contain business rules that nobody owns (The Utility Dumping Ground).
looks like A top-level directory named common/, shared/, utils/, helpers/, core/ or lib/, imported by every module, containing files whose names are categories rather than concepts — helpers.ts, utils.ts, misc.ts — and at least one function that mentions a domain noun. Its git history is almost entirely additions by many different authors and almost no deletions.
suggests Ownership is unclear somewhere specific, and this folder is where the ambiguity is being deposited. Each file in it is a concept that was never named, usually because two plausible owners each had a good reason to refuse it. The fan-in makes it the least changeable code in the system, and the absence of an owner means it is never improved, only extended (Afferent and Efferent Coupling).
fix Do not start with a cleanup. Start by giving the folder an owner and a written criterion — "no domain knowledge, no module imports, nothing that needs a fixture to test" — because a criterion is what makes refusal possible, and refusal is what stops growth. Then move by attrition: whenever you touch a file in there, name the concept it belongs to and give it to a module. Expect each move to surface a missing concept rather than a misplaced file (Extract Module).
utils/ admits. A chunk(array, size), a clamp(n, lo, hi), a branded-id helper, a Result type where the language lacks one: no domain knowledge, no I/O, no imports from any module, a definition that has not changed in two years and will not, and a name that describes exactly what it is. Code with those five properties is not a dumping ground even if it sits in a folder called utils/ — the fan-in is real but harmless, because a function that cannot change cannot ripple. The moment one of those functions grows a parameter named orderType, it has left this category.1src/common/2 utils.ts formatDate, slugify, chunk, deepEqual3 helpers.ts retryWithBackoff, sleep, memoize4 constants.ts TAX_RATES, PLAN_LIMITS, FEATURE_KEYS <- rules5 types.ts Money, OrderLine, CustomerRef <- domain6 mappers.ts orderToTaxLineItems() <- Thursday7 validation.ts isValidEmail, isValidPlanChange <- rules8 db.ts withTransaction, buildWhereClause <- I/O9 10# Four categories in one folder, and only the first is fine:11# generic + stable + dependency-free -> keep, name it, own it12# business rules with no owner -> belongs to a module13# domain types with no owner -> belongs to a module14# infrastructure helpers -> belongs to a platform package15 16# TAX_RATES is the one to look at. It is a business rule, it is17# imported by four modules, and if it is wrong nobody is on the18# hook — which is exactly the property this folder creates.The file names are the tell. utils, helpers, constants, types are categories of code, not concepts in the domain, and a file named for a category has no criterion for what belongs in it either — so the accretion is fractal (Naming).
Why it happens, which is not laziness
The Thursday function has two plausible owners and both refusals are correct. Orders should not depend on tax, because tax is a specific integration and orders is a general concept. Tax should arguably not own a function about order lines either, though this is the weaker objection. Faced with two good reasons not to decide, and a deadline, common/ is the only move that requires no meeting.
That is why discipline campaigns fail here. The engineer is responding rationally to a structural gap, and the fix has to close the gap: name the concept, assign an owner, and make the assignment cheap enough to do on a Tuesday afternoon (Designing by Responsibility).
- 1Ask what it is, not who uses it
Describe the thing in domain terms without naming its callers. "Translates an order into the tax vendor's line item format" is a description; "shared between orders and tax" is not.
fails by Answering with the consumer list, which is how it ended up here and which supports no decision.
- 2Check for domain knowledge
Does it know a business rule, a domain noun, or a format someone else owns? If yes, it belongs to a module and the only question is which one.
fails by Treating "it is only a mapping" as generic. A mapping between two domain models is one of the most domain-specific things in a codebase (Boundary Adapters).
- 3Name the concept
Give it a name that is not a category. "TaxLineItemMapping" is a name; "order helpers" is a category and will accrete exactly as its parent did.
fails by Skipping to the move. A file relocated without being named will be back within a quarter under a different path (Naming and Domain Language).
- 4Assign the owner by dependency direction
Give it to the more specific side, so the dependency points from specific to general. The tax integration owns translating into its vendor's terms; orders never learns tax exists.
fails by Splitting it across both, which produces the same knowledge in two places and a coordination requirement nobody records (Duplicate Knowledge).
- 5Move it and let the compiler find the callers
One move, one import update per caller, mechanical. Do this while it is small; the cost rises with every consumer added.
fails by Leaving a re-export shim in the old location "for compatibility", which means both paths exist forever and neither is authoritative (Deprecation).
- 6Record the exception if you cannot do it now
If Thursday wins, put it in common with a comment naming the intended owner and a ticket. Deliberate and bounded is a strategy.
fails by Absorbing it silently, which is how a folder of accidents accumulates and how nobody can later tell which entries were decisions (Deliberate Debt).
The step that does the work is the third. Most files in a common folder are a concept nobody named, and once it has a name the owner is usually obvious to everyone in the room within about ten seconds.
Where it actually goes
Having refused the easy home, you owe an answer. There are four real destinations and they are distinguished by two questions: does it contain domain knowledge, and does more than one module genuinely need the same behaviour?
Notice that only the last option is a shared location, and it is deliberately narrow — no domain knowledge, no module imports, a name that describes contents. Everything else goes to a module, which is the answer that felt unavailable on Thursday and is almost always correct (Dependency Direction).
It is needed by more than one module. What is the actual home?
when It encodes a rule, a format or a lifecycle that clearly belongs to one domain area. The other module calls its interface. This is the answer for the large majority of cases.
cost Creates a declared dependency between the two modules, which must be acyclic and must point from the more specific to the more general (Dependency Cycles).
when Neither existing module should own it and it is a real domain concept in its own right — a pricing policy, a translation between two models, a shared lifecycle. The refusal was a signal that a concept was missing.
cost A new module with an owner, an interface and tests. Fifteen minutes more than the common folder, and it is the fifteen minutes that decides the outcome (Extract Module).
when The two uses look alike and are not the same knowledge, or the code is small, unstable and cheap to change independently. A legitimate and under-used answer.
cost Two copies that could drift. Acceptable when a change would apply to one and not the other — and if you are unsure, note that a wrong abstraction is harder to detect than duplication (DRY: Knowledge, Not Lines).
when Genuinely generic, stable, dependency-free and free of domain knowledge — the chunk, clamp, Result category. Named for what it contains, never for who uses it.
cost Package setup and an owner, and permanent vigilance about the criterion: this is the option that decays back into common/ if nobody is allowed to refuse an addition (Shared Libraries).
How to build it
Most important first.
- Name the missing concept. The tax mapping is not common code; it is a translation between two models, which is a thing with a name and an owner — the tax integration owns translating into its vendor's terms (Boundary Adapters).
- Prefer giving it to one of the two candidates and letting the other call it. "Orders should not know about tax" is right; "the tax module may know what an order is" is usually fine, because the dependency points from the specific to the general (Dependency Direction).
- Where the code genuinely is generic, put it in a narrow named package with an owner and a test suite.
platform/clock,platform/ids— small, boring, no domain knowledge (Shared Libraries). - Give any existing common folder an owner and a stated criterion today, even a bad one, because a criterion enables refusal and refusal is the only thing that stops accretion (Design Review).
- Delete by attrition: whenever you touch a file in there, ask what concept it belongs to and move it. A dedicated cleanup project rarely gets funded; an attrition rule needs no budget (The Refactoring Loop).
- Watch the size as a signal. A
common/growing faster than the modules is telling you that ownership is unclear somewhere specific, and finding where is more valuable than the cleanup (What Technical Debt Actually Is).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Changing a function in
common/: unknown cost, because the consumer list is unknown. The honest estimate requires a search, and the search is the reason people avoid changing it — so it also becomes the code that never improves (Change Amplification). - Adding to it: free, immediately, which is exactly why it grows. The asymmetry between free addition and expensive change is the whole mechanism.
- Under the alternative — the concept named and owned — a change costs one module, one owner, one test suite, and the compiler lists the callers.
- The cost of the cleanup itself rises roughly with the square of the contents, because each move requires deciding both what the thing is and who should own it, and the hard cases are the ones left last.
- Refusing the easy home means the Thursday deadline meets a design conversation, and sometimes the deadline should win — provided the exception is recorded rather than absorbed (Deliberate Debt).
- Narrow named packages cost more setup than one folder, and at a small enough scale that overhead genuinely exceeds the benefit.
- Attrition cleanup means the folder is untidy for a long time, which is less satisfying than a big cleanup and considerably more likely to finish.
What can go wrong
- It is renamed rather than fixed —
common/becomesplatform/orcore/— and the identical accretion continues under a more respectable name. - A cleanup is attempted, half the files are moved, the remainder are the genuinely hard cases, and the folder is now smaller and no better structured (The Refactoring Loop).
- The criterion is written down and then never used to refuse anything, so it is documentation rather than a boundary (Documentation Decay).
- The mitigation fails in a particular way: a lint rule forbidding new files in
common/producescommon2/, or files added to the least-inappropriate existing file, which is worse because the accretion is now hidden inside modules that look owned.
- Everything depends on it, which is the same fan-in problem as a shared library and worse because nothing records who (Afferent and Efferent Coupling).
- It tends to acquire dependencies on modules, producing cycles that are invisible until someone tries to extract anything (Circular Dependencies).
- Its transitive dependencies become universal. A date library imported into
utils/is a decision made for the whole system by whoever needed a date last (Transitive Dependencies).
- "So never have shared code." Shared code is fine and often necessary. What is being refused is a location defined by who uses it rather than by what it is (Shared Libraries).
- "Rename it to
platform/and add a rule." The rule is the useful part; the rename without a criterion and an owner changes nothing but the import paths. - "It is only helpers, so it does not matter." It becomes the highest fan-in module in the system, and high fan-in with no owner is the least changeable code you have (Fan-in and Fan-out).
- "This is a discipline problem." It is an ownership problem. Engineers put things there because there is no obvious owner and there is a deadline, and blaming discipline leaves the cause untouched (Tone, Disagreement and Receiving Review).
- utility-dumping-ground
- duplicate-knowledge
- god-object
Testing it, and how it ages
- Anything genuinely generic should be trivially testable with no setup. If a
common/function needs a database or a fixture, it has domain knowledge and is in the wrong place (Purity and Testing). - Assert that shared packages import no module. It is one CI rule and it is the check that keeps domain knowledge out (Stable Dependencies).
- Before moving anything, characterize its behaviour — common utilities are the least tested code in most codebases precisely because they look trivial (Characterization Tests).
- Left alone, it grows monotonically and becomes the highest fan-in node in the dependency graph, which makes every subsequent structural change harder (Evolvability).
- With an attrition rule it shrinks slowly and, more usefully, each move surfaces a missing concept — a translation, a policy, a value type that should have had a name (Finding Seams).
- The end state worth aiming at is a small number of narrow named packages with owners, and no folder whose name describes usage rather than content.
- It reappears whenever ownership becomes unclear again, typically after a reorganisation. Treat a growing common folder as a symptom of an ownership gap rather than as a discipline failure (Code Ownership).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThe mechanism — no obvious owner plus a deadline plus a folder that accepts anything — does not depend on language, paradigm or stack, which is why the folder exists under a different name in essentially every codebase. What varies is only how fast it grows, which tracks how ambiguous module ownership is.
- CONTESTEDThe strongest opposing view: a small utility folder is a normal and healthy part of a codebase, the alternative is a proliferation of one-function packages with build overhead and ceremony, and the crusade against
utils/produces more friction than the folder ever cost. That is right about small codebases and right that the fix can be overdone — a three-person team with a fifty-lineutils.tshas no problem. It is wrong at scale, because the failure is not size but the absence of a criterion: a folder that cannot refuse anything will eventually hold business rules with no owner, and that transition happens without anyone noticing it. - SCALE-SPECIFICBelow a few thousand lines the whole codebase is one person's working memory, so an unowned utility file costs nothing and naming ceremony is pure overhead. The cost appears when the author of a helper and the person changing it are different people, and it becomes severe when nobody can enumerate the consumers — which is a codebase-size threshold rather than a headcount one.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Testing & Reliability Engineering — utility code is systematically the least tested in a codebase because it looks trivial, which is why characterizing before moving is worth the extra hour.
- — System Design — the organisational version of this is the team that owns the shared platform and is a bottleneck for everyone; the mechanism is identical and the fix is again ownership with a criterion rather than exhortation.