The Systems Gap: Why SQL and Graphs Struggle with Complexity

Most production systems don’t fail because they’re slow. They fail when their structure becomes impossible to reason about. This post explains why SQL and graph databases struggle as systems grow more complex.

Cal Hemsley


Most production systems do not break because they are slow.

They break slowly over time when they become too complex to reason about with confidence.

From my experience, this rarely happens during a rewrite or a migration (although not to say it doesn’t, but that’s another issue entirely). It happens in the edge cases. A schema change to support a new feature. A one-off exception added under pressure. A role that does not quite fit the original model. Each change is defensible on its own. Taken together, they quietly change what the system actually is, and run up the technical debt.

Months – years – later, the system still runs. Tests pass. Dashboards stay green. But when someone asks a basic question about behaviour rather than data, the answers become much more difficult. The answer shifts to an uncomfortable “it depends”, or “let me go figure that out and come back” (spoiler alert, they rarely do without much chasing).

That moment tends to arrive long before anyone admits the data model is part of the problem.

When permissions stop being data

Permissions are a useful example to highlight because they are familiar, and because they almost never stay simple.

On day zero, the model is straightforward. Users can read or write files. You represent this with a join table. It’s clean, un-controversial. Everyone understands it, including non-engineers like me.

Then the requirements grow.

You add groups.

Then temporary access.

Then nested groups.

Then contractors.

Then inheritance with overrides, plus the inevitable special cases.

None of these requests are particularly surprising. They are the normal shape of real software. The trouble is that the original data model was never designed to express this shape directly.

Eventually someone asks a question that sounds trivial.

“Who can edit this file?”

Thanks nano banana pro for a semi-reasonable visual.

If you are using a relational database, you do not answer that question by looking at a single table. You answer it by assembling logic brick by brick.

Often that logic ends up scattered across joins, conditions, and traversal patterns that reconstruct the permission hierarchy on the fly.

The journey is familiar to all of us. Even if you have never written a recursive query yourself, you have almost certainly inherited code that works around one.

The traditional answer has always been: keep business logic out of the database. Put it in the application.

In simple systems, that separation works well. In complex systems, the boundary starts to blur. Is “admin access” data, or is it logic? Is inheritance a rule, or just a query pattern?

When the database cannot express the hierarchy, the query has to do it.

The hierarchy rules live here.

The inheritance logic lives here.

The safety assumptions live here.

At some point it becomes obvious, and slightly uncomfortable, that the database is storing facts, while the system itself lives somewhere else.

This is not about bad databases

It is tempting to frame this as a criticism of SQL. It’s not (Really).

Relational databases are exceptionally good at what they were built for. They store records reliably. They enforce local constraints. They execute queries with remarkable efficiency. If you are building a ledger, a log, or a conventional CRUD application, they remain the best tool for the job.

The issue appears when we ask a record-oriented tool to behave like a model of a system.

Records answer questions such as what exists and what values are stored. Systems answer questions about behaviour. What is allowed. What follows from a change. What must never happen..

When those questions have no natural home in the data layer, they are answered elsewhere. Usually in application code. Sometimes in queries. Often in both.

That translation layer is where complexity accumulates. You end up with spaghetti code.

Records and systems are different things

A record-based model treats data as independent facts. Rows, columns, edges. Relationships are reconstructed at query time.

A system has structure beyond that. It has entities that play roles. It has relationships with meaning. It has constraints that apply globally, not just locally. It has expectations about how change should behave.

When that structure is not represented directly, it does not disappear. It leaks into:

  • SQL queries that are harder to reason about than the data they operate on.
  • Application code that exists only to compensate for missing guarantees.
  • Documentation that becomes outdated the moment a special case is added.

The system continues to function, but its structure becomes implicit. That is when understanding starts to degrade.

Why SQL ends up carrying logic it was never meant to carry

Relational databases represent entities as tables and relationships as foreign keys. The constraints they provide are useful, but limited. They are largely local. They do not describe behaviour.

To express system-level rules, engineers are forced to encode meaning procedurally. Joins become logic. WHERE clauses become policy. Query shape becomes part of the domain model.

This is why queries grow sideways. Each new requirement introduces another join, another condition, another assumption. (Don’t forget to check at least a hundred times to make sure you don’t accidentally order the joins wrong, and still do so).

The original question remains simple. Who can edit this file?

The implementation becomes a specific traversal strategy that implies a particular interpretation of the data which may or may not be truly accurate.

SQL does not lose structure here. It never had a place to store it.

Where graphs help (and where they don’t)

Graph databases address a real pain point. They make connectivity explicit. Traversals feel natural. Hierarchies are easier to express. For problems of pure topology, such as finding shortest paths or exploring networks, they are a major step forward.

But connectivity is not the same as structure.

The limitation is not that graphs lack schemas. Many support them. The limitation is that relationships are still structurally thin.

In most graph models, an edge is a labelled connection between two nodes. It may carry properties, but it rarely enforces constraints or roles.

Consider a permission relationship. Is it read or write? Is it inherited? Does it expire? Can it be overridden? Can it conflict with another permission?

To express this, teams introduce conventions. Intermediate nodes. Encoded assumptions. Application-side interpretation.

The graph shows that things are connected. It does not enforce how they are allowed to be connected, or what those connections imply.

So the ambiguity moves, but it does not go away. The system logic still lives outside the database.

The gap that keeps appearing

By now, the pattern should feel familiar. At the bottom of the stack, databases are excellent at storage, indexing, and transactions.

At the top, application code is excellent at expressing behaviour. Between them, there is often nothing.

There should be a place where system structure lives. A place where invariants are enforced. A place where relationships carry meaning beyond traversal.

Instead, that space is empty.

When it is empty, application code expands to fill it. Glue code accumulates. Refactors become risky. Bugs cluster around edge cases that nobody fully understands.

This is not a failure of discipline or talent. It is a structural absence.

That absence is the systems gap.

Closing the systems gap

If you take this seriously, the requirements for a solution are not abstract.

A system-aware database would need to treat relationships as more than links. It would need to understand roles within those relationships. It would need to enforce constraints globally, rather than relying on every application to do so independently.

In other words, it would need to store meaning alongside data.

Importantly; This is not an argument for abandoning SQL or graph databases. Both remain essential tools. It is an argument for recognising that modern systems demand something additional.

As systems grow more interconnected, the distance between storing facts and modelling reality becomes harder to ignore.

In the next post, we will look at where this gap hurts most in practice: inside the queries engineers are forced to write.

Share this article

TypeDB Newsletter

Stay up to date with the latest TypeDB announcements and events.

Subscribe to newsletter

Further Learning

Feedback