Recommend a Solution for Storing Semi-Structured Data — Lesson
AZ-305 › Unit 2 › Design data storage for semi-structured and unstructured data › Recommend a solution for storing semi-structured data
Recommend a Solution for Storing Semi-Structured Data — Lesson
Semi-structured data is the lifeblood of modern cloud applications — JSON product catalogs, IoT telemetry, user profiles, social graphs, and event streams that don't fit neatly into rows and columns but aren't truly amorphous either. On the AZ-305 exam, candidates are asked to choose the right Azure data service for these workloads, configure it correctly, and reason about the cost and performance trade-offs. This lesson centers on Azure Cosmos DB, the platform Microsoft positions as its globally distributed, multi-model database for semi-structured workloads, and walks through the design decisions an architect must make: API selection, partition key design, consistency level, throughput model, and time-to-live (TTL).
By the end of this lesson, you should be able to read a scenario describing a workload and confidently recommend the API, partition strategy, consistency level, and throughput configuration that meets the SLA at the lowest reasonable cost. We use the Well-Architected Framework's pillars — Reliability, Performance Efficiency, and Cost Optimization — to anchor every recommendation.
Why This Matters
Imagine you are the lead architect for Contoso Retail. Your application team has rebuilt the product catalog as a JSON document store and is now seeing requests per second at peak. Their initial choice — a single SQL Server instance — is being throttled to death. The CTO asks: "What should we move to, and how do we make sure it scales globally without rewriting the app?" The right answer is rarely "just pick the cheapest option" or "use the same database we use for transactions." It is a deliberate design decision that hinges on data shape, access pattern, consistency tolerance, and budget. AZ-305 tests exactly this judgment, and getting it wrong in the real world means 99.9% availability targets missed, runaway bills, or applications that can't grow into new regions.
Prerequisites
- Relational vs NoSQL fundamentals — Self-check: Can you explain why normalized SQL schemas struggle with deeply nested JSON?
- Azure resource hierarchy (subscriptions, resource groups, regions) — Self-check: Why does choosing a write region matter for latency?
- HTTP/REST basics and JSON document structure — Self-check: What is the difference between a document and a row?
- CAP theorem intuition (consistency, availability, partition tolerance) — Self-check: Which two does a globally distributed database typically prioritize, and what's the cost?
- Azure cost basics — RU/s, vCore, DTUs — Self-check: What is a Request Unit and why does Cosmos DB price on it?
Learning Objectives
By the end of this lesson, you will be able to:
- Evaluate workload requirements (data shape, access pattern, geographic distribution) and select the appropriate
Cosmos DBAPI amongSQL,MongoDB,Cassandra,Gremlin, andTable. - Design a partition key that balances storage and throughput while supporting hot query paths.
- Recommend one of the five Cosmos DB consistency levels by analyzing the application's tolerance for stale reads against latency and cost.
- Differentiate autoscale from manual provisioned throughput and calculate which is cheaper for a given utilization profile.
- Apply time-to-live (TTL) policies to control data lifecycle costs in semi-structured workloads.
- Compare Cosmos DB to alternative semi-structured stores such as
Azure Table StorageandAzure SQL Databasewith JSON, and justify the trade-off.
Building Blocks
Semi-structured data — Analogy: a stack of business cards. Each has fields (name, title, phone), but no two cards have exactly the same set, and some have stickers, doodles, or QR codes. Formal definition: data with self-describing tags or markers (commonly JSON, XML, or BSON) that enforces no fixed schema across records. Why it matters: the storage engine must handle heterogeneous shapes without requiring an ALTER TABLE for every new attribute.
Azure Cosmos DB — Analogy: a global vending-machine network where every location stocks the same catalog and accepts both dollars and yen. Formal definition: a fully managed, multi-model NoSQL database with turnkey global distribution, 99.999% availability SLA for multi-region writes, and single-digit-millisecond latency at the percentile. Why it matters: it is Microsoft's strategic answer for semi-structured workloads at planetary scale.
Request Unit (RU) — Analogy: a postage stamp where heavier letters need more stamps. Formal definition: a normalized currency for database operations that combines CPU, memory, and IOPS; a 1 KB point read costs 1 RU. Why it matters: every operation, every billing line, and every throttling decision is expressed in RUs.
Partition key — Analogy: the postcode on a parcel — it determines which warehouse stores and ships it. Formal definition: the property whose hash is used to deterministically place a document into one of many physical partitions. Why it matters: a poorly chosen key creates hot partitions that throttle while others sit idle.
Consistency level — Analogy: how many newspapers must report a story before you believe it. Formal definition: the contract between writes and reads about how stale a read may be when data is replicated across regions or replicas. Why it matters: stronger consistency increases read latency and RU cost; the right level depends on user-visible correctness needs.
Throughput (provisioned vs autoscale vs serverless) — Analogy: paying for a fixed gym membership, a "gym pass" that scales to your visits, or per-class drop-in fees. Formal definition: provisioned throughput reserves a flat RU/s capacity; autoscale provisions a max and bills for the higher of 10% of max or actual usage; serverless bills per RU consumed with no reservation. Why it matters: throughput model is usually the single largest cost lever in a Cosmos DB bill.
Time-to-Live (TTL) — Analogy: a milk carton expiry date — anything past it is removed. Formal definition: a per-item or per-container property that triggers asynchronous deletion after the specified seconds. Why it matters: cheap automatic data lifecycle management without an external job.
Deep Dive
Choosing the right Cosmos DB API
Azure Cosmos DB exposes five APIs that share the same underlying engine but expose different programming surfaces and indexing rules. The API is set at account creation and cannot be changed, so this decision deserves architectural rigor.
| API | Wire compatibility | Best for | Notes |
|---|---|---|---|
SQL (Core) | Native | Greenfield JSON workloads | All features land here first |
MongoDB | MongoDB driver $4.0/$4.2 | Migrating Mongo apps | Verify version coverage |
Cassandra | CQL v4 | Migrating Cassandra apps | Wire-compatible |
Gremlin | Apache TinkerPop | Graph traversals | Vertices and edges |
Table | Azure Table | Migrating Table Storage | Premium variant |
The Core (SQL) API is the native, recommended option for greenfield work. It speaks SQL-like queries against JSON documents, supports the richest set of features (change feed, JavaScript stored procedures, the latest consistency tooling), and is the best-documented surface for new applications. Choose it whenever there is no migration constraint pulling you elsewhere.
The MongoDB API is a wire-protocol-compatible facade that lets existing MongoDB drivers and tools point at Cosmos DB unchanged. It is the right choice when migrating an on-premises or self-managed MongoDB cluster and you want to preserve the application code. It does not implement every MongoDB feature — verify version coverage in the documentation.
The Cassandra API similarly mimics CQL for teams running Cassandra workloads who want to drop the operational burden. It is wire-compatible with CQL v4. Pick it when you have a Cassandra application and want managed scaling without rewriting the data access layer.
The Gremlin API exposes a graph database — vertices and edges traversed with the Apache TinkerPop Gremlin query language. Use it for fraud rings, recommendation engines, social graphs, identity-and-access graphs, or anything where the relationships are the queries.
The Table API is the modern successor to Azure Table Storage, offering the same key-value-with-properties model but with global distribution, single-digit-ms reads, secondary indexes, and the full Cosmos DB SLA. Choose it for migrations from Table Storage when you need premium performance or geo-replication.
[!TIP] When in doubt for a new application, choose the
SQL API. Every new Cosmos DB feature lands there first, and it has the most idiomatic Azure SDK across .NET, Java, Python, and Node.js.
Partition key design
A partition key is the most consequential schema decision in Cosmos DB. The engine hashes the key value to assign each document to one of logical partitions, each capped at 20 GB and RU/s. A bad key causes hot partitions (too many writes or reads concentrate on one partition, hitting the RU/s ceiling and triggering 429 Too Many Requests even though aggregate utilization is low), storage skew (a partition exceeds 20 GB and writes start failing with RequestEntityTooLarge), and cross-partition fanout (queries that don't filter on the partition key must hit every partition, multiplying RU cost and latency).
A good key has high cardinality (millions of distinct values), uniform distribution (no single value dominates), and aligns with the most common query filter. For an IoT telemetry store, deviceId works when you have millions of devices each emitting roughly the same volume; for a multi-tenant SaaS app, tenantId is appealing but dangerous if one tenant is much larger than the rest. In that case, consider a synthetic key like tenantId_yyyymm (a value such as "contoso_202604"), which redistributes a large tenant across months.
[!WARNING] A logical partition is a hard size limit. Once you create a container with a partition key, you cannot change it without exporting and re-importing the data. Design carefully on day one — partition-key remediation is the single most expensive Cosmos DB operation in production.
Consistency levels
Cosmos DB offers five consistency levels arranged on a spectrum from strongest (and most expensive) to weakest:
| Level | Read view | Latency cost | Typical use |
|---|---|---|---|
Strong | Linearizable across regions | Highest; multi-region writes disabled | Financial ledgers, inventory checks |
Bounded Staleness | Bounded by versions or seconds | High; configurable lag window | Group chat, online auctions |
Session (default) | Read-your-own-writes per session token | Low | User profiles, shopping carts |
Consistent Prefix | Reads never see out-of-order writes | Very low | Status feeds, social timelines |
Eventual | Any order, eventually converges | Lowest | View counts, telemetry rollups |
The default — Session — is the right answer for most user-facing apps because it gives the illusion of strong consistency to the user who wrote without paying the cost of cross-region linearizability. The exam often tests the difference between Session and Eventual: choose Eventual only when the user genuinely cannot detect stale reads (analytics dashboards, like counts).
[!IMPORTANT] Reading at a weaker consistency level than the account default costs roughly half the RUs of a
StrongorBounded Stalenessread. This is a common cost-tuning lever — keep the account atSessionand downgrade per-request only where staleness is acceptable.
Throughput models — manual, autoscale, serverless
Provisioned throughput models map to predictable workload shapes:
| Model | Floor | Best for | Cost surprise |
|---|---|---|---|
Manual provisioned | 400 RU/s flat | Steady utilization | Pays full RU even at idle |
Autoscale | 10% of max | Variable, 20–65% duty cycle | Bills hourly peak |
Serverless | 0 | Dev/test, infrequent admin | Capped at RU/s and 1 TB |
The break-even between autoscale and manual is roughly 65% utilization. Above it, manual is cheaper because you pay the same flat rate but use more of it; below it, autoscale wins because the floor is only 10% of the max. Serverless is the right answer for dev/test, infrequent admin tools, and workloads with a duty cycle, but its RU/s cap rules it out for any real production traffic.
# Switch a container from manual to autoscale (Core SQL API)
az cosmosdb sql container throughput migrate \
--account-name contoso-cdb-prod \
--database-name catalog \
--name products \
--resource-group rg-cosmos-prod \
--throughput-type autoscaleresource cosmosContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = {
name: 'orders'
parent: catalogDb
properties: {
resource: {
id: 'orders'
partitionKey: {
paths: ['/tenantIdMonth']
kind: 'Hash'
}
defaultTtl: 7776000
}
options: {
autoscaleSettings: {
maxThroughput: 4000
}
}
}
}TTL — automatic data lifecycle
Setting DefaultTimeToLive on a container instructs Cosmos DB to delete documents asynchronously after a configurable interval. The key behaviors are: null or absent means TTL is disabled and documents live forever; -1 means TTL is enabled but not enforced at the container level so per-document ttl properties take effect; a positive integer (seconds) means all documents expire after that many seconds since their last update unless they specify their own ttl.
TTL deletions are background operations and consume RUs from the same budget as foreground traffic. Plan for the spike when a large cohort of documents simultaneously hits expiry — many architects either smear writes across time or temporarily raise the autoscale max during anticipated purge windows.
{
"id": "alert-7f3",
"deviceId": "thermostat-44",
"temperature": 72.4,
"ttl": 2592000
}Worked Examples
Easy — choose the API. Synthetic scenario (AZ-305 style): Contoso has a five-year-old MongoDB application running on three VMs in their datacenter. Operations is tired of patching and wants Azure-managed. They have 200 developers using the Mongo Node.js driver. Recommend an Azure data service.
Step 1: Identify the constraint — a large existing codebase tied to MongoDB drivers. Step 2: Eliminate options that require code changes (rules out SQL API). Step 3: Among Cosmos DB APIs, only MongoDB API is wire-compatible. Step 4: Verify the version Cosmos supports covers their cluster's version (e.g., $4.2 or $4.0). Choose: Cosmos DB for MongoDB.
[!NOTE] The exam loves the phrase "minimize code changes." That phrase nearly always points to a compatibility-layer API such as
MongoDB,Cassandra, orTable.
Medium — partition key. Synthetic scenario (AZ-305 style): A B2B SaaS workspace tool stores user activity. Tenants range from 5 users (small accountancy firm) to users (a Fortune 500 enterprise). Reads are nearly always scoped to one tenant. Writes are uniformly distributed across users.
Step 1: Tenant-scoped reads suggest tenantId as a partition key — but the size skew is extreme. Step 2: A single-property tenantId would put the F500 tenant on one partition, violating the 20 GB cap. Step 3: Synthesize a key — tenantId_userId if every read knows the user, or tenantId_yyyymm if reads are time-bounded. Step 4: Choose tenantId_userId because activity reads typically scope to one user. Choose: synthetic partition key tenantId_userId with cross-partition queries reserved for tenant-wide audits.
Hard — throughput and consistency together. Synthetic scenario (AZ-305 style): A multiplayer online game stores player session state (1 KB JSON, read 5 times for every write). Peak traffic is reads/s and writes/s for 4 hours each evening; off-peak is 1% of that. Players in the same match must see each other's moves with no detectable lag, but cross-region replication lag of a few seconds during region failover is acceptable.
Step 1: Compute peak RUs. Reads cost about 1 RU each at Session consistency, so reads alone consume 50,000 RU/s. Writes cost about 5 RU each, so writes consume 50,000 RU/s. Total peak about 100,000 RU/s. Step 2: Off-peak duty cycle = 4 / 24 ≈ 17%. Average utilization at flat-provisioned 100,000 RU/s would be about 18% — well below the 65% break-even for manual. Step 3: Choose autoscale with max 100,000 RU/s. Step 4: Consistency — same-match players need read-your-writes within a region, which Session covers. Cross-region lag during failover is acceptable, so Bounded Staleness would be overkill. Choose: autoscale up to 100,000 RU/s, Session consistency, partition by matchId.
Visual Explanations
Visual 1 — API decision tree (Mermaid). The chart below walks through the API recommendation logic.
Caption: API selection flowchart for Cosmos DB. The decision is one-shot at account creation, so the choice must be made carefully.
Visual 2 — Request flow and RU billing (Mermaid).
Caption: Every request flows through partitioning and the RU budget — both inputs to throttling decisions.
Visual 3 — Partition layout (TikZ).
Caption: Each logical partition is bounded — the partition key choice fans documents across them.
Visual 4 — Consistency level comparison (table).
| Level | Stale reads possible? | Multi-region writes? | RU cost (read) | Typical pick |
|---|---|---|---|---|
Strong | No | No | High | Financial ledger |
Bounded Staleness | Bounded by or | Yes | Medium-high | Group collaboration |
Session | Only outside session | Yes | Low | User-facing CRUD |
Consistent Prefix | Yes, in-order | Yes | Low | Social timelines |
Eventual | Yes, any order | Yes | Lowest | Telemetry rollup |
Caption: The five Cosmos DB consistency levels. Session is the default and right answer for most user-facing apps.
Visual 5 — Azure semi-structured store comparison (table).
| Service | Best for | Max scale | Consistency | Cost driver |
|---|---|---|---|---|
Cosmos DB SQL API | Global, multi-model JSON | Effectively unlimited | 5 levels | RU/s + storage |
Azure Table Storage | Cheap key-value at scale | ops per account | Strong (single region) | Storage + tx |
Azure SQL DB with JSON | Mixed relational + JSON | 80 vCores | Strong | vCore + storage |
Azure Database for PostgreSQL (JSONB) | Hybrid relational/document | 96 vCores | Strong | vCore + storage |
Azure Managed Redis | Sub-ms hot data | $1.2M ops/s (Enterprise) | Eventual | Tier + memory |
Caption: Cosmos DB is rarely the only viable option — but it usually wins when global writes or schema flexibility are non-negotiable.
Common Mistakes
❌ Myth: "We can always change the partition key later if we get it wrong." ✅ Reality: The partition key is immutable on a Cosmos DB container. Changing it requires exporting all data, creating a new container, and re-ingesting. Why it's tricky: Other Azure services such as
Azure SQL Databaselet you re-shard with online operations, which leads engineers to assume Cosmos DB is similar.
❌ Myth: "
Strongconsistency is always the safest choice." ✅ Reality:Strongdisables multi-region writes and roughly doubles RU cost per read. For most user-facing flows,Sessionis correct. Why it's tricky: "Strong" sounds reassuring, and engineers who learned ACID-first instinctively reach for it.
❌ Myth: "Autoscale is always cheaper because we don't pay for idle." ✅ Reality: Autoscale bills at 10% of the max even when idle and breaks even with manual at roughly 65% utilization. A flat workload at 80% utilization is cheaper on manual. Why it's tricky: The "scales with usage" framing makes it sound free during quiet periods, but the floor charge is real.
❌ Myth: "Cosmos DB Free Tier covers production." ✅ Reality: Free Tier provides RU/s and 25 GB per account — enough for one prototype, not a real workload. Why it's tricky: Engineers who succeed in dev forget that real traffic blows past the cap immediately.
Practice Exercises
🟢 Exercise 1 — cheapest store for a tiny workload. A startup has JSON documents totalling 300 MB, queried about 10 times per day. The team wants the cheapest Azure option. Recommend a service and justify.
▶💡 Hint
Look at duty cycle. With only 10 queries per day, no provisioned reservation is justifiable.
▶✅ Solution
Cosmos DB serverless is ideal — you pay per RU, no minimum reservation, and the data fits comfortably in a single serverless container (1 TB cap). Alternatively, Azure Table Storage is even cheaper for pure key-value access, but only Cosmos DB serverless gives you the rich query surface.
🟡 Exercise 2 — partition key for hospitality. A hospitality brand stores reservation documents. Reads filter by propertyId and reservationDate. Writes are dominated by today's check-ins for the same property. What partition key minimizes hot partitions?
▶💡 Hint
A single property checking in many guests at once will hammer one logical partition. Spread the load.
▶✅ Solution
Use a synthetic key like propertyId_yyyymmdd so today's check-in storm spreads across many partitions over the year and no single property exceeds the 20 GB or RU cap. Most queries already filter by reservationDate, so the synthetic component is naturally available at query time.
🟡 Exercise 3 — consistency for a trading dashboard. A trading dashboard shows the price of one ticker. Brokers must never see a stale price across the same session, but cross-region delays during a regional failover are acceptable. Which consistency level?
▶💡 Hint
"Within a session" is the giveaway phrase.
▶✅ Solution
Session consistency. It gives read-your-own-writes within the broker's session via the session token while leaving multi-region replication eventually consistent.
🔴 Exercise 4 — fixing a hot tenant. A multi-tenant SaaS has tenants. Tenant A is 80% of all traffic. The team picked tenantId as the partition key six months ago. They are now seeing 429s under load. Recommend short-term and long-term remediations.
▶💡 Hint
Short term you cannot change the partition key. Long term you must.
▶✅ Solution
Short term: increase RU/s on the container (manual or raise the autoscale max), enable larger logical partition sizes (the 100 GB hierarchical-partition option is also worth evaluating), and add caching with Azure Managed Redis for the hottest reads. Long term: create a new container with a synthetic key like tenantId_yyyymm and migrate via the change feed; cut over once the new container is hot.
🔴 Exercise 5 — autoscale vs manual. Compute the monthly cost difference between manual at 50,000 RU/s and autoscale at max 100,000 RU/s for a workload that runs at 20% duty cycle. Use per 100 RU/s/hour for manual and per 100 RU/s/hour (peak) for autoscale.
▶💡 Hint
Autoscale bills at each hour.
▶✅ Solution
Manual: per month. Autoscale floor: $0. RU/s; in busy hours peak hits 50,000 RU/s. Approximate average billed RURU/s. Cost $1{,}577$ per month. Autoscale is roughly 46% cheaper here.
🟡 Exercise 6 — Table API vs Table Storage. Choose between Cosmos DB for Table and Azure Table Storage for a workload of 5 TB key-value data with single-digit-ms latency requirements and global reads from three continents.
▶💡 Hint
Look at the global distribution requirement.
▶✅ Solution
Cosmos DB for Table — Azure Table Storage is single-region with no built-in geo-replication of writes. Cosmos DB Table API offers turnkey global distribution and the SLA on latency.
🔴 Exercise 7 — IoT TTL strategy. An IoT platform ingests 1 million telemetry messages per minute. After 90 days, raw telemetry is no longer queried. How do you minimise storage cost without rewriting the ingestion code?
▶💡 Hint
Cosmos DB has a built-in lifecycle feature.
▶✅ Solution
Set DefaultTimeToLive = 7776000 (90 days in seconds) on the telemetry container. Documents older than 90 days are deleted automatically by background processes — no app code change needed. Run an aggregation job before expiry to land summaries in a long-term store like Azure Data Lake Storage Gen2.
Summary & Concept Map
Cosmos DBis the strategic Azure platform for semi-structured workloads at global scale — choose it when schema flexibility, multi-region writes, or guaranteed low latency are required.- The API (
SQL,MongoDB,Cassandra,Gremlin,Table) is set at account creation and immutable; pickSQLfor greenfield work and a wire-compatible API for migrations. - A good partition key has high cardinality, even distribution, and aligns with the dominant query filter; synthesise a composite key when one tenant or value dominates.
- The five consistency levels trade staleness for cost and latency;
Sessionis the default and right answer for most user-facing apps. - Throughput models pivot at $$\sim 65 utilization: above it use manual provisioned, below it use autoscale, and use serverless for very spiky or low-duty-cycle workloads.
- TTL is the cheapest, lowest-effort lifecycle tool — set it once on the container or per document to retire stale data without writing custom jobs.