DEV Community

Cover image for Why Data Models Eventually Become System Models
Derek Mwale
Derek Mwale

Posted on

Why Data Models Eventually Become System Models

There is a dangerous illusion in software engineering.

We like to believe that the architecture of a system lives in places like:

  • service classes,
  • controllers,
  • APIs,
  • message brokers,
  • frontend components,
  • background workers,
  • infrastructure,
  • deployment diagrams.

And then, somewhere underneath all of that, we imagine there is a database.

A database.

Just storage.

A place where the application puts things when it is done thinking.

I think this is one of the most expensive misconceptions in software engineering.

Because as systems grow, the data model stops being merely a representation of information.

It becomes a representation of reality.

Then it becomes a representation of behavior.

Then it becomes a representation of authority.

Then it becomes a representation of time.

Eventually, it becomes a representation of the system itself.

The database schema starts answering questions that were supposedly supposed to be answered by application code.

Who owns this?

Who can modify it?

What existed before?

What is currently active?

What depends on what?

What can be deleted?

What must survive?

What is unique?

What is optional?

What is immutable?

What happened?

What is allowed to happen next?

At that point, changing the data model is no longer equivalent to changing storage.

You are changing the architecture.

And that is the central idea:

A sufficiently mature data model eventually becomes a system model.

Not because databases magically become intelligent.

But because every serious software system eventually discovers that behavior needs memory, and memory needs structure.


1. The Database Was Never Just Storage

Imagine a simple application.

You have users.

User
----
id
name
email
password
Enter fullscreen mode Exit fullscreen mode

Easy.

You build an API.

POST /users
GET /users/:id
PATCH /users/:id
DELETE /users/:id
Enter fullscreen mode Exit fullscreen mode

The database stores users.

The API manipulates users.

The frontend displays users.

Everything seems clean.

Then reality arrives.

A user can own organizations.

User
Organization
Enter fullscreen mode Exit fullscreen mode

An organization has members.

Organization
   |
   +---- Members
Enter fullscreen mode Exit fullscreen mode

Some members are administrators.

Some are ordinary users.

Some users can invite other users.

Some can remove users.

Some can create invoices.

Some can only view invoices.

Now your data model begins changing.

User
Organization
OrganizationMember
Role
Permission
Invoice
Enter fullscreen mode Exit fullscreen mode

The application now needs to understand relationships.

Then you discover that users can belong to multiple organizations.

Then organizations can have multiple projects.

Then projects contain tasks.

Then tasks can be assigned to users.

Then tasks can have comments.

Then comments can be edited.

Then edited comments need history.

Then deleted comments need audit records.

Suddenly:

User
  |
  +---- Organization
          |
          +---- Project
                  |
                  +---- Task
                         |
                         +---- Assignment
                         |
                         +---- Comment
                         |
                         +---- CommentRevision
Enter fullscreen mode Exit fullscreen mode

Notice what happened.

Nobody sat down and said:

"Let's make the database our architecture."

It happened naturally.

The system became more complex because reality became more complex.

And every new piece of reality required somewhere to remember it.

The moment something must be remembered, it must be modeled.

The moment it is modeled, relationships emerge.

The moment relationships emerge, constraints emerge.

And once constraints emerge, architecture emerges.


2. Data Is Frozen Software Behavior

One useful way to think about a data model is this:

A schema is software behavior that has been made persistent.

Consider uniqueness.

Suppose your database contains:

email TEXT UNIQUE
Enter fullscreen mode Exit fullscreen mode

That looks like a storage constraint.

But it is actually business behavior.

You have encoded:

Two users cannot simultaneously possess the same identity key.

Consider:

FOREIGN KEY (organization_id)
REFERENCES organizations(id)
Enter fullscreen mode Exit fullscreen mode

That is also behavior.

You have encoded:

A membership cannot point to an organization that does not exist.

Consider:

NOT NULL
Enter fullscreen mode Exit fullscreen mode

You have encoded:

This concept cannot exist without this information.

Consider:

CHECK (quantity >= 0)
Enter fullscreen mode Exit fullscreen mode

You have encoded:

Negative inventory is not a valid state.

These are not passive descriptions.

They are executable statements about reality.

The database is effectively saying:

This state is possible.
This state is impossible.
This relationship is valid.
This relationship is invalid.
This object may exist.
This object may not exist.
Enter fullscreen mode Exit fullscreen mode

That is architecture.


3. The First Transformation: Data → Relationships

Early schemas are often flat.

Customer
Product
Order
Enter fullscreen mode Exit fullscreen mode

But systems rarely remain flat.

An order belongs to a customer.

An order contains products.

A product belongs to a category.

A payment belongs to an order.

A shipment belongs to a payment or order.

Suddenly we have:

Customer
   |
   v
 Order
   |
   +------ Payment
   |
   +------ Shipment
   |
   +------ OrderItem
             |
             v
           Product
Enter fullscreen mode Exit fullscreen mode

This graph is not just describing data.

It is describing how the business works.

An order cannot exist independently from certain concepts.

A shipment cannot logically exist without something being shipped.

An order item connects an order to a product.

The relationships become architectural dependencies.

This is why relational modeling is so powerful.

Relationships are not decoration.

Relationships are the skeleton of a system.


4. The Schema Becomes a Dependency Graph

Eventually, every mature application can be viewed as a graph.

Let:

$$
G = (V,E)
$$

where:

  • (V) represents entities,
  • (E) represents relationships.

For example:

User ---- owns ----> Organization
Organization ---- contains ----> Project
Project ---- contains ----> Task
Task ---- assigned_to ----> User
Task ---- has ----> Comment
Enter fullscreen mode Exit fullscreen mode

Now consider what happens when one node changes.

Suppose you remove User.

What happens?

Tasks may lose their assignees.

Comments may lose authors.

