Software Design Reference System Designer · Designing Systems That Scale

Lesson 0026 · System Designer · Module 3

Libraries and Packages vs Building It Yourself

Every dependency you skip is code you maintain forever. Every dependency you take is a contract you must follow. This lesson gives you the decision procedure.

Reading ≈ 8 minPractice ≈ 40 min

Mission tie-in: "understand when to use libraries/packages vs building solutions yourself" — the last System Designer skill, and the bridge to Phase III's first trade-off, Build vs Buy.

Knowledge: three questions decide

The principle behind the principle

You never avoid the code. You only choose whose bugs you inherit and on whose schedule you fix them. Taking a dependency converts unknown-unknowns — edge cases you have not met — into a known recurring cost: upgrades, advisories, API churn. Building does the reverse: it converts a known recurring cost into unknown-unknowns that you will meet one at a time, in production, at the worst moment.

That is why "generic → take, yours → build" works, and it is not really about genericness. For a generic problem a large population of users has already walked the edge cases, so the library encodes evidence you do not have. For your own domain no such population exists, so a library can only offer you someone else's model of your problem — which is worse than no model, because you will spend forever translating into it.

Where it stops applying. Four cases flip the default. The library is not actually mature (a 3-day-old release, one maintainer, no changelog). You need 10% of it and it drags forty transitive dependencies into a build you must audit. Its model disagrees with yours, so every call is a translation and the impedance layer outgrows the thing it wraps. Or your constraints — air-gapped, regulated, licence-incompatible — make the vendor's contract more expensive than the code. Note that only one of those four is about the code being hard to write.

