Azure Cosmos DB Data Modeling for AI Applications

Designing Better Data Models with an AI Coding Agent: Where Cosmos DB Teams Should Draw the Line

Azure Cosmos DB Data Modeling for AI Applications

“Can the agent design our Cosmos DB model for us?”

On this page

It can draft one fast. Production traffic is where you find out whether anyone actually designed it.

I’m bullish on AI coding agents. I use them. I push them hard. They’re great at generating entities, nested JSON, relationship diagrams, seed data, SDK queries, and config scaffolding in minutes. Microsoft is clearly leaning into this direction across Cosmos DB guidance for AI agents and vector workloads, Data API builder’s agent integration story, Fabric data agents, and the broader governance guidance for adopting agents across the estate Azure Cosmos DB docs Data API builder docs Fabric data agents GA Cloud Adoption Framework guidance.

The mistake is letting that competence create false confidence.

A clean model is not a production model. A convincing set of classes is not a workload strategy. And in Cosmos DB, the expensive mistakes are almost never “the JSON looked ugly.” They’re partitioning mistakes, indexing mistakes, consistency mistakes, and region decisions made by people who were still admiring the generated schema.

In Q1, I reviewed a team’s Cosmos DB design where the agent had produced beautiful order, customer, and shipment documents in under 10 minutes, and the first replay against a skewed tenant mix drove one tenant to dominate RU consumption by lunchtime because /tenantId looked semantically right but the write path was concentrated through three integration workers.

That’s the line I want teams to draw: use the agent to accelerate drafts; keep humans on the hook for workload economics and operational risk.

The dangerous competence of AI-generated schemas

Here’s why this goes wrong so often.

If you ask an agent, “Design a data model for an order management system in Cosmos DB,” you’ll get something polished enough to pass a design review with the wrong audience. You’ll see:

  • sensible entities
  • nested documents
  • relationship assumptions
  • sample data
  • CRUD queries
  • maybe even a partition key recommendation with indexing policy

That output feels like progress because it is progress at the code artifact layer.

But Cosmos DB does not reward elegance in the abstract. It rewards models that fit the actual request path. If your hot reads, writes, tenant distribution, retention rules, and latency targets aren’t settled first, the agent is optimizing for readability and pattern completion, not for your production traffic.

That’s the same issue I’ve been hammering in The next phase of AI is not bigger demos, but better fit-for-purpose systems. Good systems win on fit, not on how quickly they can generate artifacts.

So yes, let the agent draft aggressively. Just stop pretending the draft is the design.

Start with access patterns, not generated entities

When I’m working with a Cosmos DB team, I want the first approved artifact to be a workload sheet, not a schema diagram.

Before anyone accepts an entity model, document this stuff:

  • top read paths
  • top write paths
  • filters and sort patterns
  • tenant boundaries
  • retention requirements
  • latency expectations
  • expected growth
  • cross-region traffic assumptions
  • any transaction-like workflow that the app treats as atomic even if the database doesn’t

Then hand that to the agent.

That changes the quality of the output immediately. Instead of “infer a domain model,” the prompt becomes “generate candidate document shapes and query patterns that satisfy these reviewed access patterns.” Now the agent is working inside constraints that matter.

This is also where over-normalization shows up. Coding agents often preserve relational instincts because those patterns are everywhere in training data and enterprise codebases. You’ll get separate documents for things that should probably be embedded or duplicated for the dominant read path. It looks tidy. It performs like a committee.

A practical pattern I like is:

  1. humans define access patterns
  2. agent proposes 2-3 document shapes
  3. humans challenge every relationship
  4. team keeps only the shape that minimizes pain on the real request path

If your application usually needs order header, current status, payment summary, and top shipment milestone together, don’t let a generated “clean” model split those into four containers because it resembles a relational design.

Partition keys remain an architectural decision

This is the boundary I enforce hardest: do not outsource partition-key selection to an AI coding agent.

You can absolutely ask the agent for candidate keys. You should. It can enumerate options faster than a room full of architects with dry erase markers. But a human has to approve the choice after reviewing:

  • write distribution
  • read routing
  • data size growth
  • tenant isolation
  • skew risk
  • future query patterns
  • operational scaling assumptions