Organizations may lose owners.

Audit records may lose actors.

This means the user entity is not merely a row collection.

It is a structural dependency.

The schema tells us how much of the system depends upon a concept.

We can even think of an entity's architectural importance in terms of its connectivity.

A highly connected node is often a high-risk node.

Something like:

             Organization
             /     |     \
            /      |      \
         User    Project   Billing
          |        |
        Task ---- Task
Enter fullscreen mode Exit fullscreen mode

Deleting User is very different from deleting an isolated configuration table.

The database graph reveals this.

Your architecture diagram might hide it.


5. Then Constraints Arrive

Relationships alone are not enough.

Systems need rules.

Suppose you have:

Order
OrderItem
Product
Enter fullscreen mode Exit fullscreen mode

You might define:

OrderItem.quantity > 0
Enter fullscreen mode Exit fullscreen mode

Now the database knows something about valid business states.

Suppose:

Order.status ∈ {
    pending,
    paid,
    shipped,
    cancelled
}
Enter fullscreen mode Exit fullscreen mode

You have introduced a state machine.

The data model now contains temporal behavior.

An order is not merely an object.

It is an object moving through states.

pending
   |
   v
 paid
   |
   v
shipped
Enter fullscreen mode Exit fullscreen mode

Perhaps cancellation is only possible before shipping.

Then your application has a transition constraint:

pending -> cancelled
paid -> cancelled
shipped -> cancelled  ✗
Enter fullscreen mode Exit fullscreen mode

Now the system is no longer just storing orders.

It is modeling permissible evolution.

That is a significant transition.

The data model is becoming a system model because it is beginning to describe process.


6. State Is Where Data Becomes Behavior

This is one of the deepest ideas in system design.

A static object tells you:

What exists?

A stateful object tells you:

What can happen?

Consider a bank account.

Account
--------
balance
status
Enter fullscreen mode Exit fullscreen mode

If:

status = active
Enter fullscreen mode Exit fullscreen mode

the account can accept transactions.

If:

status = frozen
Enter fullscreen mode Exit fullscreen mode

perhaps withdrawals are forbidden.

If:

status = closed
Enter fullscreen mode Exit fullscreen mode

nothing should modify it.

The data model now determines which behaviors are meaningful.

We can express the system as:

$$
S_{t+1} = F(S_t, A_t)
$$

where:

  • (S_t) is the current state,
  • (A_t) is an action,
  • (F) determines the next state.

The database stores (S_t).

The application executes (F).

But the model determines what states exist in the first place.

Therefore:

The data model defines the state space over which the application operates.

And once you understand that, the phrase "just the database" starts sounding strange.


7. Your Schema Defines the Universe of Possibilities

Every software system has a universe of valid states.

Call it:

$$
\Omega
$$

Not every imaginable state belongs to (\Omega).

For example, an inventory system might allow:

quantity = 0
quantity = 10
quantity = 100
Enter fullscreen mode Exit fullscreen mode

but not:

quantity = -50
Enter fullscreen mode Exit fullscreen mode

If the database enforces:

CHECK (quantity >= 0)
Enter fullscreen mode Exit fullscreen mode

then the schema has reduced the universe of possible states.

Instead of:

$$
\Omega = \mathbb{Z}
$$

we effectively have:

$$
\Omega = {x \in \mathbb{Z} \mid x \geq 0}
$$

This matters.

A constraint is a reduction in possibility.

And architecture is largely the management of possibility.

Good architecture does not merely tell a computer what to do.

It prevents the computer from entering states that should never exist.


8. Data Models Become Authority Models

Now consider permissions.

A naive system might have:

User
role
Enter fullscreen mode Exit fullscreen mode

with:

admin
user
Enter fullscreen mode Exit fullscreen mode

But serious systems evolve.

You eventually need:

User
Role
Permission
RolePermission
UserRole
Organization
OrganizationMember
Enter fullscreen mode Exit fullscreen mode

Now the data model represents authority.

For example:

User
  |
  v
OrganizationMember
  |
  v
Role
  |
  v
Permission
Enter fullscreen mode Exit fullscreen mode

This graph answers:

Who is allowed to do what?

The authorization system may have middleware.

It may have policies.

It may have service-level checks.

But underneath all of that is a persistent model of authority.

This is why authorization bugs are often data-model bugs in disguise.

If your model cannot accurately represent:

Alice is admin of Company A
Alice is viewer of Company B
Bob is admin of Company B
Enter fullscreen mode Exit fullscreen mode

your application will eventually become a collection of special cases.

The right model removes special cases.


9. Multi-Tenancy Changes Everything

Consider a SaaS platform.

At first:

User
Project
Task
Enter fullscreen mode Exit fullscreen mode

Then you acquire multiple customers.

Now you need:

Tenant
User
TenantUser
Project
Task
Enter fullscreen mode Exit fullscreen mode

And suddenly a critical question appears:

Which tenant does this row belong to?

You could enforce this through application code.

But if tenant ownership is fundamental, the model itself should usually reflect it.

Tenant
   |
   +---- User
   |
   +---- Project
          |
          +---- Task
Enter fullscreen mode Exit fullscreen mode

Now the tenant becomes an architectural boundary.

It influences:

  • authorization,
  • queries,
  • caching,
  • indexing,
  • storage,
  • auditing,
  • billing,
  • data isolation,
  • backups,
  • compliance.

One column like:

tenant_id
Enter fullscreen mode Exit fullscreen mode

can therefore have consequences across the entire architecture.

This is the strange power of data modeling.

A small structural decision can propagate everywhere.


10. The Data Model Starts Dictating APIs

Imagine you have:

Order
OrderItem
Payment
Shipment
Enter fullscreen mode Exit fullscreen mode

Your API naturally begins reflecting the model.