Ask three questions about the problem you're about to solve:

  1. Is it generic or is it yours? Serialization, auth, date math, HTTP — generic problems with mature solutions. Pricing logic, fraud rules, workflow semantics — your domain. Generic → library. Yours → build.
  2. Is the library boring? Mature, widely used, slowly changing (Dan McKinley's "choose boring technology"). A library three days old, or a fashionable rewrite every quarter, is a dependency you'll maintain at its pace, not yours.
  3. Can you wrap it? Any dependency worth taking is worth hiding behind a port (lessons 0009, 0023) so its churn never reaches your domain.
# take: JSON Schema validation? mature, generic, boring — take it
import jsonschema
jsonschema.validate(payload, schema)

# hide: the schema library's version churn stays inside the adapter
class PayloadValidator:
    def validate(self, payload: dict) -> None:
        try:
            jsonschema.validate(payload, self._schema)
        except jsonschema.ValidationError as e:
            raise InvalidPayload(str(e))

The cost of building: you own every bug, every missing edge case, every security review forever. The cost of taking: an external contract, upgrade cadence, and supply-chain surface. The tie-breaker is always the same — is this problem your business's differentiator? If yes, build (and own it deliberately). If no, take (and wrap it).

Worked example: fourteen partner feeds and "it's just CSV"

A team ingests daily CSV files from fourteen retail partners. The proposal on the table is to write the parser: it's just splitting on commas, forty lines, no dependency.

A senior designer does not argue about the forty lines. They go and get the data: pull two hundred real files and count how many violate the naive assumption. The result decides the conversation. Six partners emit a UTF-8 byte-order mark. Three quote inconsistently, so a quoted comma appears in one file a week. Two send European decimals (1.234,56). One appends a summary row that is not a record. Nine of fourteen partners break the forty-line parser, and none of those nine knows it.

The question that follows is "how many of these will I meet in production if I hand-roll, and how will I meet them?" — the answer being: all of them, one incident at a time, each discovered because a number in a report is wrong rather than because a parse failed.

The obvious library, pandas, is rejected — and the reason is the third boundary condition, not capability. It would drag a large dependency and a DataFrame model into a domain that wants one typed record per row, so every call site would translate between the two forever. The impedance layer would be permanent and would grow.

What ships is stdlib csv for tokenizing (mature, boring, zero dependencies) plus a schema library for typing and validation, both behind a FeedReader port. The partner quirks — decimal commas, the summary row — are genuinely yours and stay in your adapter, one small class per partner.

Price the difference honestly, because it is smaller than the argument suggests. The hand-rolled path met three of those edge cases in its first year; each cost roughly four hours to diagnose, two to fix, and a feed re-run, so about eighteen hours plus three out-of-hours interruptions. Learning and wiring the library cost six. The gap is not enormous — but the eighteen hours arrived as three surprises, and the six were spent on a Tuesday afternoon by choice. Surprise is the cost that no ledger line shows.

What "boring" means, made checkable: when was the last release, and the one before it? How long do issues stay open? How many maintainers can merge? Is there a changelog and a deprecation policy? Has the public API broken twice in two years? A library that fails three of these is not a dependency, it is a fork you have not taken yet.

† The AI special case: AI will happily "write a small library" for you instead of taking one — the generated code then becomes your hand-maintained dependency with zero of the library's maturity. Prefer the real library; use AI on your domain.
Field notes · take, build, or neither
The thing you needVerdictBecause
JWT signing, password hashingtakeA subtle bug here is a breach, not a defect.
date, timezone and currency arithmetictakeThe edge cases are a decade of other people’s bugs.
your pricing or matching rulesbuildIt is the product; a library makes you a reseller.
an AI’s 60-line retry helpertake the librarytenacity is battle-tested; the copy is not.
a wrapper over one library callneitherIt adds a name and contains nothing.
What people get wrong after reading this
"Fewer dependencies is safer."
Fewer dependencies means more of your own unreviewed code doing the same job. The metric that matters is not count but exposure: what each dependency can do, how it updates, and whether it is wrapped.
"It's only forty lines, I'll write it."
Forty lines is the happy path. The library is four thousand because the other 3,960 are the edge cases you have not met. This holds for date maths, currency rounding, retries, CSV, and every parser ever written.
"Wrap every library — that's the rule."
Wrap the volatile and the foreign-vocabulary ones. A stable, pure library whose vocabulary is already yours (decimal, datetime) is not improved by a wrapper; you have added a name and a hop.
"If we build it, we control it."
You control it while the author is still on the team and someone still budgets for it. Ownership without a named owner and a maintenance allocation is abandonment with extra steps — and abandoned internal code has none of an abandoned library's escape hatches.

When this is the wrong move

Build — or vendor a small, understood copy — when the library's model fights yours, when the transitive surface is unacceptable (regulated builds, embedded targets, cold-start-sensitive functions), or when the library is unmaintained and you would be forking it anyway. Build when you need 5% of something and that 5% is both simple and fully specified: a documented checksum is not a parser. In every one of those cases, write down that you now own it, with a name attached — the failure mode is not the decision, it is the decision nobody recorded.

Skill: take, build, or wrap?

JWT authentication for your API is:

Your pricing engine is your business's edge. The right call is:

Every external library you take should be:

Practice on your own code

List the last three "small utilities" you (or an AI) hand-wrote. Run the three questions on each: generic or yours? boring available? wrapable? Replace at least one hand-rolled utility with the mature library — and wrap it.

In the lab. The phase-2 checkpoint gives you a returns-resolution service that hand-rolls retry with backoff, ISO-date parsing, and money rounding. You run the three questions on each, replace the ones that fail, and put a port around whatever you take.

Reveal: a hand-rolled utility audit

An AI-generated "retry helper" with 60 lines of exponential backoff — hand-maintained, untested. The audit: generic (yes), boring (tenacity is battle-tested), wrapable (yes). Replaced with the library behind a RetryPolicy port; the AI's version went to the graveyard with its bugs.

Teach it back
  1. Explain to a junior, in two sentences, why "it's only forty lines" is an argument for taking the library rather than against it.
  2. Find one hand-written utility in your own code this week that a mature library covers. Name its cost: which edge case is it missing, and who will find it?
  3. Predict what happens in a year if its author leaves: who fixes it, and how do they know it needs fixing?

Your win

You can run the three-question procedure on any build-vs-take decision, and you know the wrap rule that contains whatever you take. Phase II complete — on to Master Designer.

Read and watch deeper

Bring your hand-rolled utilities to your agent-teacher and run the three questions on each.