We are the kings of the code.
That may sound a little grandiose, but consider what happens when we use AI agents to develop software. We issue instructions, delegate responsibilities, establish standards, and expect our agents to carry out our intentions. We may not personally write every line of code, but every line written in our name ultimately belongs to us.
Like the kings of the past, we have vassals who act on our behalf. And like those kings, we must accept full responsibility for what our vassals do in our name.
When an AI agent makes a poor architectural decision, we don’t get to shrug and say, “Well, that’s what the AI wrote.” When it compromises security, corrupts data, or introduces an architectural flaw that costs thousands of hours to correct, the responsibility belongs to the engineer who delegated the work.
This is an important distinction in the emerging relationship between human engineers and AI coding agents. Delegating the implementation of a task does not mean delegating accountability for the result. In fact, the more work we delegate, the more important it becomes to establish clear principles governing how that work is performed.
And few examples illustrate this better than a mistake we learned to avoid more than fifty years ago: storing multiple relational values in a single database field.
A Mistake Older Than Most Programmers
One of the first principles we learned about relational databases was that a field should contain a single value. Not a comma-separated list of IDs. Not a serialized collection of related entities. Not a JSON array masquerading as a relationship.
One value.
The principle comes from First Normal Form, one of the foundations of relational database theory. There are technical subtleties to what constitutes an atomic value, but the practical engineering lesson is straightforward: when individual elements represent relationships between entities, those relationships belong in tables, not buried inside fields.
Suppose you’re developing an application where users can belong to multiple organizations. You ask your AI coding assistant to implement organization membership, and it decides to add a field to the existing users table:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name VARCHAR(100),
organization_ids JSON
);
A user’s record might now contain:
{
"id": 42,
"name": "Daniel",
"organization_ids": [12, 17, 23]
}
It’s an appealing solution. No additional tables, no joins, and very little disruption to the existing code. The AI updates the model, writes the migration, implements the API endpoints, and produces a complete set of passing tests. It may even proudly report that the feature is finished.
And that’s precisely the problem. The feature works, but the architecture is wrong.
What happens when you want to find every user belonging to organization 17? Instead of using a straightforward relational join, you need database-specific logic to search within a JSON array. What happens when organization 17 is deleted? An ordinary foreign key cannot protect references embedded inside that JSON document. What happens when you need to track membership roles, join dates, or approval status? Your convenient list begins evolving into an increasingly elaborate collection of objects.
And when two processes simultaneously update organization membership, the entire JSON field may need to be read, modified, and written back, introducing concurrency problems that a properly designed relationship table could have avoided.
Every feature built on this foundation becomes a little more complicated. Every developer who touches it inherits the original mistake. Eventually, someone suggests a major refactoring, and everyone wonders why the database was designed this way in the first place.
The answer may be that an AI agent saved itself the trouble of creating one more table.
We Already Know How to Solve This
The appropriate relational design isn’t complicated. It’s the solution we’ve been teaching developers for decades.
CREATE TABLE user_organizations (
user_id INTEGER NOT NULL,
organization_id INTEGER NOT NULL,
PRIMARY KEY (user_id, organization_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (organization_id)
REFERENCES organizations(id)
);
Now each membership is a proper relational record. The database can enforce referential integrity, prevent duplicates, and efficiently index relationships. You can add attributes describing the membership without restructuring the entire representation.
More importantly, the database itself helps enforce the rules of the application. The constraints exist independently of whatever code happens to be accessing the data.
That’s one of the great advantages of a relational database: correctness doesn’t depend entirely on every future programmer remembering to do the right thing.
I’ve explored this broader philosophy in Database Best Practices: Designing for Failure, Not Hope. A well-designed system doesn’t merely assume programmers will behave correctly. It uses architectural constraints to prevent entire classes of mistakes.
And yes, modern relational databases support JSON, arrays, XML, and other complex data types. PostgreSQL has excellent JSONB functionality, including indexing and sophisticated query operations. These capabilities are genuinely valuable when you’re storing documents, event payloads, or structured values that should be treated as a unit.
But the existence of a data type is not an argument for using it to represent relationships.
The critical question is whether the individual values have independent meaning within your data model. If they refer to other entities, require independent updates, need constraints, or participate in relationships, they probably belong in relational tables.
The distinction isn’t between old-fashioned relational databases and modern JSON capabilities. It’s between using the appropriate abstraction and taking a shortcut.
AI Knows the Rule. That Doesn’t Mean It Will Follow It.
Here’s what makes this problem particularly interesting.
Ask a modern AI coding model to explain database normalization, and it will probably give you an excellent explanation. Ask about First Normal Form, foreign keys, many-to-many relationships, or the problems with comma-separated values, and it will demonstrate substantial knowledge.
So why would that same model create a multivalued relationship field?
Because knowing a principle is not the same thing as consistently applying it.
An AI agent working on a software feature has an immediate objective. It has been asked to implement something, and it is trying to produce a satisfactory result. Depending on its instructions, context, and available tools, it may favor an implementation that requires fewer changes and appears to satisfy the task quickly.
Adding a JSON column can look easier than creating a junction table, updating database models, changing queries, and adjusting tests throughout the application.
And once the model has chosen that implementation, it may generate tests that validate its own assumptions. The tests pass because the tests exercise what was built, not necessarily what should have been built.
Think about that for a moment. The same agent can make an architectural mistake, write the code implementing that mistake, generate tests that confirm the code behaves as designed, and announce that everything is working correctly.
The AI hasn’t necessarily done anything malicious, or even anything inconsistent with its immediate assignment. It has simply optimized for completing the requested work without sufficiently respecting an architectural principle that should have governed the implementation.
An experienced engineer might recognize the danger immediately because we’ve lived through the consequences of these shortcuts. We’ve inherited badly designed databases, performed painful migrations, and spent days tracing bugs caused by integrity rules that should have been enforced by the database engine.
I made a related argument in Software Engineering Isn’t That Hard. Architecture Is.. Generating code is only one part of software engineering. Deciding what code should exist, how systems should be structured, and which tradeoffs are acceptable requires a different kind of judgment.
We cannot assume the AI will independently apply every lesson that experience has taught us, particularly when doing so makes its immediate task more complicated.
The King’s Laws
Throughout history, effective rulers couldn’t personally supervise everything happening across their kingdoms. They established laws, delegated authority, and created mechanisms to enforce those laws.
We need to do the same with our AI agents.
Fortunately, modern AI-assisted development tools increasingly support persistent instructions that define how agents should behave. GitHub’s Spec Kit, for example, introduces the idea of a project constitution: a set of governing principles that guide specification, planning, and implementation.
I’ve previously explored how this kind of discipline improves development in AI + Speckit: Turning Observability into Faster, More Predictable Engineering.
A constitution is an excellent place to encode engineering lessons that should not be casually ignored. For a project using a relational database, here’s a rule you can add directly to your Spec Kit constitution.
Article I: Relational Data Integrity
Add the following Markdown to .specify/memory/constitution.md:
## Relational Data Integrity (NON-NEGOTIABLE)
All relational database designs MUST preserve
normalization and referential integrity.
1. Multivalued fields MUST NOT be used to represent
relationships between entities. This prohibition
includes comma-separated values, arrays, JSON,
JSONB, and other serialized collections of
foreign keys or independently managed entities.
2. Many-to-many relationships MUST use junction
tables with appropriate primary keys, foreign
keys, and indexes.
3. Referential integrity MUST be enforced through
database constraints wherever supported.
4. JSON, arrays, and other structured types MAY be
used for self-contained document-oriented data
that does not represent relational associations.
5. Before implementing any database schema change,
the agent MUST review the design for normalization
violations and document its findings.
6. If a proposed implementation would violate these
rules, the agent MUST stop, explain the conflict,
and request explicit human approval before
proceeding. Approval of an exception MUST include
a documented technical justification.
7. The agent MUST NOT silently bypass, weaken, or
reinterpret these requirements to simplify
implementation or satisfy a feature request.
Compliance with this article is a prerequisite for
accepting any database schema change.
That is a law of the kingdom, expressed in terms an AI agent can understand and evaluate.
In Spec Kit, the constitution can be established or revised through /speckit.constitution. Other coding environments have their own mechanisms for persistent project instructions, whether through repository instruction files, agent configurations, or reusable development rules.
The particular tool isn’t important. What matters is that we’re transforming years of engineering experience into explicit instructions that govern the behavior of our agents.
And we shouldn’t stop at database normalization. We can establish laws governing test-driven development, security, dependency management, source control, architecture, documentation, and any other practices we consider essential.
Our agents shouldn’t have to guess what good engineering means in our projects. We should tell them.
A Law That Isn’t Enforced Is Just a Suggestion
Of course, writing a constitution doesn’t guarantee that an AI will obey it.
Agents can overlook instructions, misinterpret requirements, or produce implementations that technically satisfy the wording of a rule while violating its intent. Anyone who has spent time with AI coding tools has seen an agent confidently explain why its solution is correct, only to discover that it has misunderstood an important constraint.
That’s why the next step is enforcement.
If database normalization is a constitutional requirement, schema changes should be reviewed against it. Every migration involving a new multivalued column should trigger scrutiny. Every new relationship should be evaluated to ensure that integrity is enforced by the database wherever practical.
We can automate part of this process. Static checks can flag suspicious schema definitions, CI pipelines can require architectural reviews, and a separate AI agent can inspect proposed migrations specifically for violations of the constitution.
I particularly like the idea of using narrowly focused agents for this work. Rather than asking one enormous AI agent to simultaneously design, implement, test, and validate every aspect of a feature, we can assign specialized responsibilities. One agent implements, another reviews database architecture, and another evaluates compliance with the specification.
This is not unlike the surgical-team model Frederick Brooks described in The Mythical Man-Month. Different participants have distinct responsibilities, but the work remains organized around a clear authority and shared engineering standards.
There is an important limitation, however. Having another AI review the code doesn’t automatically make the result correct. If both agents share the same mistaken assumptions, the second may simply endorse the first.
The review must be anchored in independently defined requirements, and important safeguards should be automated wherever possible.
The king can delegate inspections. He cannot delegate responsibility.
The Danger of an Eager-to-Please Vassal
In When AI Is Smart Enough to Get Around the Rules, I described an encounter with Cursor in which the AI attempted to work around a Git safeguard instead of respecting the development process it was supposed to follow.
The underlying problem was revealing. The agent wasn’t ignorant of Git. It was capable of understanding branches, uncommitted changes, and the reasons those protections exist. But faced with an immediate development objective, it proposed a path that bypassed the very safeguards designed to protect the project.
The AI wasn’t necessarily trying to undermine our engineering process. It was trying to be helpful.
But eagerness to please isn’t always a virtue.
The same phenomenon applies to database architecture. An AI that knows all about normalization may nevertheless decide that a JSON array is the quickest way to implement a feature. It can even generate a perfectly persuasive explanation for why its implementation works.
And technically, it might work quite well today.
Sometimes the most valuable thing an engineering partner can say is, “I can implement that, but doing it this way would violate our architectural standards.”
That’s the behavior we should be cultivating in our agents. We don’t merely want them to know our rules. We want them to recognize when a requested implementation conflicts with those rules, explain the conflict, and refuse to quietly take a shortcut.
A good agent should occasionally tell its king that the proposed course of action is a bad idea.
And a good king should listen.
Faster Development Demands Stronger Discipline
None of this is an argument against AI-assisted software development. Quite the opposite. I believe AI gives us an extraordinary opportunity to improve both our productivity and the quality of the software we produce.
But the fundamental lessons of software engineering don’t become obsolete just because we can generate code faster.
Database normalization, automated testing, version control, code review, architectural standards, and continuous integration exist because generations of engineers learned, frequently the hard way, what happens without them.
I discussed this broader need for structure in Vibe Coding Needs Guardrails: Why Rigid Ecosystems Are Safer for AI-Generated Code. The more freedom we give AI to implement solutions, the more valuable it becomes to establish boundaries that prevent predictable engineering mistakes.
AI hasn’t repealed those lessons. It has dramatically increased the speed at which we can ignore them.
A poor architectural choice that once took a developer several hours to implement can now spread throughout an application in minutes. By the time anyone notices, the implementation may be deeply embedded in models, APIs, tests, and user interfaces.
This is why I believe the next major improvement in AI-assisted development won’t simply come from models that write better code. It will come from engineering organizations that become better at establishing and enforcing the principles governing that code.
We need to move beyond asking AI to build features and toward creating disciplined development environments in which architectural integrity is an explicit condition of success.
That means writing constitutions, defining specialized agent responsibilities, incorporating automated safeguards, and teaching our AI partners not merely how to accomplish tasks, but how we expect those tasks to be accomplished.
Because ultimately, we are the kings of the code.
Our AI agents may write it. They may test it. They may review it. They may even deploy it.
But they do all of those things in our name.
And when someone discovers a JSON array full of foreign keys buried in our database five years from now, nobody is going to care which AI model put it there.
They’re going to ask who was responsible for the system.
The answer is us.

Leave a Reply