GET /orders
GET /orders/:id
GET /orders/:id/items
GET /orders/:id/payment
GET /orders/:id/shipment
Enter fullscreen mode Exit fullscreen mode

Why?

Because the API is exposing relationships.

The endpoint structure is often a projection of the underlying data graph.

This is not always desirable.

A database-shaped API can become ugly.

But it demonstrates something important:

Your data model creates gravitational pull.

Developers tend to build APIs around the entities they have.

They create services around the relationships they have.

They write authorization rules around ownership relationships they have.

They create frontend screens around state models they have.

Eventually:

Database
   ↓
Domain model
   ↓
Services
   ↓
API
   ↓
Frontend
Enter fullscreen mode Exit fullscreen mode

The data model becomes an architectural attractor.


11. ORM Models Make This Even More Obvious

ORMs expose the phenomenon directly.

Consider:

class Order(models.Model):
    customer = models.ForeignKey(Customer)
Enter fullscreen mode Exit fullscreen mode

Now the application has an object relationship:

order.customer
Enter fullscreen mode Exit fullscreen mode

The database relationship becomes a programming-language relationship.

Or in Rust:

struct Order {
    customer_id: CustomerId,
}
Enter fullscreen mode Exit fullscreen mode

Or TypeScript:

interface Order {
    customerId: string;
}
Enter fullscreen mode Exit fullscreen mode

The same relationship travels upward.

Database:

orders.customer_id
Enter fullscreen mode Exit fullscreen mode

Domain:

Order.customerId
Enter fullscreen mode Exit fullscreen mode

API:

{
  "customerId": "..."
}
Enter fullscreen mode Exit fullscreen mode

Frontend:

order.customerId
Enter fullscreen mode Exit fullscreen mode

One relationship.

Multiple layers.

This is why data modeling decisions have such enormous blast radius.


12. The Most Dangerous Tables Are Not Always the Largest

A common architectural mistake is measuring importance by size.

A table with 500 million rows looks important.

But sometimes a table with 10 rows is more architecturally significant.

Consider:

Permission
Enter fullscreen mode Exit fullscreen mode

It may contain only:

READ
WRITE
DELETE
ADMIN
Enter fullscreen mode Exit fullscreen mode

Tiny table.

Huge architectural impact.

Or:

Currency
Enter fullscreen mode Exit fullscreen mode

Perhaps only 150 rows.

But it affects:

  • invoices,
  • payments,
  • exchange rates,
  • reporting,
  • accounting,
  • pricing.

Or:

Status
Enter fullscreen mode Exit fullscreen mode

with ten possible values.

Those ten values can define the entire workflow.

The number of rows tells you about data volume.

It does not tell you about architectural influence.


13. Data Models Encode Time

Time is where many systems become genuinely difficult.

Suppose a customer changes their address.

If you only store:

Customer.address
Enter fullscreen mode Exit fullscreen mode

you know the present.

But what if you need:

Where did the customer live when this invoice was issued three years ago?

Now you need history.

Perhaps:

Customer
Address
CustomerAddressHistory
Enter fullscreen mode Exit fullscreen mode

Or:

CustomerAddress
valid_from
valid_to
Enter fullscreen mode Exit fullscreen mode

Now your data model contains time.

The model has evolved from:

What is true?
Enter fullscreen mode Exit fullscreen mode

to:

What was true?
Enter fullscreen mode Exit fullscreen mode

And eventually:

What was believed to be true at a particular point in time?
Enter fullscreen mode Exit fullscreen mode

That distinction matters enormously.

Financial systems, medical systems, logistics systems, audit systems, and distributed systems all eventually encounter it.

Once history matters, your database is no longer merely storing objects.

It is storing a timeline of reality.


14. Event Sourcing Makes the Point Extreme

Consider event sourcing.

Instead of storing only:

Account.balance = 500
Enter fullscreen mode Exit fullscreen mode

you might store:

Deposited(1000)
Withdrawn(200)
Withdrawn(300)
Enter fullscreen mode Exit fullscreen mode

The current state becomes a projection:

$$
S_t = f(E_1,E_2,\dots,E_t)
$$

The events become the primary model.

Now the data model describes not merely what exists, but what happened.

This is almost the purest form of a system model.

The architecture becomes:

Events
  |
  v
Projection
  |
  v
Current State
Enter fullscreen mode Exit fullscreen mode

The distinction between "data" and "behavior" becomes blurry.

An event such as:

OrderPaid
Enter fullscreen mode Exit fullscreen mode

is data.

But it also represents a transition.

It also represents history.

It also represents causality.

It also represents something other parts of the system may react to.

Data has become architecture.


15. Audit Logs Are Another Hidden System Model

Imagine:

AuditLog
--------
actor_id
action
resource
timestamp
metadata
Enter fullscreen mode Exit fullscreen mode

At first this looks like observability.

But eventually it becomes part of the business system.

You need to answer:

Who changed this?
When?
From what?
To what?
Why?
Enter fullscreen mode Exit fullscreen mode

Now your system has memory beyond current state.

Current state says:

role = admin
Enter fullscreen mode Exit fullscreen mode

Audit history says:

09:00 user was viewer
09:30 user became editor
10:00 user became admin
Enter fullscreen mode Exit fullscreen mode

The second model contains more architectural information.

It tells you not only what the system is, but how it became that way.


16. Soft Deletes Change System Semantics

Consider:

deleted_at
Enter fullscreen mode Exit fullscreen mode

It looks harmless.

But it changes everything.

If records are never truly deleted, then:

SELECT *
FROM users
WHERE deleted_at IS NULL
Enter fullscreen mode Exit fullscreen mode

becomes part of system semantics.

Now every repository must understand deletion.

Every unique constraint might need reconsideration.

Every relationship must decide whether deleted records remain visible.

Every report must decide whether deleted records count.

