Data Modeling and Partitioning in Azure Cosmos DB

Posted in
CosmosDB_Partitioning_DataModel

For many database developers, the first experience with Azure Cosmos DB can feel both exciting and uncomfortable.

Exciting because Cosmos DB gives us a globally distributed, highly scalable, low-latency database platform.

Uncomfortable because it forces us to rethink habits we learned from years of relational database design.

If you come from SQL Server, Oracle, PostgreSQL, or another relational database engine, your instincts are probably well trained around normalization, foreign keys, joins, relational constraints, and designing tables around entities. Those instincts are valuable, but they are not always the right starting point when designing for Azure Cosmos DB.

One of the most important lessons in Cosmos DB is this:

You do not start by asking, “What are my tables?”
You start by asking, “How will my application read and write the data?”

That shift changes everything.

A great Microsoft session titled Data Modeling and Partitioning in Azure Cosmos DB by Leonard Lobel explains this beautifully. The session walks through the real-world challenge that many developers face when moving from a relational mindset to a document database mindset: how to structure data, how to choose containers, when to denormalize, when to combine multiple entity types, and how partitioning affects performance, scalability, and cost.

This topic is not optional knowledge for Cosmos DB developers. It is foundational.

If you get the data model and partitioning strategy wrong, Cosmos DB can become expensive, inefficient, and frustrating. If you get them right, Cosmos DB can deliver tremendous performance and scale.

Let’s break down the major lessons:

Cosmos DB Is Not Just a Place to Store JSON

A common mistake is to think of Azure Cosmos DB as simply “SQL Server, but with JSON documents.”

That is the wrong mental model.

Cosmos DB is a distributed database designed for scale. It stores JSON documents, but the real power comes from how those documents are grouped, partitioned, indexed, queried, replicated, and charged.

In a relational database, we often design around normalized tables:

  • Customers
  • Orders
  • Order Details
  • Products
  • Addresses
  • Categories

Each concept gets its own table. Relationships are represented through foreign keys. Queries reconstruct the business object using joins.

In Cosmos DB, that same approach can become painful.

Why?

Because distributed databases behave differently. Joins across large distributed datasets are not the same as joins inside a relational engine. Network boundaries, partitions, request units, and query fan-out matter. The data model must be designed with the distributed nature of the platform in mind.

That means Cosmos DB modeling is less about theoretical normalization and more about practical application behavior.

The Most Important Question: What Are Your Access Patterns?

In relational design, we often begin with the entity relationship diagram. In Cosmos DB, we begin with access patterns.

Before creating containers and partition keys, we need to ask questions like:

  • What does the application need to retrieve most often?
  • Which data is always read together?
  • Which data changes together?
  • Which queries must be extremely fast?
  • Which writes are frequent?
  • Which entities grow without limit?
  • Which operations must be transactional?
  • Which fields are used to filter data?
  • Which fields are used to route requests to a partition?

These questions drive the model.

For example, imagine an e-commerce system. In a relational database, an order may be stored across several tables: Order Header, Order Details, Customer, Address, Product, and Payment. But in Cosmos DB, if the application typically retrieves an entire order with its line items, shipping address, and order status, it may make more sense to store that order as a single document.

Instead of reconstructing the order through joins, you retrieve one document.

That is a huge mindset shift.

The goal is not to eliminate all relationships. The goal is to model data so the most important application operations are efficient, predictable, and scalable.

Embedding vs. Referencing: The Core Modeling Decision

One of the biggest Cosmos DB design decisions is whether to embed related data inside the same document or reference it as a separate document.

Embed data when it is naturally contained

Embedding works well when related data is owned by the parent and usually read together.

For example, an order and its order lines are a strong candidate for embedding:

{
  "id": "order-1001",
  "type": "order",
  "customerId": "customer-42",
  "orderDate": "2026-06-03",
  "status": "Shipped",
  "items": [
    {
      "productId": "product-10",
      "name": "Mountain Bike",
      "quantity": 1,
      "unitPrice": 1200
    },
    {
      "productId": "product-20",
      "name": "Helmet",
      "quantity": 1,
      "unitPrice": 75
    }
  ],
  "shippingAddress": {
    "street": "123 Main Street",
    "city": "Orlando",
    "state": "FL",
    "postalCode": "32801"
  }
}

