The rejected dependency: why the libraries you didn't install deserve a decision record
When a new engineer joins a codebase and asks "why aren't we using X?", the answer is usually one of three things: we never considered it, we considered it and rejected it for reasons that are now lost, or we evaluated it and built the same capability ourselves. Only the first case doesn't need a decision record. The rejected dependency is the most invisible category in a dependency graph — it isn't in package.json, it doesn't appear in any audit or dependency scan, and the deliberation that produced the rejection exists only in someone's AI chat history from fourteen months ago.
The pattern repeats across codebases at a predictable cadence. A library gets evaluated, a decision gets made not to add it, and the reasoning disperses immediately — into the memory of the engineers who ran the evaluation, into the AI chat sessions that contained the deliberation, into a Slack thread that archived itself nine months ago. Then a new engineer joins. They read the codebase, notice the absence of a library that seems obviously useful for the problem the codebase is solving, and propose adding it. The engineers who remember the evaluation have to reconstruct reasoning from memory. If they can't reconstruct it well enough to be convincing, the proposal goes to a vote that costs the team four hours of discussion and produces the same outcome as the original evaluation. If no one who ran the original evaluation is still on the team, the re-evaluation is treated as a fresh question — which it is, because the prior reasoning no longer exists.
This is the dependency re-proposal cycle, and it happens to most teams at least once per rejected dependency per fourteen months of team turnover. The cost is not just the engineering time for the re-evaluation. It is also the erosion of the team's confidence in its own decisions. When the same question keeps being re-opened, the implicit message is that prior deliberation was insufficient — which is often false. The prior deliberation may have been thorough and correct. What was insufficient was the documentation.
The most commonly missing ADR type is the "not building this" record — the decision not to implement a feature, not to adopt a pattern, not to pursue an approach. The rejected dependency is the dependency-management version of this record, and it has the same characteristic: the absence is the artifact. There is nothing in the codebase to find, which means there is no artifact to prompt the documentation, which means the reasoning disappears without a trace.
The asymmetry of re-proposals
The dependency re-proposal has a structural disadvantage for the team defending the prior decision. The engineer proposing the library has done fresh research. They have read the documentation, evaluated the current API, checked the recent release history, and formed a view based on the library as it exists today. Their case is well-grounded and current. The engineers who ran the original evaluation are working from memory. Their case is grounded in reasoning they completed fourteen months ago, which may or may not accurately reflect what they actually found during the evaluation.
Memory reconstruction under challenge is not reliable. The engineer who rejected a library for transitive dependency reasons may remember "we were concerned about the security surface" but not the specific dependency that concerned them. The engineer who rejected a library for maintenance reasons may remember "the maintainer wasn't responsive" but not whether that observation came from checking the issue tracker, from a direct support inquiry, or from a team member's prior experience. The specificity that made the original decision correct has degraded.
The proposing engineer, meanwhile, can point to the library's current npm download count, its recent commit history, its current maintenance status. If the library has improved since the original evaluation — which is often the case for libraries that were rejected for immaturity rather than fundamental problems — the team is now defending a stale decision against a current evaluation, and the burden of proof runs the wrong way. The team must prove that their prior reasoning still applies, when what they should be doing is evaluating whether the specific constraint that drove the rejection remains true today.
A rejection record converts this dynamic. Instead of reconstructing reasoning from memory against a prepared proposal, the team points to the record: here is what we evaluated, here is the specific constraint that drove the rejection, here is whether that constraint still applies. The proposing engineer can now make a case that the constraint has changed — that the transitive dependency has been dropped from a more recent version, that the maintainer has become more responsive, that the capability gap they identified has been addressed. This is a much more productive conversation than a memory contest about what someone concluded fourteen months ago.
Five rejection reasons worth documenting
Not every library evaluation that concludes with "we won't add this" produces a decision worth recording. A library that was mentioned in passing and immediately dismissed because it's clearly the wrong category doesn't require documentation. What requires documentation is a library that was seriously evaluated — where an engineer or a team spent time reading the documentation, comparing it against alternatives, or prototyping with it — and was rejected for a specific, articulable reason. Five categories of rejection reason produce decisions that are consistently worth documenting.
The security-surface reason. A library was evaluated and rejected because of what it brings in. The rejection wasn't about the library's own behavior but about its transitive dependency tree: a specific dependency with a poor security track record, an unusual network access pattern, an unusually large footprint for the problem being solved, or a supply chain concern (the library's release process was assessed and found to be at risk of package hijacking). Security-surface rejections have a specific time dimension: they're often correct at the time of evaluation and may stop being correct when the library drops the problematic transitive dependency in a later version. The record should name the specific concern — not "we were worried about security" but "lodash-merge version X carries a prototype pollution vulnerability that has not been patched in the current release line, and the library's transitive dependency on it means we'd be importing the exposure." This level of specificity lets a later engineer check whether the concern remains live without re-running the full evaluation.
The maintenance-burden reason. A library was evaluated and rejected because the maintenance situation was assessed as a risk. The three maintenance signals that produce this rejection: release cadence that has slowed significantly below the stated support commitment; issue tracker behavior where opened issues age past 90 days without response or acknowledgement from maintainers; bus factor assessment where the library's commit history shows a single maintainer who hasn't committed in six months. The maintenance-burden rejection is especially prone to stale record failure: a library that was slow in 2024 may be actively maintained in 2026 following a new maintainer adoption. The rejection record should name which signal drove the decision — not "the library seemed unmaintained" but "the core maintainer's last commit was 11 months before we evaluated, there were 47 open issues with no responses, and the npm downloads had fallen 60% year-over-year." This specificity lets the evaluation be re-run quickly: has the maintainer situation changed? Are issues now being responded to? Has download trend reversed?
The native capability sufficiency reason. A library was evaluated alongside the native language or runtime feature that addresses the same problem, and the native feature was found to be adequate. This is the rejection type most likely to be re-proposed, because the absence of the popular library looks like an oversight to a new engineer. The team isn't using date-fns because they don't know about it; they're not using it because they evaluated it against Intl.DateTimeFormat and found that the native API covered their specific use cases without the additional surface. The record should name both sides of the comparison: what native feature was evaluated as the alternative, which specific capabilities of the library were found unnecessary for the team's use cases, and what the team would do if they encountered a case that native doesn't handle. "We evaluated date-fns against the native Temporal API for our date arithmetic and formatting needs. The library's main advantages — timezone handling and locale support — are available in Temporal for our four supported locales. We'll add the library if we need calendar arithmetic across multiple calendar systems, which is not a current use case." This record is easy to evaluate when the proposal comes back: has the use case appeared? Has the native API changed?
The deliberate dependency diet reason. Some teams maintain an explicit architectural policy of minimizing dependency count. The reasons are specific: dependency count directly affects the surface area for security review; a team doing annual SOC 2 audits reviews every direct dependency; a team deploying to AWS Lambda has a bundle size constraint that makes each new dependency expensive; a team building a library intended for embedding in other projects minimizes dependencies to reduce the integration burden on downstream consumers. When a library is rejected for dependency diet reasons, the record should name the policy and the constraint that produced it, not just "we try to keep dependencies minimal." An unnamed policy is not a policy — it's a preference that the next engineer can override without understanding what they're overriding. "Our Lambda function has a 250KB unzipped size target to stay under the default 50ms cold start budget at our traffic volume. Adding this library would bring the bundle to 310KB. We evaluated the capability against a manual implementation that adds 1.8KB; the library adds 62KB. We'll use the manual implementation unless the library's capability becomes necessary." This record converts a constraint into a calculable question: has the Lambda size budget changed? Has the library been tree-shaken to a smaller footprint?
The internal implementation decision. The library was rejected because the team built the same capability internally. This is the most consequential rejection to leave undocumented, because the absence of documentation creates a specific and predictable problem: a new engineer discovers the internal implementation, concludes that it is technical debt (an undocumented custom solution where a well-maintained library exists), and proposes replacing it with the library that was originally rejected. Without a rejection record, there is no way to establish whether the internal implementation was deliberate or an accident of history. "We looked at express-rate-limit and rejected it because our rate limiting logic needed to be aware of our tenant isolation model, which requires rate limit buckets that span multiple request paths based on billing account identity rather than IP address or route. The library's bucket model is per-route-and-IP; our bucket model is per-billing-account. We built the rate limiter internally because the mapping from billing account to request identity wasn't expressible in the library's configuration surface. Revisit this decision if express-rate-limit adds support for custom key resolution that can span multiple routes." This record answers the new engineer's question before they spend a sprint attempting a migration that encounters the same constraint the original engineer did.
The internal implementation problem in detail
The "we built it ourselves instead" decision is the rejection category that most frequently produces cascading undocumented decisions. The rejection of the external library was the decision to build; the internal implementation then accumulated decisions of its own — API shape, behavior under edge cases, error handling, retry logic — none of which have a corresponding decision record because they were built as part of the implementation, not recorded as architecture choices.
A new engineer encountering a custom implementation without a rejection record goes through a characteristic sequence. They find the implementation. They look for documentation explaining why it exists. They find none. They check package.json to see if the popular library for this problem is installed. It isn't. They conclude either that no one knew about the library (oversight) or that the team hasn't gotten around to replacing the custom implementation (technical debt). Both conclusions produce the same proposal: replace the internal implementation with the standard library.
If a rejection record exists, the sequence changes at the third step. The engineer finds the rejection record linked from the implementation's header comment or from the decisions directory. The record explains that the library was evaluated, names the specific constraint that made it unsuitable, and names the condition under which the decision should be re-evaluated. The engineer can now answer the correct question: has the constraint changed? If express-rate-limit has added per-billing-account key resolution since the original evaluation, the migration case is strong. If it hasn't, the custom implementation remains the correct solution and the proposal is closed without a re-evaluation sprint.
The new-CTO onboarding problem takes a specific form with internal implementations: a new technical leader sees a custom solution where a standard library exists and reads it as a signal about the team's engineering culture — they rolled their own instead of using the standard tool. The rejection record is the document that converts this inference from a culture judgment into an engineering explanation. The custom implementation isn't evidence of not-invented-here syndrome; it's evidence of a deliberate decision against a library that didn't fit a specific constraint, with the implementation built to serve that constraint. The record is the difference between these two readings.
The transitive dependency problem
The security-surface rejection is particularly prone to a failure mode that makes documentation especially important: the concern is about something inside the dependency, not the dependency itself. A library can fix the transitive dependency problem that caused its rejection without making any visible change to its own API or behavior. The rejecting team would have to re-audit the library's dependency tree to notice the change. Without a rejection record that names the specific transitive dependency concern, there is no entry point for this audit — no way to know that the question should be re-asked.
The transitive dependency concern compounds across the ecosystem. The npm package ecosystem has a well-documented pattern of libraries pulling in large numbers of transitive dependencies that the library's authors didn't write and don't maintain. A team that rejects lodash for lodash-merge's prototype pollution vulnerability isn't rejecting lodash as a library; they're rejecting a specific version's specific transitive exposure. If lodash-merge fixes the vulnerability, or if lodash drops the dependency, the rejection reason has changed. If the record doesn't name the specific concern, the team cannot answer the question "is the reason we rejected this library still true?"
The supply chain concern is the hardest version of the security-surface rejection to document precisely, because it's often based on assessment rather than a specific named vulnerability. A team that rejects a library because "the publish process looked like a single maintainer with no 2FA on the npm account, which puts the package at risk of account hijack" has a legitimate concern that is specific to a moment in time and a state of the ecosystem. If the maintainer adds a second committer, or if the npm registry adds additional verification requirements, the concern may be addressed without the team's knowledge. The record should name the specific concern and ideally a named signal that would let the concern be re-evaluated: "we'll reconsider this library if the maintainer joins the npm security key signing program or if a major maintainer with an established track record formally adopts the project."
The dependency diet as an architectural decision
When a team maintains a deliberate policy of minimizing dependency count, the policy itself deserves documentation as an architectural decision, and individual rejection decisions reference it. The policy record names the reasoning behind the diet: deployment constraints (Lambda size limits, CDN bundle budgets, embedded library integration requirements), audit requirements (SOC 2 dependency review scope, SBOM generation requirements), or philosophical stance (the team has decided that the maintenance burden of a large dependency tree exceeds the benefit of using pre-built solutions for small problems).
Without the policy record, individual rejections look like arbitrary decisions. With it, they look like consistent application of a documented constraint. The new engineer who asks "why aren't we using X?" gets a two-part answer: the individual rejection record explains why this specific library was rejected, and the policy record explains why the team evaluates dependency additions with higher friction than the default.
The policy record also needs a re-evaluation condition. A dependency diet policy that was correct for a team with a Lambda constraint may become unnecessary if the team moves to a containerized deployment without the bundle size concern. A policy that was correct when the team was doing SOC 2 Type I preparation may need revision after the audit is complete and the team has established a dependency review cadence that accommodates a larger footprint. The policy ADR should be superseded when the constraint that produced it changes, not silently abandoned when engineers start adding dependencies that violate it without incident.
Writing the rejected dependency record
The Nygard ADR format works directly for rejected dependency records. The decision-statement title convention applies with the verb "rejected" or a comparison construction:
- "Rejected moment.js in favor of native Intl.DateTimeFormat for date formatting" — the classic native-sufficiency rejection
- "Built internal rate limiter over express-rate-limit — billing-account key resolution required" — the internal implementation decision
- "Rejected axios in favor of native fetch — browser compatibility and bundle size" — the native API sufficiency rejection for a library that was once necessary but became redundant
- "Excluded left-pad — deliberate dependency diet for npm publish surface" — the policy-driven rejection (historical example but the format holds)
The title should make the comparison visible at the list level without opening the file. A new engineer scanning a decisions directory with these titles can answer "was X evaluated?" in one second per title rather than opening files to check.
Context. The problem being solved, the library evaluated, and why the library appeared as a candidate. This establishes that the evaluation was serious — not a dismissal, but a deliberate assessment of a plausible option. "We needed date arithmetic and formatting across four locales for the dashboard date range selector. date-fns appeared as the standard choice for this problem in the team's initial research; it is the most widely used JavaScript date library and has strong TypeScript support."
Alternatives Considered. The evaluated library and the alternatives, including the path chosen. This is the inverse of the typical ADR structure, where the alternatives are the paths not taken: in a rejection record, the rejected library is an alternative, and the path chosen is the actual decision. "date-fns (version 3.x, 78KB gzipped direct footprint, 12 transitive dependencies). Chose over date-fns: native Temporal API with polyfill (8KB via temporal-polyfill, zero transitive dependencies, covers all four locales needed). Also evaluated: Luxon (45KB, similar locale coverage, would still add 10 transitive dependencies). Native Date object (no dependencies, insufficient timezone support for our Europe/London use case)."
Decision. What was chosen and the constraint that drove the selection. "We chose native Temporal API with temporal-polyfill over date-fns because our specific use cases (date range display, relative date formatting in four locales, business day arithmetic in Europe/London timezone) are covered by the Temporal API without requiring date-fns's full feature set. The bundle size difference (8KB vs. 78KB unzipped at the import granularity we'd use) was the secondary consideration given our Lambda bundle size constraint."
Consequences. What was accepted. This section names the real trade-offs of not installing the library, not just the reasons the decision was correct. "We accepted that date-fns's helper functions (differenceInBusinessDays, format, add) will need to be implemented in a local utility if our date calculation needs expand beyond the Temporal API's coverage. We accepted the temporal-polyfill maintenance dependency until Temporal reaches baseline availability in our supported browser set. We did not evaluate date-fns's tree-shaking granularity — if our bundle size constraint changes, this evaluation should be re-run with the actual import footprint rather than the total package size."
Revisitation condition. Named triggers under which the decision should be re-evaluated. "Re-evaluate this decision if: (1) the date calculation use cases expand to require calendar arithmetic across non-Gregorian calendar systems; (2) the Temporal polyfill adds breaking changes or is abandoned; (3) our Lambda bundle size constraint changes such that the size difference becomes acceptable; (4) date-fns 4.x significantly reduces its bundle footprint and drops transitive dependencies."
The record does not need to be long. A well-written rejection ADR with all five sections is 300–500 words. The discipline is in naming the specific constraint that drove the rejection (not "we decided native was good enough" but "Temporal covers our four specific date formatting use cases without the 78KB overhead") and the specific revisitation condition (not "revisit if needs change" but "revisit if calendar arithmetic across non-Gregorian systems becomes a requirement").
Finding rejected dependencies in AI chat
The WhyChose extractor finds rejected dependency decisions through a session shape that is characteristic but distinct from upgrade decision sessions. Dependency rejection sessions begin as evaluation questions — "should I use X for Y?" or "what's the best library for Z?" — and conclude with a decision not to install the most commonly proposed option. The session contains evaluation content (the engineer researched the library, considered its API, thought about integration) but produces no artifact: no package.json change, no import statement, no configuration file.
This makes rejected dependency sessions particularly valuable as extraction targets and particularly hard to locate. There is no git commit to anchor a date range search. The session appears in the AI chat export as a dependency evaluation that concluded with a non-installation, which leaves it invisible to the standard search path of "find the commit where X was added and look at the chat from the weeks before." For rejected dependencies, the standard path produces nothing.
Three search strategies work for rejected dependency sessions. First, search for the library name directly in the AI chat export. A library that was seriously evaluated will appear by name in one or more sessions. The WhyChose extractor finds these through named library mentions combined with comparison language and outcome markers. Second, search for the capability description rather than the library name: "date formatting library," "rate limiting middleware," "state management for React" — engineers often begin evaluations with a capability description before settling on specific library names to compare. Third, use the quarterly decision review as the mechanism: a systematic extraction pass across all sessions from the past 90 days surfaces rejection decisions alongside forward decisions. The extractor's rejection session signal ("ended up not adding it," "we'll just use native," "decided to write it ourselves") is high-confidence enough to make these sessions findable in a pass that isn't specifically looking for them.
Writing the rejection record changes the evaluation. An engineer who opens the ADR template before concluding a library evaluation and tries to fill in the Alternatives Considered section for a native or internal implementation alternative may discover that the alternative hasn't been seriously evaluated — the library was simply the default candidate, and its rejection hadn't been checked against a specific alternative. The blank Alternatives section is diagnostic: if the engineer can't name what they evaluated the library against, the evaluation is incomplete. Opening the template before committing to "we'll just use native" surfaces the evaluation gap.
The dependency rejection log as a capability map
A collection of rejection records is also an implicit map of the team's capability evaluations. When a team has records for rejected date libraries, rejected HTTP clients, rejected state management solutions, and rejected logging frameworks, the collection shows not just what the team doesn't use but what problems the team has thought carefully about. A new engineer reading the rejection log learns where the team has done deliberate evaluation and where the current choices are the result of informed selection rather than default adoption.
The collection also surfaces patterns in the team's rejection reasoning. A team that has rejected three libraries in the past 18 months for maintenance-burden reasons is expressing a consistent risk tolerance for dependencies with uncertain long-term support. A team that has rejected multiple libraries for transitive dependency concerns is expressing a consistent security posture. These patterns are architectural — they represent real decisions about what kind of dependency graph the team is willing to maintain — and they are invisible without the rejection records that encode them.
The rejection log is also the correct place to document deliberate dependency minimalism when the reason is philosophical rather than constraint-driven. Some teams have a genuine preference for dependencies that do one thing and are small and auditable over dependencies that do many things and are large and opaque. This preference shows up as a consistent pattern across rejection decisions. It is worth naming explicitly: "We evaluate dependencies against a minimalism threshold — the library should be justified by what it provides beyond the native alternative, and the gap should be specific enough to define a re-evaluation condition." An explicit statement of this preference, linked from individual rejection records, converts a culture signal into a documented policy.
The lifecycle of a rejection record
Rejection records are not permanent. A library that was rejected for immaturity in 2023 may be the correct choice in 2026. A library that was rejected for a transitive dependency that carried a CVE may have dropped the dependency in a subsequent release. The record should be reviewed and updated when the named revisitation condition is met, not left as permanently closed.
When the revisitation condition is met and the library is re-evaluated, the outcome is a new decision that supersedes the rejection record. If the re-evaluation concludes that the rejection should stand (the library has improved, but not enough to change the decision), a brief amendment note on the original record is sufficient: "Re-evaluated 2026-03 following date-fns 4.0 release. The bundle size has been reduced but remains 45KB unzipped at our import granularity. Temporal API still covers our use cases. Decision stands; revisit if date-fns reaches bundle parity with the polyfill." If the re-evaluation concludes that the library should now be adopted, a new adoption ADR supersedes the rejection record, and both records cross-link to complete the chain of custody.
The chain of custody across a rejection and subsequent adoption is the most valuable artifact the rejection record enables. A team that adopted date-fns in 2026 after rejecting it in 2024 has a record showing why the 2024 rejection was correct (the bundle size and transitive dependencies made it the wrong choice at the time) and why the 2026 adoption was correct (the 4.0 release addressed the specific concerns). The two records together are a more complete picture of the team's dependency management than either alone — and the dependency is adopted with full understanding of what the earlier evaluation found, rather than in ignorance of it.
What the rejected dependency record protects
The rejected dependency record protects four things that are otherwise lost when a serious evaluation concludes with a non-installation decision.
It protects the reasoning that justified the rejection. The constraint that drove the decision — security exposure, maintenance risk, native sufficiency, deployment constraint — is preserved in a form that is retrievable and evaluatable rather than dependent on the memory of the engineer who ran the evaluation.
It protects the team's time. The four to eight engineering hours that a well-run re-evaluation consumes are saved every time the record answers the question before the full evaluation is initiated.
It protects the team's internal implementations. A custom solution built to serve a specific constraint that the popular library didn't meet is preserved as a deliberate decision rather than misread as accidental technical debt.
It protects the architectural intent when the intent is a deliberate dependency diet. A team that has chosen to minimize its dependency surface for principled reasons has that intention documented and retrievable, rather than subject to incremental erosion as each new engineer adds one more dependency that seems obviously useful for the problem they're solving.
The case for architecture decision records is that the decisions worth documenting are the ones where a new team member would behave differently knowing the reasoning. The rejected dependency meets this test completely. A new engineer who knows why a library was rejected — the specific constraint, the specific alternative that was chosen instead, the specific condition under which the decision should be re-evaluated — will behave differently than one who doesn't. They will not propose the library without checking the record. They will not mistake the custom implementation for accidental technical debt. They will not erode the dependency diet by adding the library that the team has decided to avoid. The record is the instrument that converts a past deliberation into current guidance — and for rejected dependencies, that guidance is available nowhere else.
Further reading
- Decisions that never get written down — the "not building this" record type; the rejected dependency is its dependency-management equivalent: an absence that needs a record more than a presence does, because the presence is self-documenting and the absence is invisible
- The dependency upgrade decision record — the companion piece: documenting the upgrade decision; the rejected dependency record is the inverse, documenting the non-installation decision; both are triggered by the same evaluation work and produce the same absence of artifact from that work
- The ADR title convention — rejection records should use the "Rejected X over Y" or "Chose native Y over library X" title format; the comparison must be visible at the list level to serve the filtering purpose
- Nygard ADR template — the base format works directly for rejected dependency records; Context names the problem and the library evaluated, Alternatives Considered names the library and its alternatives, Decision names what was chosen instead with the specific constraint, Consequences names the trade-offs accepted by not installing the library
- How to document architecture decisions — the standard ADR practice; rejected dependency records are a category of ADR that most ADR guidance underspecifies, but the same principles apply: the constraint that drove the decision is more valuable than the description of the outcome
- WhyChose extractor — dependency rejection sessions in AI chat are high-value extraction targets because they contain evaluation content without a corresponding package.json entry; the session that concluded with "actually, native fetch handles this" or "let's just build it ourselves" is the content source for the rejection record
- The new-CTO onboarding problem — custom implementations without rejection records are systematically misread by new technical leaders as evidence of not-invented-here culture; the rejection record is the document that converts the inference from a culture judgment into an engineering explanation
- The ADR as a forcing function — opening the rejection record template before concluding an evaluation surfaces whether the alternative has been seriously evaluated; the blank Alternatives Considered section is diagnostic for evaluations that haven't examined what the native or internal alternative actually covers
- The quarterly decision review — the practical mechanism for finding rejection decisions across an AI chat export; the extractor's rejection session signal is high-confidence enough to surface these sessions in a systematic pass that isn't specifically looking for them