Every API must decide what "deleted" means.

One nullable timestamp becomes a system-wide concept.

This is why apparently tiny data-model decisions can become architectural decisions.


17. The Schema Becomes a Contract

APIs are often called contracts.

But databases have contracts too.

Consider:

NOT NULL
UNIQUE
FOREIGN KEY
CHECK
ENUM
Enter fullscreen mode Exit fullscreen mode

These constraints establish promises.

For example:

email IS NOT NULL
Enter fullscreen mode Exit fullscreen mode

means:

Every persisted user has an email.

A foreign key means:

This relationship cannot point into nothingness.

A unique constraint means:

This identity cannot be duplicated.

The database contract is often stronger than the application contract.

Why?

Because the database is usually the final authority over persistence.

You can have five application servers.

Ten API services.

Three background workers.

Two migration scripts.

But if they all write to the same database, the database constraints can protect the shared state.

The model becomes a synchronization boundary.


18. Distributed Systems Make Data Models Even More Important

Now imagine a distributed architecture.

API
 |
 +---- Service A
 |
 +---- Service B
 |
 +---- Service C
 |
 +---- Worker
 |
 +---- Scheduler
 |
 +---- Mobile App
Enter fullscreen mode Exit fullscreen mode

All these components may have different implementations.

But they often depend on shared concepts.

User
Order
Payment
Inventory
Enter fullscreen mode Exit fullscreen mode

The model becomes a common language.

This is one reason distributed systems become difficult when ownership is unclear.

If two services believe they own the same concept, contradictions emerge.

Service A says:
Order = paid

Service B says:
Order = pending
Enter fullscreen mode Exit fullscreen mode

The problem is not necessarily networking.

It may be the absence of a coherent state model.

Distributed architecture is, among other things, the problem of maintaining consistent interpretations of state.

And state begins with data modeling.


19. The Data Model Defines Boundaries

Suppose you have:

Customer
Order
Payment
Inventory
Shipping
Enter fullscreen mode Exit fullscreen mode

Where should the boundaries be?

Maybe:

Customer Service
Order Service
Payment Service
Inventory Service
Shipping Service
Enter fullscreen mode Exit fullscreen mode

But those boundaries are not arbitrary.

They are influenced by ownership of data.

Who owns the order?

Who owns payment state?

Who owns inventory?

Who is allowed to mutate them?

This is why good service boundaries often resemble data ownership boundaries.

Not always.

But often enough to matter.

A useful architectural question is:

Which component is the authoritative source of truth for this piece of state?

That is fundamentally a data-model question.


20. Data Models Eventually Capture Invariants

An invariant is something that must remain true.

For example:

$$
balance \geq 0
$$

Or:

$$
order.total = \sum item_i.price \cdot quantity_i
$$

Or:

$$
shipment.order_id \rightarrow order.id
$$

Or:

$$
membership.user_id \rightarrow user.id
$$

The more mature the system becomes, the more invariants it discovers.

At first, developers keep these rules in their heads.

Then they put them in documentation.

Then validation code.

Eventually, the strongest invariants migrate toward the data layer.

Why?

Because invariants need enforcement.

A rule that exists only in documentation is a wish.

A rule enforced by the database is a property of the system.

This is a profound shift.


21. Architecture Is Constraint Management

We often describe architecture using components:

Frontend
Backend
Database
Queue
Cache
Enter fullscreen mode Exit fullscreen mode

But another way to describe architecture is:

Architecture is the arrangement of constraints under which computation occurs.

The database contributes many of those constraints.

It determines:

  • what states are representable,
  • what relationships are valid,
  • what identities are unique,
  • what dependencies exist,
  • what history survives,
  • what can be removed,
  • what can be changed.

Therefore the schema participates directly in architectural design.

You cannot design the architecture independently of the data model for very long.

The two eventually converge.


22. The Data Model Becomes a Language

A mature system develops vocabulary.

Customer
Account
Subscription
Invoice
Payment
Refund
Settlement
Enter fullscreen mode Exit fullscreen mode

These are not merely table names.

They become nouns in the organization's language.

Developers use them.

Product managers use them.

Support teams use them.

Documentation uses them.

APIs expose them.

Reports use them.

Analytics uses them.

The data model becomes a semantic language.

This is why renaming a database entity can be surprisingly dangerous.

Changing:

Customer
Enter fullscreen mode Exit fullscreen mode

to:

Account
Enter fullscreen mode Exit fullscreen mode

might sound cosmetic.

But perhaps:

Customer ≠ Account
Enter fullscreen mode Exit fullscreen mode

in the business domain.

The database schema is therefore also a vocabulary map.

And changing vocabulary can change how people understand the system.


23. The Data Model Can Outlive the Code

This is one of the strangest properties of software.

Code gets rewritten.

Frameworks change.

Services get replaced.

Frontend technologies disappear.

But data often survives.

A company may migrate:

PHP → Python
Enter fullscreen mode Exit fullscreen mode

then:

Python → Go
Enter fullscreen mode Exit fullscreen mode

while the core business data remains.

Or:

Django → Laravel
Enter fullscreen mode Exit fullscreen mode

while:

users
orders
payments
Enter fullscreen mode Exit fullscreen mode

remain.

The data model becomes historical infrastructure.

New applications must understand it.

This means schemas accumulate institutional memory.

A table may contain decisions made by engineers who left the company ten years ago.

A column may exist because of a requirement nobody remembers.

A strange nullable field may be preserving compatibility with a system that no longer exists.

The schema becomes an archaeological site.


24. Technical Debt Becomes Data Debt

Developers often talk about technical debt.

But data debt is more dangerous.

Bad code can sometimes be rewritten.

Bad data is harder.

Imagine:

10 years of customer records
Enter fullscreen mode Exit fullscreen mode