A partition key that looks obvious in the model can still be disastrous in production. Popular tenants get hot. Time buckets get hot. Device IDs get hot. Workflows with bursty writes get hot. Semantic neatness has almost nothing to do with whether the key survives traffic.

Here’s a lightweight review loop I like for agent-assisted modeling:

Diagram 1

The point of that flow is simple: the agent can propose, but the workload harness gets a vote before production does. If you don’t have that loop, you’re trusting syntax over evidence.

To make the test concrete, I usually generate a skewed operation set first. Not production-perfect. Just reviewable enough to expose bad assumptions early.

# Generate skewed tenant operations for a reviewable Cosmos DB workload test.
import random
from collections import Counter

def build_ops(tenants=20, writes=200, reads=120, hot_ratio=0.7):
    ids = [f"tenant-{i:02d}" for i in range(tenants)]
    hot = ids[: max(1, tenants // 5)]
    ops = []
    for i in range(writes):
        tenant = random.choice(hot if random.random() < hot_ratio else ids)
        ops.append({"kind": "write", "tenantId": tenant, "id": f"doc-{i:04d}"})
    for _ in range(reads):
        tenant = random.choice(ids)
        ops.append({"kind": "read", "tenantId": tenant, "id": f"doc-{random.randint(0, writes-1):04d}"})
    random.shuffle(ops)
    return ops

ops = build_ops()
print("sample:", ops[:5])
print("tenant skew:", Counter(op["tenantId"] for op in ops).most_common(5))

Run something like that against each candidate design. You’re looking for skew, not beauty. If one tenant or workflow dominates the operation mix, your partition-key discussion just got real.

Then compare candidate strategies under the same synthetic pattern:

# Compare two candidate partition-key strategies using the same synthetic workload observations.
import random

def score(strategy):
    total_ru = 0.0
    hot_hits = 0
    for _ in range(200):
        tenant = random.choice(["tenant-00"] * 7 + [f"tenant-{i:02d}" for i in range(1, 10)])
        is_hot = tenant == "tenant-00"
        if strategy == "tenantId":
            total_ru += 7.5 if is_hot else 5.5
            hot_hits += 1 if is_hot else 0
        else:
            total_ru += 6.2
            hot_hits += 0
    return {"strategy": strategy, "total_ru": round(total_ru, 1), "hot_partition_events": hot_hits}

for name in ("tenantId", "tenantId+bucket"):
    print(score(name))

What should you observe? Not which strategy has the prettiest name. Look at total RU behavior and whether one key creates concentrated hot events. Sometimes the “pure” key is the wrong key. Sometimes a bucketing strategy is worth the extra complexity. Sometimes tenant isolation matters more than smoothing the write path. Those are product decisions, not autocomplete decisions.

I made a similar point in Azure Cosmos DB Is the Agent Memory Bet: memory systems succeed or fail on access behavior and operational design, not on whether the document examples look clever.

Consistency, indexes, and regions are not boilerplate

This is where teams get lazy because the generated config looks reasonable.

Reasonable is not the bar.

Consistency settings affect application correctness and user experience. Indexing policy affects write cost and query efficiency. Region layout affects latency, availability posture, and the bill. None of that should ride along as default-looking boilerplate under a generated schema.

I want the agent to help with the inventory:

  • which fields are filtered
  • which fields are sorted
  • which query combinations repeat
  • which large properties should be excluded from indexing
  • which assumptions the app is making about read-after-write behavior
  • which regions users and services actually hit

Then I want a human review before deployment.

A good example is drafting a container configuration with explicit choices instead of hidden defaults:

# Draft a Cosmos DB container configuration with explicit partition-key and indexing parameters.
param(
    [string]$DatabaseName = "appdb",
    [string]$ContainerName = "orders",
    [string]$PartitionKeyPath = "/tenantId",
    [int]$DefaultTtl = -1,
    [string[]]$IncludedPaths = @("/*"),
    [string[]]$ExcludedPaths = @("/largeBlob/*")
)

$config = [pscustomobject]@{
    databaseName = $DatabaseName
    containerName = $ContainerName
    partitionKey = @{ paths = @($PartitionKeyPath); kind = "Hash" }
    defaultTtl = $DefaultTtl
    indexingPolicy = @{
        indexingMode = "consistent"
        includedPaths = $IncludedPaths | ForEach-Object { @{ path = $_ } }
        excludedPaths = $ExcludedPaths | ForEach-Object { @{ path = $_ } }
    }
}

$config | ConvertTo-Json -Depth 6

That’s useful because it forces the discussion into the open: partition key path, TTL posture, included paths, excluded paths. Once those settings are visible, architects can challenge them.

I also like a basic validation gate that rejects “we forgot to decide” masquerading as a design:

# Validate that an agent draft does not hide risky defaults behind omitted settings.
param(
    [string]$PartitionKeyPath = "/tenantId",
    [string[]]$IncludedPaths = @("/*"),
    [string[]]$CompositeIndexes = @()
)

if ([string]::IsNullOrWhiteSpace($PartitionKeyPath) -or $PartitionKeyPath -eq "/id") {
    throw "Partition key must be explicit and should not default to /id without review."
}
if ($IncludedPaths.Count -eq 0) {
    throw "Indexing paths must be explicit; empty included paths require human sign-off."
}

[pscustomobject]@{
    PartitionKeyReviewed = $true
    IncludedPathCount = $IncludedPaths.Count
    CompositeIndexCount = $CompositeIndexes.Count
    Status = "Ready for human review"
} | Format-List

What should you do after this step? Review the draft against the approved query catalog. If the app filters on fields that the indexing policy ignores, fix it now. If the partition key is just /id because nobody wanted the meeting, stop the rollout.

For broader patterns around agent governance and control loops, the Azure Architecture Center is useful, and the same discipline shows up in my post on Fabric Data Agent Query Governance and Control. Different product surface, same lesson: agent speed is only safe when review boundaries are explicit.

The failure modes hidden by elegant code

The four failure modes I see most often are boring, predictable, and expensive:

  1. hot partitions
  2. RU spikes
  3. over-normalization
  4. elegant code that collapses under real access patterns

Generated sample queries prove almost nothing beyond syntax. They do not prove:

  • workload coverage
  • partition routing quality
  • RU efficiency
  • latency under skew
  • resilience when documents evolve
  • behavior under bursty writes

So test like an adult.

I want load tests that include:

  • skewed tenants
  • burst writes
  • read-heavy windows
  • fan-out style reads the product team swears are “rare”
  • document growth over time
  • new queries that show up after sprint three

Once you have observations, summarize them in a way humans can review outside the agent loop:

# Simulate RU and latency observations so humans can compare candidate models before deployment.
import random
import statistics

def observe(ops):
    rows = []
    for op in ops:
        hot_penalty = 1.8 if op["tenantId"] in {"tenant-00", "tenant-01", "tenant-02", "tenant-03"} else 1.0
        base_ru = 6.0 if op["kind"] == "write" else 2.5
        base_ms = 18 if op["kind"] == "write" else 9
        rows.append({
            "kind": op["kind"],
            "tenantId": op["tenantId"],
            "ru": round(base_ru * hot_penalty * random.uniform(0.9, 1.2), 2),
            "latency_ms": round(base_ms * hot_penalty * random.uniform(0.8, 1.4), 1),
        })
    return rows

ops = [{"kind": "write", "tenantId": "tenant-00"}, {"kind": "read", "tenantId": "tenant-09"}] * 20
rows = observe(ops)
print("avg_ru=", round(statistics.mean(r["ru"] for r in rows), 2))
print("p95_ms=", sorted(r["latency_ms"] for r in rows)[int(len(rows) * 0.95) - 1])

Then rank the hot-tenant signals:

# Summarize hot-tenant signals from observations to support a human go/no-go decision.
from collections import defaultdict

def summarize(rows):
    by_tenant = defaultdict(lambda: {"ru": 0.0, "count": 0, "max_ms": 0.0})
    for r in rows:
        t = by_tenant[r["tenantId"]]
        t["ru"] += r["ru"]
        t["count"] += 1
        t["max_ms"] = max(t["max_ms"], r["latency_ms"])
    ranked = sorted(by_tenant.items(), key=lambda kv: kv[1]["ru"], reverse=True)
    for tenant, stats in ranked[:5]:
        avg_ru = round(stats["ru"] / stats["count"], 2)
        print(f"{tenant}: total_ru={stats['ru']:.1f}, avg_ru={avg_ru}, max_ms={stats['max_ms']}")

sample = [
    {"tenantId": "tenant-00", "ru": 12.1, "latency_ms": 31.0},
    {"tenantId": "tenant-00", "ru": 10.4, "latency_ms": 28.2},
    {"tenantId": "tenant-09", "ru": 2.7, "latency_ms": 8.9},
]
summarize(sample)

And if you want a dead-simple artifact for architecture review or a postmortem, write the observations to CSV and let people inspect them in Excel, Fabric, or whatever they actually use:

# Record observations to CSV so reviewers can inspect cost and latency outside the agent loop.
import csv

rows = [
    {"kind": "write", "tenantId": "tenant-00", "ru": 11.8, "latency_ms": 29.4},
    {"kind": "read", "tenantId": "tenant-09", "ru": 2.6, "latency_ms": 8.7},
]

with open("cosmos_workload_observations.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["kind", "tenantId", "ru", "latency_ms"])
    writer.writeheader()
    writer.writerows(rows)

print("wrote cosmos_workload_observations.csv")

What should you watch for? Tenants with disproportionate RU, ugly max latency, and patterns where the “good” average hides a terrible tail. Average numbers are how weak models sneak into production.

Put the coding agent inside a human review loop

The safe operating model is straightforward.

  • The agent drafts.
  • The data architect sets constraints.
  • The Cosmos DB lead validates operational implications.
  • The application team verifies business behavior.
  • Telemetry decides whether the model was actually right.

That means your input contract to the agent should include:

  • approved access-pattern catalog
  • candidate partition keys
  • consistency requirements
  • indexing constraints
  • retention rules
  • regional expectations
  • load assumptions

And the output contract should require:

  • explicit assumptions
  • supported queries
  • unsupported queries
  • decisions requiring approval
  • known tradeoffs

If the generated output doesn’t state what it does not support, it is not ready for review.

I also want checkpoints at four moments:

  1. before container creation
  2. before index changes
  3. before regional rollout
  4. after representative load testing

That’s not bureaucracy. That’s how you keep AI acceleration from becoming AI-assisted rework.

Use agents to compress iteration, not accountability

My opinion is simple and I’m not moving off it:

The best Cosmos DB teams will not ban AI-generated schema suggestions. They will make those suggestions cheap to test and hard to promote without evidence.

Let the agent generate:

  • entities
  • relationships
  • document examples
  • sample queries
  • config drafts
  • test fixtures

Keep humans accountable for:

  • partition keys
  • consistency tradeoffs
  • indexing strategy
  • multi-region cost posture

That’s the line.

If your team can’t explain how the model behaves under real reads, writes, skew, failures, and growth, then you do not have a design yet. You have a draft with good formatting.

Treat every proposed Cosmos DB model as a workload hypothesis. Let the agent help you create and revise that hypothesis faster. But don’t hand it ownership of the decisions that determine whether the system survives production.

Which claim would you push back on hardest: keeping partition-key selection fully human-owned, or forcing every agent-generated model through skewed workload testing before approval?

#CosmosDB #AIAgents #DataArchitecture


Sources & References

  1. Azure Architecture Center - Azure Architecture Center
  2. Azure Cosmos DB documentation - Azure Cosmos DB
  3. Cloud Adoption Framework for Microsoft - Cloud Adoption Framework
  4. Fabric data agent creation - Microsoft Fabric
  5. Data API builder documentation - Data API builder

Try it yourself

Run this tutorial as a Jupyter notebook: Download runbook.ipynb (24 cells, 18 KB).

Link copied