The Words You Inherit
Every system hands you a vocabulary before it hands you a problem. The names you didn't choose draw the boundaries you can see, and a wrong one doesn't confuse you — it quietly removes designs from consideration.
Every system hands you a vocabulary before it hands you a problem. The names you didn't choose draw the boundaries you can see, and a wrong one doesn't confuse you — it quietly removes designs from consideration.
Naming gets treated as a documentation concern — a matter of clarity and taste. It behaves like a schema decision: cheap for an afternoon, load-bearing forever, and reviewed like neither.
Code that never runs isn't preserved — it holds references to an environment that keeps moving without it. The common path is continuously re-validated by reality; the rare path is a stopped clock nobody has looked at.
Every fix to the common path makes the exception path less frequent, less familiar, and less examined — so reliability work quietly concentrates your remaining risk in the code nobody has watched run.
Execution is the only maintenance mechanism that reliably works, and nobody schedules it. That makes readiness a flow rather than a stock — which is why the only honest question about any part of a system is when it last actually ran.
Failing to reproduce a bug tells you something real. It just doesn't tell you the thing most people close the ticket believing it said.
Both halves of reproduction — succeeding and failing — quietly replace the reporter's failure with yours. Only one of the two is the bug.
Getting a bug to reproduce feels like the end of the investigation. It's the point where you quietly substitute your version of the failure for theirs.
Most of a system's shape was never chosen. It's the fossilized remains of whatever was expedient the first time, and it constrains everything downstream as firmly as if someone had decided it on purpose.
Provenance is a proxy. What actually holds a system's shape in place isn't anyone's reasoning — it's the count of things standing on it, and that you can measure at any time.
Picking a metric and starving the work it can't see aren't two separate problems. They're the same act, viewed from either side.
A proxy measurement is only honest for as long as nobody is optimizing against it yet. The moment it becomes the target, it starts drifting away from the thing it was supposed to represent.
Every measurement system creates two categories: work that counts and work that doesn't. The second category is where prevention lives, and it starves quietly.
You inherited code with no explanation attached. The safe default is not caution, and it is not confidence — it is finding out.
Delete is the one word in a product that every user thinks they understand and every implementation defines differently.
Deleting a record is easy. Deleting every derivative of that record is the part nobody scoped.
Everyone agrees users should be able to take it back. The disagreement is about how much the system has to remember, and for how long.
Search inside a product fails on the queries people actually make: partial names, misspellings, and the one detail they happen to recall.
Export is the feature nobody demos and everybody evaluates. A product that makes leaving hard is not one people commit to.
Import and export are one-time events. Keeping two systems agreeing is a permanent one — and the hard part is identity, not transfer.
The first thing a new customer does with your product is hand it a file of their real data. Whatever happens next is what they learn about the software.
Notifications get added one feature at a time, each individually justified. Nobody owns the total, and the total is what determines whether any of them get read.
Notification preferences look like a courtesy. They're actually the mechanism that keeps the channel usable — and the coarser they are, the more people mute everything.
Notifications are deliveries to systems you don't control, on paths that can retry. The duplicate that reaches a customer is more visible than almost any other bug.
A recurring manual repair is a bug report written in someone's calendar. Building a faster way to perform it is progress; not needing it is the actual goal.
Internal tools get built quickly, reviewed lightly, and given more power than anything customers can touch. They deserve the standards the customer-facing paths get.
Storing when something happened is easy. Storing it in a way that still means the same thing in another country, six months later, is where systems quietly go wrong.
Recurring work looks simple until you ask what happens when a run is late, overlaps the next one, or fires on a machine that thinks it's a different hour.
Every machine's clock is slightly wrong, and they're wrong in different directions. Code that compares timestamps across machines is trusting a consensus that doesn't exist.
Where a permission is enforced matters more than how it's modeled. Interfaces hide options; only the layer that touches data can actually deny anything.
Access control gets treated as plumbing and built by whoever drew the short straw. It's actually a description of how your customers' organizations work.
"Who can see this record, and why?" is the question a permission system exists to answer. If nobody can answer it without reading code, the system has already failed.
Your code is identical to yesterday's and the behavior is different, because the change happened on the other side of an integration you don't control.
Most queue failures are transient and retrying fixes them. The interesting case is the message that will fail identically forever, and what your system does when it meets one.
Changing a table's definition takes one line. Getting millions of existing rows into the new shape, while the system keeps running, is the actual project.
Applications get rewritten, frameworks get replaced, services get split apart. The data usually survives all of it, which is why the schema is the most expensive decision in the system.
A column name tells you what something is called. It rarely tells you what counts, what's excluded, or which of three plausible definitions the number actually uses.
Most log lines are written by someone who already knows what the code does, for a reader who doesn't and won't be able to ask.
A config edit can change production behavior as completely as a code change can, and in most places it does so with none of the review, testing, or staged rollout that code gets.
Most config options exist because someone couldn't decide, or didn't want to. The knob ships, the decision never gets made, and everyone downstream inherits the question.
An alert that fires correctly and leaves the person receiving it with no idea what to do has done half a job. The response isn't downstream of the alert — it's the reason the alert exists.
Rolling back a deploy feels like undoing it. Mostly it undoes the code. Everything the bad code already did to your data, your queues, and your downstream systems is still there, waiting.
Retrying a failed operation feels like giving it another chance to succeed. What it actually does is ask a question nobody thought to answer: what happens if the first attempt worked and the failure was just in hearing about it?
Most operations get designed once, for the happy path, and retries get bolted on after as an afterthought. Treating the retry as part of the operation's design from the start closes most of the gaps this thread has described.
Every cache makes an implicit promise: this value is still true. The performance win is real, but so is the risk you're quietly signing up for every time you decide not to check.
Clever caching strategies solve problems most systems don't have yet, and create ones most systems can't afford. The boring cache — short TTL, simple key, easy to reason about — is usually the right amount of cleverness.
A cache key is a claim about what makes two requests the same. Get that claim slightly wrong and the cache doesn't fail loudly — it just quietly serves the wrong answer to someone.
You can't design for a scale you don't have yet without paying for flexibility you may never use. What you can do is build honestly for today and leave yourself a way to notice the exact moment today stops being enough.
Code that looks like it does one pass over the data can secretly do one pass per item — a hidden multiplication that's invisible at small size and unmissable once the input grows.
A green test suite feels like a broad statement about your code's health. It's actually a narrow one: these specific inputs produced these specific outputs, today. Confusing the two is where false confidence comes from.
A test that runs and passes tells you the code did what the test checked. It doesn't tell you the test checked the right thing. That second question is easy to skip and expensive to skip.
Every test suite has a shadow: the tests that don't exist because nobody thought to write them. That absence doesn't show up on a coverage report, which is exactly why it's where the real risk tends to live.
A deprecation notice that nobody acts on isn't a warning, it's decoration. The gap between marking something deprecated and actually being able to remove it is where most interfaces quietly calcify.
You decide what your interface promises. Your users decide what they depend on. When those two differ — and they always do — the second one is the real contract.
A version number is supposed to tell callers something. Too often it just tells them the code changed — which they already knew, and which doesn't help them decide whether to worry.
Error handling gets treated as the cleanup after the real work — the branch you fill in to make the compiler happy. But what a system does when something goes wrong is part of what the system is.
A bug in a rarely-used option affects the people who chose it. A bug in the default affects everyone who didn't choose anything — which is usually almost everyone. The blast radius of a mistake tracks how many people never had to opt in.
Defaults feel like the absence of a choice — the value nobody had to set. In practice they're the choice most users will live with, made once by someone who won't be there to see the consequences.
Adding a setting can be a way of avoiding a decision — shipping both answers instead of finding the right one. Sometimes that's respect for real variation. Sometimes it's an unresolved argument, permanently installed.
Some code has no fast feedback loop at all — you can't easily run it, watch it, or reproduce its failures. That code doesn't just move slowly. It resists being understood, and the slowness compounds.
How fast you build isn't set by how fast you type. It's set by how quickly you can go around the loop of making a change, seeing what it did, and learning from it. That cycle time is the real speed.
Building for an imagined future feels like foresight. Usually it's a bet against odds you'd never take if you saw them clearly: pay the cost of flexibility now, on a guess about needs that mostly never arrive.
If code is a liability and speculative flexibility is a bad bet, the discipline that follows is to build the simplest thing that solves today's real problem. The hard part is telling simple from naive.
The urge to write a comment explaining what a piece of code does is usually a signal, not a solution. Most of the time the honest fix is to make the code say it — and a comment is what you write only when the code can't.
Code that works but behaves unexpectedly still charges a tax: every reader has to stop and verify the thing they assumed. Consistency isn't aesthetic tidiness — it's what lets people trust their assumptions and move on.
Struggling to name something isn't a vocabulary problem. It's the code telling you that you don't yet understand the thing you're building — and a good name is what understanding looks like once you do.
The old joke — the first 90% of the work takes 90% of the time, and the last 10% takes the other 90% — isn't cynicism. It's a precise description of where estimates go to die: the unglamorous finishing that no one pictures.
A coverage number tells you which lines ran during the tests. It says nothing about whether anything was actually checked — and the gap between those two is where teams get a false sense of safety.
A mock replaces a real dependency with your belief about how it behaves. When the belief is wrong, the test passes and production fails — because you tested the version of the world in your head, not the one that exists.
A test that fails when you refactor working code isn't protecting you — it's charging you. And the real damage isn't the wasted hour; it's that the suite slowly teaches people that failures don't mean anything.
Code persists perfectly; the reasoning behind it evaporates. Which is why so much of a mature codebase is lines nobody dares touch — not because they're wrong, but because nobody remembers what they were for.
Rewriting feels like replacing bad code with good code. It's really replacing accumulated knowledge with a fresh guess — and the ugliness you're removing is often where that knowledge is stored.
Checking for invalid states at runtime means remembering to check everywhere. Shaping your data so the invalid state can't be expressed at all means you only have to be right once — at the type, not at every call site.
Computation is mostly easy; the hard part of software is state — the accumulated memory of everything that happened before. Most bugs aren't wrong logic, they're the system being in a combination of states nobody pictured.
The moment a fact lives in two places, you've taken on a job you'll eventually fail at: keeping them equal. Most 'impossible' bugs are just two copies of something that were supposed to agree and quietly stopped.
A module boundary is a bet about what will change together. Draw it along the axis of change and edits stay local; draw it along surface resemblance and every change cuts across every module.
Every useful abstraction hides the layer beneath it — until the day it can't. The ones that serve you longest aren't the ones that hide the most, but the ones that fail honestly when the thing underneath breaks through.
The wrong abstraction is more expensive than duplication, because it's harder to reverse. Which is an argument for waiting — abstracting on the third occurrence, not the first, when you finally know which parts actually vary.
Most messaging systems promise to deliver each message at least once, not exactly once. The gap between what you assumed and what the system actually guarantees is where the duplicate-processing bugs live.
The moment you add retries to a system, you've made a promise you might not be keeping: that doing the same operation twice is the same as doing it once. Idempotency is what makes that promise true.
Messages arrive in the order they were sent — until they don't. Assuming global ordering in a distributed system is one of those beliefs that holds in testing and breaks in production, quietly, in ways that are hard to trace.