with inconsistent definitions.

Perhaps one system calls something:

active
Enter fullscreen mode Exit fullscreen mode

while another means:

paid
Enter fullscreen mode Exit fullscreen mode

Another means:

logged in recently
Enter fullscreen mode Exit fullscreen mode

Now the problem is semantic.

You don't merely have messy code.

You have competing realities.

Data debt spreads because every new system must interpret old data.

The cost becomes:

$$
C_{future} \propto D \times N
$$

where:

  • (D) is accumulated data complexity,
  • (N) is the number of systems depending on it.

The exact equation isn't literal accounting mathematics.

It is an architectural intuition:

The more systems depend on an unclear model, the more expensive ambiguity becomes.


25. The Schema Creates Gravity

Software architectures have gravity.

Once a model exists, developers build around it.

Suppose you create:

Product
Category
Supplier
Inventory
Enter fullscreen mode Exit fullscreen mode

Then someone wants a feature.

Instead of asking:

What is the correct domain model?

developers often ask:

How do we fit this into the existing tables?

This is where schema gravity appears.

The existing model starts constraining future designs.

Sometimes this is good.

A stable model creates consistency.

Sometimes it is terrible.

A historical mistake becomes a permanent architectural limitation.

For example:

user.role = "admin"
Enter fullscreen mode Exit fullscreen mode

might work for years.

Then someone needs:

admin in organization A
viewer in organization B
Enter fullscreen mode Exit fullscreen mode

The old model fights the new reality.

You can either redesign the model or build increasingly elaborate exceptions around it.

Most systems choose the second path until the pain becomes unbearable.


26. Migration Is Architectural Evolution

Database migrations are often treated as mechanical operations.

ADD COLUMN
DROP COLUMN
CREATE TABLE
ALTER TABLE
Enter fullscreen mode Exit fullscreen mode

But every migration is potentially an architectural decision.

Adding:

tenant_id
Enter fullscreen mode Exit fullscreen mode

may introduce multi-tenancy.

Adding:

deleted_at
Enter fullscreen mode Exit fullscreen mode

may introduce soft deletion.

Adding:

version
Enter fullscreen mode Exit fullscreen mode

may introduce optimistic concurrency.

Adding:

parent_id
Enter fullscreen mode Exit fullscreen mode

may introduce hierarchical structures.

Adding:

effective_from
effective_until
Enter fullscreen mode Exit fullscreen mode

may introduce temporal modeling.

The migration file may be only five lines.

The architectural consequence may be enormous.


27. Why "Just Add a Column" Is Sometimes Dangerous

Developers love this sentence:

"We'll just add a column."

Sometimes that is exactly right.

Sometimes it is architectural poison.

Suppose you have:

orders.status
Enter fullscreen mode Exit fullscreen mode

and you add:

orders.approved_by
Enter fullscreen mode Exit fullscreen mode

Now approval has become part of order state.

But what if there can be multiple approvals?

You eventually need:

OrderApproval
Enter fullscreen mode Exit fullscreen mode

Then perhaps:

OrderApprovalStep
Enter fullscreen mode Exit fullscreen mode

Then:

ApprovalPolicy
Enter fullscreen mode Exit fullscreen mode

Then:

ApprovalRole
Enter fullscreen mode Exit fullscreen mode

One column becomes a workflow.

The deeper lesson:

When a concept has its own identity, lifecycle, relationships, or history, it probably wants to become an entity rather than remain a column.


28. Normalization Is About Meaning, Not Just Storage

Database normalization is often taught as a method of reducing duplication.

But its deeper purpose is semantic clarity.

Suppose you store:

customer_name
customer_email
customer_phone
Enter fullscreen mode Exit fullscreen mode

inside every order.

You have duplicated meaning.

The order is carrying customer information.

Instead:

Customer
Order
Enter fullscreen mode Exit fullscreen mode

with:

Order.customer_id
Enter fullscreen mode Exit fullscreen mode

you separate concepts.

This distinction becomes important when reality changes.

The customer changes their phone.

The order does not suddenly become a different order.

The model needs to distinguish:

Customer identity
Enter fullscreen mode Exit fullscreen mode

from:

Order history
Enter fullscreen mode Exit fullscreen mode

Good modeling is therefore about identifying which facts belong to which concepts.

That is architecture.


29. Denormalization Is Also Architecture

But then performance arrives.

You might add:

orders.customer_name
Enter fullscreen mode Exit fullscreen mode

for historical snapshots or performance.

Now you have intentionally duplicated information.

That is not necessarily bad.

It simply means you have introduced another architectural rule:

This copy has a particular meaning.

Perhaps:

customer_name_at_purchase
Enter fullscreen mode Exit fullscreen mode

is not a duplicate at all.

It is historical state.

This demonstrates an important point:

A field's meaning matters more than its physical representation.

Data architecture is semantics.

Not just tables.


30. The Frontend Eventually Mirrors the Model

Look at most complex applications.

They contain screens like:

Users
Organizations
Projects
Tasks
Orders
Payments
Reports
Settings
Enter fullscreen mode Exit fullscreen mode

Why these screens?

Because the system has entities.

The UI becomes a visual projection of the domain model.

Consider an order page:

Order
 ├── Customer
 ├── Items
 ├── Payment
 ├── Shipment
 └── History
Enter fullscreen mode Exit fullscreen mode

That structure probably appears in the database.

Then in the API.

Then in frontend state.

Then in the UI.

A model propagates upward.

This is why changing a core entity can cause a chain reaction:

Database
 ↓
Backend
 ↓
API
 ↓
State management
 ↓
UI
 ↓
Analytics
 ↓
Documentation
Enter fullscreen mode Exit fullscreen mode

The model has become the spine.


31. Analytics Creates Another Version of the System Model

Eventually someone asks:

How many customers became active after subscribing?