This model is powerful because the application can retrieve the full order in one read operation.

No joins.

No multi-step lookup.

No extra round trips.

This is one of the major advantages of document modeling.

Reference data when it is large, shared, or independently updated

Embedding is not always the right answer.

If a related entity is large, reused by many documents, or updated independently, referencing may be better.

Product catalog data is a good example. A product may appear in thousands of orders. You probably do not want every product update to require updating thousands of historical order documents.

So you might store product details separately and only copy the product information needed at the time of the order.

For example, the order line may store:

{
  "productId": "product-10",
  "name": "Mountain Bike",
  "quantity": 1,
  "unitPrice": 1200
}

That preserves the historical truth of the order while avoiding the need to join to the current product record every time the order is displayed.

This is an important Cosmos DB principle:

Denormalization is not duplication for the sake of duplication. It is duplication for performance, scalability, and read efficiency.

Denormalization Is Not a Dirty Word

Relational developers are often taught to avoid duplication. In normalized relational design, duplication can create update anomalies and data integrity issues.

But in Cosmos DB, strategic duplication is often the correct design.

Why?

Because distributed databases reward locality.

If the data needed by a screen, API call, or business operation is stored together, the application can retrieve it efficiently. If that data is scattered across containers and partitions, the application may pay more in latency, request units, and complexity.

Denormalization is not about being careless. It is about being intentional.

A good Cosmos DB model may store the same data in multiple shapes for different access patterns. For example:

  • A customer document optimized for customer profile reads
  • An order document optimized for order history
  • A product document optimized for catalog browsing
  • A read-optimized projection optimized for a dashboard or search screen

This is very different from relational modeling, where we often try to maintain one canonical normalized structure and derive everything else through queries.

In Cosmos DB, it is common to design documents that directly serve the application.

Containers Are Not Tables

Another common mistake is to map every relational table to a Cosmos DB container.

Customers table becomes Customers container.

Orders table becomes Orders container.

Products table becomes Products container.

OrderDetails table becomes OrderDetails container.

That may look familiar, but it is often not optimal.

A Cosmos DB container is not simply the equivalent of a table. A container is a scalability and partitioning boundary. It has a partition key. It has throughput. It stores documents. It may contain multiple document types. It can scale horizontally across partitions.

That means we should not automatically create one container per entity type.

Sometimes it makes sense to store multiple entity types in the same container, especially when they share a partition key and are queried together.

For example, in a customer-centric system, you might store customer profile documents, order documents, support ticket documents, and address documents in the same container using a shared partition key such as customerId.

{
  "id": "customer-42",
  "type": "customer",
  "customerId": "customer-42",
  "name": "Contoso Bikes"
}
{
  "id": "order-1001",
  "type": "order",
  "customerId": "customer-42",
  "orderDate": "2026-06-03"
}
{
  "id": "ticket-9001",
  "type": "supportTicket",
  "customerId": "customer-42",
  "status": "Open"
}

With this design, related data for a customer can be colocated in the same logical partition.

That can make customer-centric queries efficient.

But this design only works if it matches the application’s access patterns.

The lesson is simple:

Do not design containers by copying relational tables. Design containers around workload, partitioning, and query patterns.

Partitioning Is the Heart of Cosmos DB Design

Partitioning is one of the most important Cosmos DB concepts.

Cosmos DB scales by distributing data across partitions. The partition key determines how data is logically grouped and how requests are routed.

When you create a container, you choose a partition key path, such as:

/customerId

or

/tenantId

or

/categoryId

Each document has a partition key value. Documents with the same partition key value belong to the same logical partition.

For example, if the partition key is /customerId, all documents with customerId = customer-42 are grouped into the same logical partition.

