Development

Beyond the Prompt: Architecting Databases AI Can't Deceive

Beyond the Prompt: Architecting Databases AI Can't Deceive

AI can produce a convincing database schema in seconds. It can name tables well, suggest indexes confidently, and explain trade-offs in language that sounds like it came from a careful architecture review. That is useful. It is also dangerous.

The problem is not that an AI model is uniquely unreliable. The problem is that databases punish assumptions more severely than most parts of an application. A plausible-looking schema can accept invalid data for years. A missing constraint can turn a transient API bug into permanent corruption. A convenient denormalization can become a reconciliation job nobody owns.

The goal is not to make an AI write fewer SQL statements. It is to design a system where neither AI-generated code nor hurried human code can quietly violate the rules that matter.

Move business truth below the application layer

Application validation is valuable, but it is not a trustworthy final boundary. Requests can arrive through a web controller, a queue consumer, a maintenance script, an import, an admin tool, or a future service that bypasses today’s validation path.

If a rule must always be true, encode it where every writer must confront it: in the database.

Consider an order line. It should not exist without an order. Its quantity should be positive. The same product should not accidentally appear twice when the business model says one row represents the aggregate quantity for that product.

CREATE TABLE order_lines (
    id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    order_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    unit_price_cents INTEGER NOT NULL CHECK (unit_price_cents >= 0),
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_order_lines_order
        FOREIGN KEY (order_id) REFERENCES orders(id),

    CONSTRAINT fk_order_lines_product
        FOREIGN KEY (product_id) REFERENCES products(id),

    CONSTRAINT uq_order_lines_order_product
        UNIQUE (order_id, product_id)
);

These constraints are not documentation. They are executable policy. An AI may forget a check constraint, misunderstand an edge case, or offer an elegant shortcut. The database can still reject an impossible state.

Model invariants before tables

Teams often begin with entities: users, invoices, subscriptions, messages. A stronger starting point is invariants: facts that must remain true regardless of which endpoint, job, or developer changes the data.

For each important workflow, ask questions such as:

  • Can this record exist without its parent?
  • Which values are mandatory, finite, unique, or immutable?
  • Can a relationship be duplicated?
  • Which state transitions are valid?
  • What must remain true under concurrent requests?
  • What data may be deleted, and what must be retained?

Some invariants fit naturally into NOT NULL, UNIQUE, foreign keys, and check constraints. Others require transactions, carefully designed update statements, or a small amount of server-side database logic. The important distinction is to identify them explicitly rather than hoping a generated ORM model captures them by accident.

State transitions deserve particular attention. A generic status column is easy to add and easy to misuse. If an invoice can move from draft to issued and then paid, decide whether it can be edited after issue, whether it can be cancelled after payment, and which actor is allowed to make each change. Those decisions belong in the design, not in a prompt asking an AI to “handle invoice statuses.”

Use the database as a concurrency boundary

Many data bugs are not validation bugs. They are race conditions wearing business-language disguises.

Imagine an inventory reservation endpoint. Two requests both read an available quantity of one, both decide they may reserve it, and both write a reservation. Every individual query may look reasonable. The sequence is not.

A safer approach makes the condition part of the write:

UPDATE inventory
SET available_quantity = available_quantity - :requested
WHERE product_id = :product_id
  AND available_quantity >= :requested;

The application then checks the affected-row count. One affected row means the reservation succeeded; zero means stock was unavailable. This turns a read-then-write race into one atomic decision. If the reservation also creates related records, place the work in a transaction and define what happens when any step fails.

AI can suggest transactions, but it cannot infer your consistency requirements from table names alone. A technical lead should be able to point to each workflow and state what is atomic, what can be eventually consistent, and what must be idempotent.

Design APIs for retries, not ideal networks

Networks fail after a server has committed a transaction but before the client receives the response. Queue consumers can receive a message more than once. Deployment rollbacks can replay work. These are ordinary operating conditions, not exotic failure scenarios.

For externally initiated write operations, an idempotency key can be more valuable than a clever prompt. Store a caller-provided key with enough scope to identify the operation, enforce uniqueness, and return the original result when the same operation is retried.

CREATE TABLE payment_requests (
    id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    account_id BIGINT NOT NULL,
    idempotency_key VARCHAR(255) NOT NULL,
    request_hash VARCHAR(64) NOT NULL,
    payment_id BIGINT NULL,

    CONSTRAINT uq_payment_request
        UNIQUE (account_id, idempotency_key)
);

The exact implementation depends on the database and API contract, but the principle is stable: retries should not create duplicate side effects. Also compare a stored request fingerprint when appropriate. Reusing an idempotency key for a materially different request should produce a clear conflict, not silently return an unrelated prior result.

Make schema changes survivable

Generated migrations often succeed on an empty local database and fail in production because real databases are large, old, and actively receiving writes. Treat each migration as a deployment artifact with an operational plan.

Prefer additive, staged changes. Add a nullable column first. Deploy code that writes both the old and new representation. Backfill in controlled batches. Add constraints only after existing rows satisfy them. Switch readers. Remove obsolete fields in a later release.

This approach is less dramatic than a one-shot migration, but it gives rollback options and limits lock duration. It also forces useful questions: Can old and new application versions run simultaneously? Does an index build block writes on this database engine? How will a failed backfill resume safely?

Docker helps make local development repeatable, but it does not make migrations safe by itself. A containerized database is excellent for testing schema setup and migration sequences. It is not a substitute for testing against realistic data volume, transaction behavior, and deployment ordering.

Give AI narrow jobs and verifiable inputs

AI is most useful when the architecture already supplies guardrails. Ask it to draft a migration after you provide invariants, target database version, table definitions, expected data volume, and rollback requirements. Ask it to review an index plan, then validate its suggestions with query plans and representative workloads.

Do not ask it to decide silently whether a foreign key, unique constraint, or transaction is necessary. Those are design decisions with business consequences.

A good review checklist is simple: What prevents invalid rows? What prevents duplicate effects? What happens if this operation runs twice? What happens if it fails halfway through? What happens when two requests run at once? What happens during a rolling deployment?

The durable advantage is resistance to mistakes

The best database architecture does not assume every developer will remember every rule, every API will be called correctly, or every generated snippet will understand the domain. It expects mistakes, retries, partial failures, concurrent traffic, and future change.

That is what makes a system difficult to deceive. Not skepticism toward AI, but a design in which confidence is never accepted as proof. Put truth in constraints, make writes atomic, design for retries, and evolve schemas in stages. Then AI becomes a faster assistant inside a system that still knows how to say no.

Blog author portrait

Mihajlo

I’m Mihajlo — a developer driven by curiosity, discipline, and the constant urge to create something meaningful. I share insights, tutorials, and free services to help others simplify their work and grow in the ever-evolving world of software and AI.