SlamData

Systems & Data

Schemaless Storage Still Has a Schema

Removing the schema from the database does not remove it — it moves it into application code, where nothing checks it and every version disagrees.

A document store will accept any shape you give it. That is a statement about where the schema is enforced, not about whether one exists.

For a separate people-operations perspective, this separate resource covers ways measurement systems can be gamed.

The schema is in the code that reads the data. Every function assuming a field is present, has a particular type, and means a particular thing is expressing part of it. The difference from a relational database is that these assumptions are scattered, unstated, and unverified — and that several versions of them coexist in the data at once.

This is a real trade with real advantages. It is worth making deliberately.

What you actually gain

Deployment decoupling. Adding a field requires no migration and no coordinated deploy. For a system under rapid change, this is a genuine and substantial saving.

Heterogeneous records. Where entities genuinely differ in shape — user-defined fields, varied product attributes, evolving event payloads — modelling that relationally means either sparse columns or an entity-attribute-value table, and both are unpleasant.

Locality. An order and its line items in one document is one read instead of a join, and the document boundary can be chosen to match your access pattern.

Ingesting data you do not control, where imposing a schema at write time means rejecting records you would rather keep.

What you pay for it

Read code handles every version ever written. Five years of format changes means the reader must cope with all of them, and the branching accumulates because nothing tells you when the old shape is finally gone.

Nothing catches a typo. Writing emial creates a new field. No error, and the bug appears later as missing data.

Type inconsistency within one field. A field holding a number in some documents and a string in others, because a client changed. Queries and aggregations behave unpredictably and nothing flagged it.

No referential integrity. References to documents that were deleted are just there, and finding them requires a scan.

Analytics get harder. Every consumer must independently work out the shape, including the ones written by people who were not there for the changes.

The discipline that makes it work

The teams for whom this works well do not skip the schema. They move it somewhere explicit.

Write a schema definition anyway, outside the database. JSON Schema, Protobuf, Avro, or your language's validation library. Validate on write at the application boundary.

This gives you what the database no longer provides — a single stated definition — while keeping the deployment flexibility, because you control when the definition changes rather than needing a migration.

Version every document. A schema_version field on every record. Without it, readers must infer the version from which fields are present, which is guesswork that gets worse over time.

{ "schema_version": 3, "user_id": "...", "email": "..." }

Migrate on read, and write back. When a reader encounters an old version, upgrade it in memory and persist the upgraded form. Over time the old versions disappear naturally, driven by access rather than by a batch job.

Then run a backfill for the cold tail, or you will carry the old branches forever for documents nobody reads.

Delete old version handling once the data is gone. The point of versioning is to know when it is safe, and this step is what stops the branching accumulating.

The things worth enforcing anyway

Even in a store that does not require them, several constraints are available and worth using.

Unique indexes on fields that must be unique. This is a real constraint, not a convention.

Server-side validation rules where the engine supports them — several document stores do, and it is a second line of defence behind application validation. Applied in a permissive mode first, they also tell you how much existing data violates the rule before you enforce it.

Required fields on the write path, in one place rather than in every caller.

Modelling for the access pattern

The relational instinct is to normalise and let the planner sort it out. Document stores reward the opposite, and the constraint is real.

Embed what is read together. If line items are always read with the order, embed them.

Reference what is large, shared, or changes independently. Embedding a user's full profile into every order means updating a name requires touching every order.

Watch the document size limit. An unbounded array in a document — comments, events, history — will eventually hit it, and the failure arrives suddenly for the one document that crossed the line. Bound the array and overflow to a separate collection, or model it separately from the start.

Accept duplication deliberately, and know how you will update the duplicates. This is the main cost of embedding, and it is manageable when it is a decision rather than an accident.

Finding out what your schema actually is

For an existing collection with no definition, this is worth doing once and it is usually alarming.

Sample and profile. Iterate over a sample, count field presence and record the types observed per field. A short script produces the real schema — the one in the data rather than the one in anyone's head.

What it reveals, typically: fields present in 94% of documents that the code assumes are always there; three types in one field; misspelled field names with a handful of documents each; and fields nobody remembers adding.

Run it on a schedule and alert on new fields or new types appearing. This is the closest thing to a schema check you can have, and it catches the client that started sending a string where a number belonged.

The summary

Schemaless moves the schema into the readers, where it is implicit and multiple versions coexist.

Keep an explicit definition anyway, validate at the boundary, and version every document.

Migrate on read and write back, then backfill the tail, then delete the old branches.

Profile the collection to find the schema you actually have — it will differ from the one you intended, and that gap is where the bugs are.

The flexibility is real and worth having. It is not an exemption from knowing what shape your data is.

For primary background on this topic, consult MongoDB data-modeling documentation.