That matters because efficient Cosmos DB design depends heavily on routing operations to the right partition.

A good partition key helps Cosmos DB distribute data and workload evenly.

A bad partition key creates hot partitions, expensive queries, and scalability bottlenecks.

The Partition Key Is Not Just a Technical Setting

The partition key is a business design decision.

It affects:

  • How data is distributed
  • How queries are routed
  • How throughput is consumed
  • How scalable the workload becomes
  • How transactional operations are scoped
  • How expensive queries may be
  • Whether the application can grow cleanly over time

This is why choosing a partition key should never be treated as a minor setup step.

It is one of the most important architecture decisions in Cosmos DB.

A good partition key usually has three qualities:

1. High cardinality

The key should have many possible values.

For example, customerId, tenantId, deviceId, or userId may provide many distinct values.

A low-cardinality key like status is usually a poor choice because there may only be a few values such as Pending, Active, Closed, or Cancelled. That can concentrate too much data and traffic into a small number of partitions.

2. Even distribution

The key should distribute data and requests evenly.

High cardinality alone is not enough. If one customer, tenant, or device receives most of the traffic, that partition can still become hot.

For example, in a multi-tenant system, tenantId may look like a good partition key. But if one tenant is responsible for 80% of the workload, the design may create a hot partition.

3. Query alignment

The key should align with common query patterns.

If most queries filter by customer, then /customerId may be a strong candidate.

If most queries filter by tenant and user, then a hierarchical or synthetic partition strategy may be better.

The key point is that partitioning and querying cannot be designed separately. They must be designed together.

Request Units: The Currency of Cosmos DB

Cosmos DB uses Request Units, or RUs, to measure the cost of operations.

Reads cost RUs.

Writes cost RUs.

Queries cost RUs.

Stored procedures cost RUs.

Indexing impacts RUs.

The more work Cosmos DB has to do, the more RUs are consumed.

This is why data modeling matters so much. A poor model can turn a simple application operation into an expensive cross-partition query. A good model can make the same operation efficient and predictable.

For example:

  • Reading one document by id and partition key is usually very efficient.
  • Querying across many partitions is more expensive.
  • Returning large documents costs more than returning small documents.
  • Complex queries cost more than point reads.
  • Indexing every property may increase write costs.

This is why Cosmos DB design must consider both performance and cost.

In relational databases, we often focus heavily on query plans and indexes. In Cosmos DB, we must also focus on how many RUs each operation consumes and whether the model supports efficient partition routing.

Point Reads Are Gold

One of the best operations in Cosmos DB is the point read.

A point read retrieves a document by its id and partition key.

That is the most direct path to the data.

For example:

id = order-1001
partition key = customer-42

When the application knows both values, Cosmos DB can route the request directly to the correct logical partition and retrieve the document efficiently.

This is why many Cosmos DB designs aim to make the most common operations point reads or single-partition queries.

The more your application can avoid broad cross-partition scans, the better your performance and cost profile will usually be.

Cross-Partition Queries Are Not Evil, But They Must Be Understood

A cross-partition query is a query that may need to search across multiple partition key values.

Sometimes cross-partition queries are necessary. Cosmos DB supports them. But developers must understand their cost.

A query that touches many partitions may consume more RUs and have higher latency than a query targeted to one partition.

For example, this query may be efficient if customerId is the partition key:

SELECT * FROM c
WHERE c.customerId = "customer-42"

But this query may fan out across many partitions:

SELECT * FROM c
WHERE c.status = "Pending"

If the container is partitioned by customerId and status is not part of the partition strategy, Cosmos DB may need to search many partitions to find matching documents.

That does not mean the query is forbidden. It means you must understand the tradeoff.

For reporting, analytics, dashboards, and broad search scenarios, you may need a separate read model, materialized view, analytical store, or different container design.

Transaction Boundaries Matter

Cosmos DB supports transactional operations within a logical partition.

That makes partition-key design even more important.

If multiple documents need to be updated transactionally, they should usually share the same partition key value.