Now your current-state tables may not be enough.

Analytics wants events.

UserSignedUp
SubscriptionCreated
SubscriptionActivated
SubscriptionCancelled
Enter fullscreen mode Exit fullscreen mode

The system now has two models:

Operational model
Enter fullscreen mode Exit fullscreen mode

and:

Analytical model
Enter fullscreen mode Exit fullscreen mode

The analytical model tries to reconstruct behavior from data.

This reveals something fascinating:

The system model is not necessarily one schema.

It may exist as multiple projections of reality.

Operational State
        |
        v
      Events
        |
        +------> Analytics
        |
        +------> Audit
        |
        +------> Search
        |
        +------> Machine Learning
Enter fullscreen mode Exit fullscreen mode

The same underlying reality produces multiple models.


32. Search Indexes Become Shadow Data Models

Consider Elasticsearch or another search system.

Your relational database says:

Product
Enter fullscreen mode Exit fullscreen mode

Your search index contains:

{
  "name": "...",
  "description": "...",
  "category": "...",
  "brand": "...",
  "price": ...
}
Enter fullscreen mode Exit fullscreen mode

This is another model.

Caching creates another model.

Materialized views create another model.

Data warehouses create another model.

Machine-learning feature stores create another model.

Eventually your architecture contains a constellation of models.

The question becomes:

Which model is authoritative?

This is one of the central problems of modern software.


33. The Real System Is Often the Collection of Its Models

We like to draw:

Application → Database
Enter fullscreen mode Exit fullscreen mode

But modern systems look more like:

                 ┌── Cache
                 │
                 ├── Search Index
                 │
Application ─────┼── Primary Database
                 │
                 ├── Event Stream
                 │
                 ├── Warehouse
                 │
                 └── Materialized Views
Enter fullscreen mode Exit fullscreen mode

Each is a representation.

Each contains some projection of reality.

Therefore architecture is increasingly about maintaining relationships between models.

The hardest problems are often not:

How do we store this?

They are:

Which representation is authoritative?

How quickly must another representation converge?

What happens when they disagree?

Which state is reconstructable?

Which state is permanent?

Now data architecture has become distributed-systems architecture.


34. The Database Is a Memory of the System

Computers execute instructions.

But systems remember.

And memory changes architecture.

A stateless function:

f(x) -> y
Enter fullscreen mode Exit fullscreen mode

does not need to know what happened yesterday.

A system with memory does.

It needs:

identity
state
history
relationships
ownership
time
Enter fullscreen mode Exit fullscreen mode

That is exactly what data models provide.

So perhaps the deeper statement is:

The architecture of a system is largely the architecture of what it remembers.

If a system remembers only current state, it will be shaped around current state.

If it remembers history, it becomes temporal.

If it remembers ownership, it becomes permission-aware.

If it remembers causality, it becomes event-driven.

If it remembers versions, it becomes concurrency-aware.

If it remembers relationships, it becomes graph-like.

The things a system chooses to remember become architectural primitives.


35. Designing Data Models Means Designing Futures

A data model is not merely a description of the present.

It is a decision about which futures are easy.

Suppose you model:

user.role
Enter fullscreen mode Exit fullscreen mode

You have made one future easy:

one user → one role
Enter fullscreen mode Exit fullscreen mode

And another future difficult:

one user → many roles
Enter fullscreen mode Exit fullscreen mode

Suppose you model:

Order.customer_id
Enter fullscreen mode Exit fullscreen mode

you make customer ownership straightforward.

Suppose you instead model arbitrary:

metadata JSON
Enter fullscreen mode Exit fullscreen mode

you make flexibility easy but relational guarantees harder.

Every modeling decision creates a topology of future possibilities.

Some paths become cheap.

Others become expensive.

Therefore:

Schema design is future-cost design.


36. The Best Data Model Is Not the Most Flexible One

This is another trap.

Developers sometimes believe flexibility means:

JSON
metadata
key-value pairs
dynamic fields
Enter fullscreen mode Exit fullscreen mode

Every problem becomes:

{
  "anything": "goes"
}
Enter fullscreen mode Exit fullscreen mode

At first this feels powerful.

But a completely flexible model contains very few constraints.

And fewer constraints mean more responsibility moves upward into application code.

Instead of:

Database guarantees X
Enter fullscreen mode Exit fullscreen mode

you get:

Every developer must remember X
Enter fullscreen mode Exit fullscreen mode

That is dangerous.

Strong systems don't maximize flexibility.

They maximize meaningful constraints.

The goal is not to represent everything.

The goal is to represent reality accurately enough that invalid states become difficult to create.


37. The Data Model Is a Compressed Specification

Think about a schema:

