Skip to content
A Bekkour
Writing

Vendor the small algorithm, don't add the dependency

5 min read

Here’s a take that gets me disagreed with: for a lot of small, stable, well-understood code, adding a dependency is the lazy choice, and vendoring it — copying the implementation into your own codebase and owning it — is the senior one. The industry reflex runs the other way. “Don’t reinvent the wheel,” “there’s a package for that,” and off you go adding a line to your manifest. I think that reflex is right often enough that people stop asking when it’s wrong.

I hit this deciding how to schedule flashcards in an iOS app. I wanted FSRS — the modern spaced-repetition algorithm Anki ships. There are perfectly good FSRS packages. I vendored the math instead, and I’d make the same call again.

What a dependency actually costs

The value of a dependency is obvious — someone else wrote it and maintains it. The costs are quieter, and for a small package they often outweigh the value:

  • Resolution risk. Every dependency is a constraint the resolver has to satisfy forever. Add enough and you get version conflicts, a slow resolve step, and the occasional “can’t build until we untangle this.”
  • Supply-chain surface. Every package is code you didn’t write running in your app, plus a publisher who could be compromised. For a huge, well-run library that’s a fair trade. For a hundred lines of math, you’ve widened your attack surface to save an afternoon.
  • An upstream that can break you. The maintainer can change an API, drop your platform, or just stop. Now your build depends on someone else’s Tuesday.
  • Churn you don’t need. A dependency that updates constantly is updates you have to track, even when nothing you use changed.

None of that is a reason to never take a dependency. It’s a reason to notice you’re paying it.

Why FSRS was a vendor, not a dependency

FSRS-5 is the specific kind of thing vendoring is for: small, stable, well-specified, and correctness-critical. It’s a published algorithm with a fixed, widely-shared 19-weight parameter vector and multiple reference implementations that agree. The math does not move. So I copied it into a small, dependency-free module — a comment citing where it came from, and a pure function:

// R(t) = (1 + FACTOR * t / S)^DECAY — the FSRS-5 forgetting curve.
private let DECAY: Double = -0.5
private let FACTOR: Double = pow(0.9, 1.0 / DECAY) - 1.0   // so R == 0.9 exactly when t == S

public func retrievability(elapsedDays: Double, stability: Double) -> Double {
    guard stability > 0 else { return 0 }
    return pow(1.0 + FACTOR * max(0, elapsedDays) / stability, DECAY)
}

What that bought me: a module with zero dependencies that resolves instantly, compiles on every platform I care about, and has no upstream that can break my build. And because I owned it, I could make it pure — no Date(), no global state, the caller passes in elapsedDays — which meant I could unit-test the whole scheduler deterministically, on macOS, in milliseconds. A third-party package optimized for its own API might not have handed me that.

The cost of the dependency would’ve been paid on every resolve, every update, forever. The cost of vendoring stable math was paid once, the day I copied it in.

When vendoring is the wrong call — and it often is

I’m not telling you to reimplement everything. Vendoring is wrong more often than it’s right, and the line is clear once you name it:

  • Anything that changes. If the thing gets patched — bug fixes, new features, evolving specs — take the dependency and let the maintainer do that work. Vendoring a moving target means manually chasing every upstream change, which is worse than the dependency you were avoiding.
  • Anything security-sensitive. Never vendor crypto, TLS, auth, or parsers exposed to hostile input. Those need a steady stream of security updates, and “I copied it in once” is exactly how you end up running a known-vulnerable version. Take the dependency, and update it.
  • Anything big. A hundred lines of stable math is a vendor. A ten-thousand-line framework is not — you will not maintain it as well as its authors, and copying it in is just a fork you’ll regret.
  • Anything you won’t actually own. Vendoring isn’t copy-paste-and-forget. It’s taking responsibility: cite the source, test it, and know where to pull fixes from. If you won’t do that, take the dependency.

FSRS-5 clears every one of those: it doesn’t change, it’s not security-sensitive, it’s small, and I tested it and noted its origin.

The actual principle

The default shouldn’t be “add a package,” and it shouldn’t be “write it myself.” It should be match the decision to the code. Small, stable, well-specified, correctness-critical, rarely-updated → vendoring is often the disciplined choice: fewer moving parts, no upstream risk, and code you can test and reason about completely. Large, evolving, security-sensitive, or genuinely someone else’s expertise → take the dependency and keep it updated.

Vendoring has a bad reputation because people picture the worst version of it — a stale, forked copy of something that needed patches. Done right, on the right code, it’s the opposite of sloppy. It’s refusing to take on a lifelong cost to avoid a one-time one.


I make these build-vs-buy calls constantly, and I have opinions about them formed by living with the consequences. If you’re weighing one, I’m happy to argue it through. The full FSRS implementation and why it’s a pure function is its own post.

Al Mokhtar Bekkour

Senior Rails & Go engineer in Quebec. I build multi-tenant SaaS and financial integrations, and ship my own products — Constat, Railyard, and Recall. Open to work.

← All writing