Code is cheap to change in a way that data is not. A badly named function gets renamed in an afternoon. A badly structured table has rows in it — millions of them, written by three generations of application code, read by a reporting job nobody owns, exported nightly to a system whose maintainers left. Changing the code is a pull request. Changing the shape of the data is a project with a rollout plan, a backfill, and a period where both shapes must be valid at once. The gap in cost between those two things is enormous, and it isn’t obvious at the moment the schema gets written, which is usually early, quickly, and by someone thinking mostly about the first feature.

What makes this asymmetry compound is that the schema quietly becomes an interface. It starts as an implementation detail of one service, then a dashboard queries it directly, then an analytics pipeline depends on a particular column, then someone writes a script against it for a one-off that becomes a monthly process. None of those consumers signed a contract, and none of them will be visible when you go looking for who might break. The service’s API might be carefully versioned while the tables underneath it — accessed by everything, guaranteed to no one — have effectively become a public interface with no documentation and no deprecation policy.

The good news is that most of the expensive mistakes come from a short list. Storing something as a string that has structure — a status, a timestamp, an amount with a currency — invites every consumer to parse it differently, and by the time you notice the inconsistencies, they’re persisted. Overloading a column’s meaning based on the value of another column means every future query has to know the rule, and half of them won’t. Deleting information rather than marking it inactive throws away the ability to answer questions you haven’t been asked yet. In each case the shortcut saves an hour at design time and creates a permanent constraint on what the system can say about itself.

The corresponding discipline isn’t up-front perfection — you can’t design for questions nobody has asked. It’s a bias toward keeping the data more explicit than the current feature strictly needs, because explicit costs a little now and ambiguity costs a lot later. Separate fields rather than encoded strings. Real types rather than text that means something. Events or state transitions recorded rather than a single mutable current-state column that erases its own history. None of that is speculative generality — it’s declining to throw away information you already have in hand, which is a different and much safer bet than trying to predict future requirements.

The framing that helps is treating the schema as the longest-lived artifact you produce. Whatever service owns it today will probably be rewritten, split, or replaced within a few years; the data it wrote will still be there, being read by whatever came next. Designing at that timescale doesn’t mean gold-plating — it means noticing which decisions are cheap to revise and which ones you’ll be living with long after the code that motivated them is gone.