CREATE TABLE order_items (
    order_id UUID NOT NULL,
    product_id UUID NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    price DECIMAL NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

A surprising amount of domain knowledge is encoded here.

We know:

  • an item belongs to an order,
  • it refers to a product,
  • quantity is required,
  • quantity must be positive,
  • price exists.

The schema is therefore a compressed specification.

It says:

Here is what the system believes reality looks like.

That is much more powerful than a list of columns.


38. When the Data Model and Domain Model Diverge

Of course, they do not always need to be identical.

A domain model may say:

Money
Enter fullscreen mode Exit fullscreen mode

while the database stores:

amount
currency
Enter fullscreen mode Exit fullscreen mode

A domain object might expose:

Money(100, USD)
Enter fullscreen mode Exit fullscreen mode

while the persistence model uses two columns.

This is healthy.

The database does not have to perfectly mirror the domain.

But there must be a meaningful translation.

Domain Model
     ↕
Persistence Model
Enter fullscreen mode Exit fullscreen mode

Problems arise when the database model becomes accidental.

When nobody knows why tables exist.

When business concepts are represented inconsistently.

When the application constantly fights the schema.

That is usually a sign the models have drifted apart.


39. The Most Important Question: What Must Never Be False?

When designing a model, developers often ask:

What fields do we need?

A better question is:

What must never be false?

For an inventory system:

stock >= 0
Enter fullscreen mode Exit fullscreen mode

For an organization:

every member belongs to an organization
Enter fullscreen mode Exit fullscreen mode

For payments:

a payment belongs to an order
Enter fullscreen mode Exit fullscreen mode

For identity:

email uniqueness
Enter fullscreen mode Exit fullscreen mode

For accounting:

debits and credits balance
Enter fullscreen mode Exit fullscreen mode

These invariants reveal the true model.

Once you know what must never be false, you can decide:

  • where to enforce it,
  • how to represent it,
  • which relationships are necessary,
  • which constraints belong in the database,
  • which belong in application logic.

This is architectural reasoning disguised as schema design.


40. From CRUD to State Machines

Many systems begin as CRUD.

Create
Read
Update
Delete
Enter fullscreen mode Exit fullscreen mode

But CRUD is often a temporary simplification.

Eventually reality says:

Created
Submitted
Approved
Processed
Completed
Cancelled
Refunded
Enter fullscreen mode Exit fullscreen mode

The system becomes a state machine.

Then:

status
Enter fullscreen mode Exit fullscreen mode

is no longer just a field.

It is an architectural control mechanism.

You can model:

$$
S = {created, submitted, approved, processed, completed, cancelled}
$$

and transitions:

$$
T \subseteq S \times S
$$

The valid transition graph might be:

created
   |
   v
submitted
   |
   v
approved
   |
   v
processed
   |
   v
completed
Enter fullscreen mode Exit fullscreen mode

while:

completed → approved
Enter fullscreen mode Exit fullscreen mode

is invalid.

Once the model reaches this level, your database schema is describing a computational process.

That is the moment data has become system behavior.


41. Why This Matters for Developers

If you are primarily a backend developer, this idea changes how you approach architecture.

Don't ask only:

What endpoint should I build?

Ask:

What state does this endpoint change?

Don't ask only:

What table should I create?

Ask:

What concept am I introducing?

Don't ask only:

What foreign key do I need?

Ask:

What dependency am I declaring?

Don't ask only:

Can this field be nullable?

Ask:

Does this concept logically exist without it?

Don't ask only:

Can we delete this?

Ask:

What historical meaning disappears if we delete it?

These questions produce better systems.


42. A Practical Modeling Workflow

When designing a new system, I like thinking in layers.

Layer 1: Entities

What things exist?

User
Organization
Project
Task
Enter fullscreen mode Exit fullscreen mode

Layer 2: Relationships

How are they connected?

User → Organization
Organization → Project
Project → Task
Enter fullscreen mode Exit fullscreen mode

Layer 3: Invariants

What must always be true?

Task must belong to Project
Project must belong to Organization
Enter fullscreen mode Exit fullscreen mode

Layer 4: State

How do things change?

Task:
todo → doing → done
Enter fullscreen mode Exit fullscreen mode

Layer 5: Ownership

Who controls what?

Organization owns Project
Project owns Task
Enter fullscreen mode Exit fullscreen mode

Layer 6: Time

What history matters?

TaskStatusHistory
Enter fullscreen mode Exit fullscreen mode

Layer 7: Authority

Who can perform which actions?

Role → Permission
Enter fullscreen mode Exit fullscreen mode

Layer 8: Events

What changes should other systems know about?

TaskCreated
TaskCompleted
Enter fullscreen mode Exit fullscreen mode

At this point, you are no longer simply designing tables.

You are designing the system's ontology.


43. A Small Example

Imagine building a warehouse system.

A beginner might start with:

Product
quantity
Enter fullscreen mode Exit fullscreen mode

But reality quickly expands.

You need:

Product
Warehouse
Inventory
InventoryMovement
Supplier
PurchaseOrder
PurchaseOrderItem
SalesOrder
SalesOrderItem
Enter fullscreen mode Exit fullscreen mode

Then:

InventoryMovement
-----------------
product_id
warehouse_id
quantity
movement_type
timestamp
Enter fullscreen mode Exit fullscreen mode

Now inventory is no longer just:

quantity = 500
Enter fullscreen mode Exit fullscreen mode

It is derived from movements.

$$
Inventory_t =
Inventory_0 +
\sum_{i=1}^{n} movement_i
$$

Now the system can answer:

Why is there 500 units?

Because the model remembers the movements.

Then you add:

User
Actor
AuditLog
Enter fullscreen mode Exit fullscreen mode

Now you can answer:

Who moved them?

Then:

Reason
Enter fullscreen mode Exit fullscreen mode

Now:

Why?

The data model has progressively transformed from:

quantity
Enter fullscreen mode Exit fullscreen mode

into:

state + history + causality + authority
Enter fullscreen mode Exit fullscreen mode

That is a system model.


44. When Should You Push Rules Into the Database?

Not every business rule belongs there.

A useful distinction is:

Structural invariants

These often belong close to the database.

Examples:

unique identity
foreign-key validity
non-null requirements
basic numeric constraints
Enter fullscreen mode Exit fullscreen mode

Complex workflows

These may belong primarily in application/domain logic.

Examples:

loan approval rules
pricing algorithms
fraud detection
multi-step authorization
Enter fullscreen mode Exit fullscreen mode

But even then, the database should model the concepts necessary to make those rules enforceable.

The mistake is not choosing database constraints versus application logic.

The mistake is pretending the data model has nothing to do with the rules.


45. The Schema Is a Political Document

There is another layer engineers rarely discuss.

Data models encode organizational decisions.

Suppose you have:

Organization
Department
Team
Enter fullscreen mode Exit fullscreen mode

Someone decided that these concepts exist.

Someone decided their relationships.

Someone decided ownership.

Someone decided what can be deleted.

Someone decided what counts as identity.

The schema therefore contains institutional decisions.

In large organizations, the database can become a map of power.

Who owns customer data?

Who owns billing?

Who controls permissions?

Who can access audit history?

Architecture is never purely technical.

The data model makes many organizational assumptions concrete.


46. Eventually, the System Starts Protecting Its Own Model

At maturity, systems often develop defenses around their data model.

Examples:

foreign keys
constraints
transactions
locks
optimistic versions
audit logs
event streams
validation
authorization
Enter fullscreen mode Exit fullscreen mode

Why?

Because the model has become valuable.

The system must protect the consistency of its reality.

This is the point where:

Database
Enter fullscreen mode Exit fullscreen mode

becomes:

State authority
Enter fullscreen mode Exit fullscreen mode

And once something becomes state authority, the architecture must revolve around it.


47. The Deepest Lesson

The deepest lesson is not:

Databases are important.

Everyone already knows that.

The deeper lesson is:

Data models define the shape of reality that software is capable of remembering.

And whatever a system can remember eventually influences what the system can do.

If you cannot represent ownership, you will struggle with authorization.

If you cannot represent history, you will struggle with auditing.

If you cannot represent state transitions, you will struggle with workflows.

If you cannot represent relationships, you will struggle with domain complexity.

If you cannot represent identity correctly, you will struggle with everything built on identity.

The model is therefore not beneath the architecture.

It is one of the things from which the architecture emerges.


48. The Strange Loop

There is a fascinating feedback loop here.

We start with reality.

Reality
   ↓
Data Model
   ↓
Application
   ↓
System
Enter fullscreen mode Exit fullscreen mode

But then the system changes reality.

System
   ↓
Behavior
   ↓
New Reality
   ↓
New Data
Enter fullscreen mode Exit fullscreen mode

Then new data requires a new model.

So:

Reality
   ↓
Model
   ↓
Software
   ↓
Behavior
   ↓
Reality
   ↓
Model
   ↓
...
Enter fullscreen mode Exit fullscreen mode

Software becomes part of the world it models.

The model changes the system.

The system changes the world.

The world generates new data.

The new data changes the model.

This is why architecture is never finished.

It is continuously negotiating between reality and representation.


49. Data Models Eventually Become System Models

This is the conclusion I keep coming back to.

A data model starts innocently.

User
Product
Order
Enter fullscreen mode Exit fullscreen mode

Then relationships appear.

User → Order
Order → Product
Enter fullscreen mode Exit fullscreen mode

Then constraints.

Order must belong to User
Quantity must be positive
Enter fullscreen mode Exit fullscreen mode

Then state.

pending → paid → shipped
Enter fullscreen mode Exit fullscreen mode

Then history.

OrderStatusHistory
Enter fullscreen mode Exit fullscreen mode

Then authority.

User → Role → Permission
Enter fullscreen mode Exit fullscreen mode

Then tenancy.

Tenant → Organization → Project
Enter fullscreen mode Exit fullscreen mode

Then events.

OrderCreated
OrderPaid
OrderShipped
Enter fullscreen mode Exit fullscreen mode

Then analytics.

Events → Warehouse
Enter fullscreen mode Exit fullscreen mode

Then distributed projections.

Database
  ↓
Events
  ↓
Search
  ↓
Cache
  ↓
Analytics
Enter fullscreen mode Exit fullscreen mode

At this point, calling the database "storage" is almost meaningless.

It has become a structured memory of the system.

The relationships describe architecture.

The constraints describe invariants.

The states describe behavior.

The history describes time.

The ownership describes authority.

The events describe causality.

The schema describes what the system believes can exist.

And that is a system model.


50. Final Thought

Software engineers often spend enormous amounts of time thinking about algorithms.

We think about:

O(n)
O(log n)
O(1)
Enter fullscreen mode Exit fullscreen mode

We think about distributed systems.

We think about queues.

We think about microservices.

We think about containers.

We think about APIs.

But underneath all of these abstractions is a simpler question:

What does the system need to remember?

Because memory creates state.

State creates relationships.

Relationships create constraints.

Constraints create boundaries.

Boundaries create architecture.

And architecture eventually shapes behavior.

That is why the data model is more powerful than it first appears.

A database table is not merely a bucket of rows.

A foreign key is not merely a pointer.

A constraint is not merely validation.

A status column is not merely metadata.

A timestamp is not merely a date.

A history table is not merely logging.

These are statements about reality.

And when enough of those statements accumulate, they stop describing a system from the outside.

They become the system's internal model of itself.

The most important architectural diagram in your project might therefore not be the one hanging on the wall.

It might be the schema.

Because if you look closely enough, the schema tells you what your software believes is real.

And software eventually becomes constrained by what it believes is real.

Data models eventually become system models.

Not because we planned it that way.

But because systems cannot escape the architecture of what they remember.

Top comments (1)

Collapse
 
raknaos profile image
Raknaos

Reading the schema as persisted behaviour explains a migration failure I keep running into: the expensive part is never moving columns, it is that a CHECK or UNIQUE constraint is often the only written form of a business rule, and it is invisible in every code review. Dropping one looks like schema cleanup and silently removes a state machine that nobody knew was documented there.

The dependency-graph view also earns its keep on delete paths. Blast radius does not appear in the ORM the way it appears in the graph — ON DELETE CASCADE on a table everyone assumed was a leaf turns a test-data cleanup into a production incident, and no application code changed to warn you.

The "most dangerous tables are not always the largest" line belongs in an onboarding doc. The question that finds them fast is the inverse one: which rows would be impossible to reconstruct? Permission, feature-flag and configuration tables rank above the big event table almost every time, because the event table is usually rebuildable from the log while those are not.