Rules of schema growth (2017)
blog.datomic.com
blog.datomic.com
Alternative advice: never allow more than one app to share the db and expose data through APIs, not queries. Then you can actually remove cruft and solve compatibility through API versioning that you probably need to do anyway. Also, never maintain more than two versions at the time.
https://www.ribbonfarm.com/2010/07/26/a-big-little-idea-call...
As just a very simple example, unless I am explicitly clear about speaking from a Living Systems world view and a regenerative paradigm, when I talk about permaculture design here in this forum, I end up talking past the vast majority of people here on HN.
But even staying with the same shared technological world views and paradigms -- the people across the Three Tribes of Programmers (https://josephg.com/blog/3-tribes/) will get into flamewars because the frame differs in distinct ways.
Because a schema makes distinctions on information in a particular way, it will always encode a frame in which to view and understand that information.
If the query itself cannot be explained simply then what does its output even communicate? The more freedom you put in the API the more room you leave for confusion, this might be appropriate for research but not when you're reporting on something.
I see the purpose of reporting within an organization is to allow someone, somewhere to evaluate things and make decisions.
You can have an agreed upon report, but it doesn’t mean that the report itself will always lead to wise decisions. It can also become worse when those very decisions lead to forcing things around you to make things easier to report — that is the core thesis described in that essay, and the book, “Seeing Like a State”
Another example — in the realm of strategic decision making, decisions will always be made with imperfect information. A core way of strategy is deliberately manipulating how the opposing force gathers and interprets information, this influencing their actions. (Example, OODA). So what is already messy gets incredibly messy. This is the kind of stuff that falls way beyond reporting, and not something I think can ever be adequately modeled.
Similarly: any separation of concerns you can implement with APIs and multiple databases you can also implement with a schema. The difference being you have to reimplement a bunch of capabilities that are baked into an rdbms (and will probably never correctly implement something like a hash join).
In practice, the programming language and software engineering communities have largely failed to provide usable modularity. IMNSHO due to our clinging to call/return (so procedural/functional/method-oriented) as our modularity mechanism.
It ain't working.
So we the OS/systems guys and gals need to bail us out. Process boundaries are pretty hard, though of course we then manage to build distributed monoliths.
One thing that's interesting is that µservices, if actually REST-based, use data as the modularity mechanism, rather than procedures.
"Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious." -- Fred Brooks, The Mythical Man Month (1975)
And, somehow, this is going to fix the fact that this happened because we have bad programmers. Bad programmers that now have to also deal with network complexity on top of the basic complexity they already can't deal with.
Er, no. We actually don't have the right stuff provided by the programming language.
And no, µservices are not a good solution. They are a bad solution to a real problem that programming languages do not solve.
Microservices are a good solution for scaling a company into multiple sub-sections, I/E a solution for an org chart problem. If your org chart has less than 4 levels, most likely you are doing microservices too soon. At that point, you can have people in charge of dealing with network interactions exclusively, and that allows your bad programmers to still be productive.
That said, what I think what is needed is general support for architectural connectors in programming languages. Our so-called "general purpose programming languages" (actually: domain specific languages for the domain of algorithms, see ALGOL) effectively support one: the procedure call.
https://insights.sei.cmu.edu/library/procedure-calls-are-the...
See also:
https://blog.metaobject.com/2019/02/why-architecture-oriente...
and:
https://2020.programming-conference.org/details/salon-2020-p...
The article is pretty devoid of actionable advice.
That something as simple as change--a universal condition of all systems--defeats so many schema designs in SQL databases should suggest that there's something wrong with the SQL databases.
> Are these rules specific to a particular database? > > No. These rules apply to almost any SQL or NoSQL database. The rules even apply to the so-called "schemaless" databases.
The article thesis is essentially make breaking changes as infrequently as possible. The easiest way to do that is never change your data but that’s a sure way to have your competitors crush you as you stagnate. The next best thing you can do is make sure existing producers and consumers are not impacted when you make changes. For most changes being made the advice in the article gives a set of things you can do to achieve this goal.
For times where your database itself is not scaling which are the types of things you’re mentioning, I think there are other things you can do to, if not eliminate backwards incompatibility, at least make the transition easier. For example fronting your DB via an API and gate all producers/consumers through that. If you’re frequently having to handle scaling issues perhaps it’s time to reevaluate your system design all together.
never break it
Never remove a name
Never reuse a name
Your point is a very reasonable statement, but you are really disrespecting the author by putting a reasonable statement in their mouth. They had every chance to say the reasonable thing, and they clearly made a choice to say the unreasonable thing. Respect that decision (and tell them that they're wrong).These kinds of best practices make sense regardless of how many apps access a db.
Following the advice doesn't also prevent you from enforcing a strict contract for external access and modification of the data.
2 deploys is all it takes to solve this problem.
* 1 to deploy the new schema for the new version.
* 1 to remove the old schema.
This sort of "tick tock" pattern for removing stuff is common sense. Be it a database or a rest API, the first step is to grow with a new one and the second is to kill the old one which allows destructive schema actions without downtime.* Add the new schema
* Write to both the new and old schemas, keep reading from the old one (can be combined with the previous step if you're using something like Flyway)
* Backfill the new schema; if there are conflicts, prefer the data from the old schema
* Keep writing to both schemas, but switch to reading from the new one (can often be combined with the previous step)
* Stop writing to the old schema
* Remove the old schema
Leave out any one of those steps and you can hit situations where it's possible to lose data that's written while the new code is rolling out. Though again, it depends on the change; if you're, say, dropping a column that no client ever reads or writes, obviously it gets simpler.
Nonetheless, I agree with the OP that the article's advice is pretty bad. If you ensure that multiple apps/services aren't sharing the same DB tables, refactoring your schema to better support business needs or reduce tech debt is
a. tractable, and
b. good.
The rules from the article make sense if you have a bunch of different apps and services sharing a database + schema, especially if the apps/services are maintained by different teams. But... you really just shouldn't put yourself in that situation in the first place. Share data via APIs, not by direct access to the same tables.
I'm a little stunned by this suggestion. I've worked in quite a few different context for application systems, e.g. retail, manufacturing of fiber optic cable, manufacturing of telecommunications equipment, laboratory information management, etc.
I wouldn't even know what you mean by "app" in this context. There may a dozen or more classes of users who collectively have hundreds, even thousands, of different types of interaction with the system.
Sometimes there were natural divisions where you could separate things into a separate database. For example, the keep/dispose system for laboratory specimens, which tracked which specimens needed to be kept for possible further testing, where, and for how long. But most problem domains were not like that.
And sometimes we had to interact with other systems because they were for a separate division (because of mergers and acquisitions). But those kinds of separations made for more limited functionality and more difficulty in managing change, not less.
I agree with this in theory and have seen it go oh so very wrong in practice. Tables with dozens of columns, some of which may be unusued, invalid, actively deceiving, or at the very least confusing. Then a new developer joins and goes "A-ha! This is the way to get my data." ... except it's not and now their query is lying to users, analysts, leadership, anyone who thinks they're looking at the right data but isn't.
You absolutely have to make time to deprecate and remove parts of the schema that are no longer valid. Even if it means breaking a few eggs (hopefully during a thorough test run or phased rollout)
Edited to add: docs can help, but only so much. Environments that cluttered also tend to have layers of docs that are equally misleading.
I understand these “ten rules” as: as long as you have a decent codebase and decent engineers, these ten rules will make your life easier.
These rules are nothing if you are dealing with crap codebases (they can help, sure, but they will be just patches)
Any system that ultimately relies on "engineers need to always do the right thing" is a flawed, brittle, ineffectual system. Because even the best engineers will make a mistake somewhere, and because you can't exclusively hire "the best" engineers.
Let's spend our time figuring out how to recover from mistakes rather than trying to pretend they'll never happen.
Also, even the best team will sometimes make mistake.
Db schemas are unforgiving.
Hence: make your code and data easy to change, but simple, as you cannot predict in what way it will change.
Even then ain't nobody in a 10 person seed-stage startup got time, resources, or need to build the database you'll want to have when you're a 600 person Series C monster.
The immediate effect of that, of course, is that they also won’t try to hire any such person until the DB is a problem they can’t scale via throwing money at it.
Probably an unpopular opinion, but I think having a central database that directly interfaces with multiple applications is an enormous source of technical debt and other risks, and unnecessary for most organizations. Read-only users are fine for exploratory/analytical stuff, but multiple independent writers/cooks is a recipe for disaster.
I prefer an architecture where the central "database" is a central, monolithic Django/Rails/NodeJS/Spring app that totally owns the actual database, and if someone needs access to the data, you whip up an HTTPS API for them.
Yes, it is a tiny bit of effort to "whip up an API" but it deals with so many of the footguns implied by this article. "I need X+Y tables formatted as Z JSON" is a 5 minute dev task in a modern framework.
I think that the operative word here is "over time". So what is meant is not necessarily supporting many applications at the same time, but rather serially.
So the message is supposed to be: Apps come and go as they can be rewritten for so many reasons, but there will be a lot less reasons to redesign / replace a "valuable" database.
In my org I've felt the pain of having centralized DBs (with many writers and many readers) a lot of our woes come because of legacy debt some of these databases are quite old - a number date back to the mid 90's so over time they've ballooned considerably.
The Architecture I've found which makes things less painful is to transition the the centralized database into two databases.
On Database A you keep the legacy schemas etc and restrict access only to the DB writers (in our case we have A2A messaging queues as well as some compiled binaries which directly write to the DB). Then you have data replicated from database A into database B. Database B is where the data consumers (BI tools, reporting, etc) interface with the data.
You can exercise greater control over the schema on B which is exposed to the data consumers without needing to mass recompile binaries which can continue writing to Database A.
I'm not sure how "proper" DBAs feel about this split but it works for my usecase and has helped control ballooning legacy databases somewhat.
The issue is not how much apps depend on them, but maintenance options: in a central-database model, refactoring is high-friction. Whereas an API model can be built so that refactoring is low-friction.
When we talk about composition, we distinguish between data structures that are private to their codebase, and data structures that have a contract between codebases.
In the shared database model, everything is shared, so any change can affect all stakeholders.
The API model respects composition. This allows you to make changes behind the perimeter without the permission of stakeholders. If you want to make a major change to internal data structures, you can retain the old API, offer a new API, and then grandfather apps from the old endpoints to the new endpoints.
So, many of these rules still apply: You should only grow an existing API, not shrink it. You shouldn’t rename things in an API. You shouldn’t use the same name with different meanings except in different namespaces/endpoints. Etc.
*much respect to what the Crunchy Data folks have accomplished
The downside is this can cause devs to never have to think in terms of what the DB is capable of, so they may not consider writing the code such that the ORM decides to write the query differently. I’ve specifically seen a dearth of semijoins in output, even though they’re a common construct in RDBMS, and often far faster for the desired end goal. Sometimes the SQL planner itself will produce them, of course, but that’s not a guarantee either.
One way to minimize the human error is by only extending the schema rather than changing it and forcing your monolith to correctly make changes to existing queries.
I’m not saying adding an API is bad, because it’s not. I just think it’s solving a different set of problems.
I totally agree with you, but I think in the real world (mostly in monolithic apps, microservices shouldn't be affected) at some point someone will try to access directly the database. There are several reasons for doing this: API are too slow, it's simpler and more immediate writing some SQL vs a http client, the team responsible for APIs it's no more around and similar.
The entire idea of an unanticipated JOIN is beyond the ken of most APIs as well, unfortunately. For an external API that may not be much of a problem, but for an internal one you might end up creating a new de facto schema with a new query language.
::foo
resolves to (keyword *ns* "foo")
In Datomic, namespaces tend to represent your application's models, like :person/date-of-birth.I find it very useful mostly for human readability, it offers a way to distinguish what exactly :name refers to in your application's model. It also helps with editor autocomplete since you can type a namespace and see all keys up front, no need to consult a keys spec itself (or the schema of your database in Datomic). And when in doubt, in Datomic, you can always pull, and it is not too hard to run a query that extracts all attributes that exist in your database (this is actually an exercise in Learn Datalog Today[1], highly recommend going through this tutorial yourself if you want to play with databases like Datomic or XTDB).
[1] Exercise 2 in https://www.learndatalogtoday.org/chapter/4
I don't think the article mentions it, but one other technique we used- which still is a big question mark to me- is that we "trickled" changes to the database. Instead of changing all the rows in a single big transaction, the change was broken into thousands of little changes that were rolled out over a series of days. The reason for this is that if there is an unexpected problem, you have more time to stop the change and mitigate the damage.
Having been in a situation where a large schema update required migrating data in a transaction, which then broke for some data, but the data was so big that rolling back the transaction caused the database to become totally hosed, I can see this technique being very attractive.
Could you elaborate on how this worked?
We called it trickling, see https://www.vertica.com/docs/9.3.x/HTML/Content/Authoring/Ad...
One recent one was moving from bitflags in an int column to flags in a jsonb column. Tedious.
What makes it work? Testing, testing, testing and management that gives time for that process.
Postulate #2: once in production, a schema is likely to outlive you. Spend an enormous amount of time minimizing its scope and making sure it’s correct.
Postulate #3: you data has a schema no matter what the mongodb users try to say otherwise.
In my view, if the schema must change so radically that traditional migrations and other 'grow-only' techniques fall apart, you are probably looking at a properly-dead canary and in need of evacuating the entire coal mine.
The Quote regarding flowcharts and tables applies here - if you radically alter the foundation, everything built upon it absolutely must adapt. Every flowchart into the trashcan instantly. Don't even think about it. They're as good as a ball & chain now. Allowing parts of the structure to dictate parts of the foundation is where we find ourselves with circular firing squads.
Take things to the extreme - There is a reason you will start to find roles like "Schema Owner" in large, legacy org charts. These people cannot see the code or they will become tainted. They only have one class of allegiance - LOB owners. These are who they engage to develop & refine schema over time. The schema owner themselves has a full time job that is entirely dedicated to minimizing the impact of change over time to the org. This person is ideally the most ancient wizard in the org chart and has the prior Fred Brooks quote framed on their wall.
You can make schema change a top-down event that touches the entire organization. This happens quite often in banking when the central system is completely swapped for a different vendor & tech stack. Most of a bank is just a SQL database, but every vendor has a different schema that has to be adapted to. This is known as "core conversion" in the industry and is one of the more hellish experiences I have ever seen. If a bank with 4 decades of digital records can pull something like this off with regularity, there aren't many excuses that remain for a hole-in-the-wall SaaS app with 6 months of customer data.
All fields are optional. This is because fields have a lifecycle. Your code might be reading data generated by some other code written before the field was defined, or generated by some other code written after the field became obsolete.
With protobufs, you can never reuse a field number. However, you can remove a field (keeping its number reserved), which effectively means ignoring it in new applications. It's neither read nor written, which is fine because see above.
You can also rename the field itself. This doesn't affect the wire protocol.
It seems more sensible than having misleading names around for people to trip over. Maybe we'd be better off if databases used field numbers to refer to columns in the wire protocol?
My number one rule of growing schemas is to design your schemas and applications with a good custom field system. Some kind of flexible way of being able to add fields to items as data.
When your sql database needs another sql engine to translate to actual EAV queries.
But they are fun schemas!
The old joke that there are two actually hard problems in CS: caching and naming things. The older I get, naming things IMO is far more difficult.
There are 2 hard problems in computer science: caching, naming things, and off-by-one errors.
[0]: https://github.com/replikativ/datahike
[1]: https://github.com/replikativ/datahike#when-to-choose-datahi...
Sounds like 90s DB admins making a case for the bad old way of doing things.
If you want detailed change history it is hard to avoid using DDL files with source code control, although notes go a long way. Most databases are not good at change history - not even good at representing it, unfortunately.
It is also quite easy to do a mediocre job of anything, and I would be careful of counting anything as the "bad old way" with out a careful evaluation of the pros and cons of the "new good way" as well. A casual observer might wonder why so many modern web apps have response times that are ten to twenty times longer than they were two decades ago, for example. Perhaps - on occasion - the new good way isn't so good.