For example, if customer profile updates, customer preferences, and customer audit events must participate in operations scoped to one customer, partitioning by customerId may be useful.

But if related documents are scattered across different partition key values, you may lose the ability to perform simple transactional operations across them.

This is another reason why the partition key is not just a storage decision. It is a consistency and transaction design decision.

The Danger of Hot Partitions

A hot partition happens when too much traffic targets one partition key value or a small subset of partition key values.

For example, imagine a container partitioned by tenantId.

That sounds reasonable for a SaaS application. But if one tenant is much larger than the others, that tenant may dominate the workload.

The result can be throttling, uneven RU consumption, and poor scalability.

This is why partition-key selection must consider not just the number of values, but the distribution of reads and writes across those values.

Ask yourself:

  • Will one customer become huge?
  • Will one tenant dominate traffic?
  • Will recent dates receive most writes?
  • Will one category get most queries?
  • Will one status value contain most records?
  • Will one partition key value grow without limit?

A partition key that looks good during development may fail under production workload.

Synthetic and Hierarchical Partition Keys

Sometimes no single property is good enough as a partition key.

In that case, we may need a synthetic partition key.

A synthetic partition key combines multiple values into one partition key value.

For example:

tenantId|customerId

or

region|tenantId

or

customerId|orderYear

This can help distribute data more evenly while still supporting important query patterns.

Azure Cosmos DB also supports hierarchical partition keys, which allow multiple levels in the partitioning strategy. This can be especially useful in multi-tenant systems where the first level may be tenantId, but additional levels help distribute large tenants more effectively.

The goal is always the same:

Distribute data and workload evenly while preserving efficient query routing.

Common Mistakes Developers Make

Mistake 1: Creating one container per table

This often recreates a relational model without gaining the benefits of document modeling.

Mistake 2: Avoiding denormalization

Trying to keep everything normalized can create excessive queries, joins, and round trips.

Mistake 3: Choosing a low-cardinality partition key

Keys like status, type, region, or category may not distribute data well enough by themselves.

Mistake 4: Ignoring access patterns

A beautiful theoretical model can perform terribly if it does not match how the application actually uses data.

Mistake 5: Forgetting about RU cost

Every design decision affects cost. Query shape, document size, indexing, partitioning, and consistency all matter.

Mistake 6: Designing only for today’s data volume

A partition key may work with 10,000 records but fail with 100 million records.

Mistake 7: Assuming Cosmos DB works like a relational database

Cosmos DB is a distributed NoSQL database. It requires a distributed systems mindset.

The Big Lesson for Database Developers

The most important lesson from this topic is that Cosmos DB data modeling and partitioning are inseparable.

You cannot design the document model first and pick the partition key later as an afterthought.

You cannot choose the partition key without understanding queries.

You cannot estimate cost without understanding RUs.

You cannot optimize performance without understanding how data is distributed.

Everything is connected:

  • Data model
  • Partition key
  • Query patterns
  • RU consumption
  • Transaction scope
  • Scalability
  • Cost
  • Application behavior

This is why Cosmos DB design is both an art and an engineering discipline.

Lino’s 2 Cents:

Azure Cosmos DB is an incredible platform, but it rewards developers who design intentionally.

The best Cosmos DB solutions do not come from copying relational schemas into JSON documents. They come from understanding how the application works, how users access data, how data grows, and how Cosmos DB distributes workload across partitions.

For database developers, this is a critical shift.

Relational modeling teaches us to protect data integrity through normalization.

Cosmos DB modeling teaches us to protect performance and scalability through access-pattern-driven design, denormalization, and intelligent partitioning.

Both skills matter.

But when building with Cosmos DB, the winning design is the one that serves the workload efficiently.

So before creating your next Cosmos DB container, pause and ask:

What will my application actually do with this data?

That question is the beginning of good Cosmos DB design.

The Training Boss stands ready to engage with your organization to reach the best architecture for your CosmosDB database.  Reach out today

Leave a Reply

Your email address will not be published. Required fields are marked *

Are you human? Please solve:Captcha