Juliana Assalti
Back to the blog
2 min read

Data modeling: the decision that is hardest to reverse

Frameworks, languages and interfaces get swapped. The data model, once in production with real data, resists — and every feature is born fighting it. Why this is the decision that most deserves care up front.

Almost every technical decision is reversible at a tolerable cost. Switching frameworks is work; switching languages, more so; rebuilding an interface is routine. The data model is the exception: once in production, with real data from real people, it resists change like no other part of the system.

The cost is not in the diagram — it is in what has already been written. Migrating a live table means a reversible migration script, integrity to preserve, a downtime window to negotiate, and coupled code to adjust in cascade. Getting it wrong early is cheap; wrong after a thousand rows is expensive; wrong after a million becomes a project.

Model the domain, not the screen

The most common mistake is shaping the schema around the current interface — one table per screen, one column per input. Interfaces change every quarter; the domain does not. Modeling the domain means capturing the real business entities and the rules that always hold (the invariants), regardless of how the screen shows them today.

  • Ask what is always true, not what the screen needs right now.
  • Name entities the way the business names them, not the way CRUD exposes them.
  • Let the database guarantee the invariants — keys, uniqueness, referential integrity — instead of trusting application code alone.

Service boundaries follow data ownership

When the system grows and the question becomes “where to split”, the answer usually lives in the data, not the code. A service should own what it writes. Two services fighting over the same table are not two services — they are one, with the boundary drawn in the wrong place, and each deploy of one breaks the other.

A concrete example: instead of letting the application guarantee that every order has a valid customer and a non-negative total, the model itself closes that door.

CREATE TABLE pedido (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  cliente_id  uuid NOT NULL REFERENCES cliente(id),
  total_cents integer NOT NULL CHECK (total_cents >= 0),
  criado_em   timestamptz NOT NULL DEFAULT now()
);

The constraint is not bureaucracy: it is the invariant written where no one can bypass it. Application code forgets to validate; the database does not. An order with no customer or a negative total simply does not exist — and that holds for every write path, including the rushed Friday-night migration script.

No model is born perfect, and that is not the point. The point is spending care where it pays off most: a good model makes new features cheap; a bad one makes every feature fight the past. It is the foundation — and foundations are set before the walls go up.