Aggregation Persistence with Change Feed
Complete the full lesson to earn 25 points — 50 with Pro
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks, the wait, and see fewer ads — read each lesson on a single page with Pro
Aggregation Persistence with Azure Cosmos DB Change Feed
Introduction: The Power of Event-Driven Aggregations
In modern distributed systems, the way we handle data has shifted from simple request-response patterns to event-driven architectures. When working with Azure Cosmos DB, one of the most powerful tools at your disposal is the Change Feed. The Change Feed acts as a persistent, ordered log of all modifications made to the documents within a container. Instead of querying for the current state of a database, the Change Feed allows you to react to the evolution of data in real-time.
Aggregation persistence—the process of calculating and storing running totals, averages, or state snapshots as data flows into the system—is a classic use case for this technology. Imagine an e-commerce platform where you need to track the total value of orders placed by a specific user or calculate the inventory level of a product across multiple warehouse regions. If you perform these calculations on every read request, your application will struggle with performance bottlenecks and high Request Unit (RU) costs.
By using the Change Feed to perform aggregations asynchronously, you shift the computational burden away from the user-facing request path. You transform the database from a passive storage bin into an active, event-driven engine. This lesson will guide you through the architectural patterns, implementation strategies, and operational best practices required to build efficient aggregation persistence solutions using the Cosmos DB Change Feed.
Understanding the Change Feed Mechanism
At its core, the Azure Cosmos DB Change Feed is a sorted list of documents in the order in which they were modified. When you enable the Change Feed on a container, you are essentially subscribing to a stream of events. Each event represents a document update, insertion, or deletion. Because the Change Feed is durable, if your processing application goes offline, it can resume exactly where it left off once it restarts.
The Change Feed provides the "what happened" component of your system. To turn this into "what is the current state," you need a processor. In the Azure ecosystem, the most common way to consume this feed is through the Change Feed Processor (CFP) library. This library manages the complex state of your processing progress, handles load balancing if you scale out your compute instances, and ensures that your aggregation logic runs reliably.
Callout: Change Feed vs. Querying Querying your database directly for aggregations (e.g.,
SELECT SUM(c.price) FROM c WHERE c.userId = '123') requires the database engine to scan potentially thousands of documents every time a user refreshes their dashboard. This is expensive and slow. Using the Change Feed, you calculate the sum incrementally as documents arrive. When a user requests their total, you simply read a single pre-aggregated document. This changes an O(N) operation into an O(1) operation.
Architectural Patterns for Aggregation
Before writing code, you must decide on the granularity of your aggregations. There are two primary patterns for storing these results:
1. The Single-Document Aggregate
In this pattern, you maintain a single document that holds the current state for a specific entity. For example, a document with the ID user-123-summary stores totalSpent, orderCount, and lastOrderDate. Every time the Change Feed triggers for an order belonging to user-123, your processor reads this summary document, updates the values, and saves it back to the database.
2. The Time-Series Windowing
If you need to track metrics over time (e.g., hourly sales), you use windowing. The Change Feed processor identifies the time bucket for an incoming order and updates a document corresponding to that bucket (e.g., sales-2023-10-27-14). This is excellent for telemetry and analytics where you need to see trends rather than just a single running total.
Note: When using the Single-Document Aggregate pattern, remember that you are creating a "hot spot" for that specific document. If a single user places orders at an extremely high frequency, updating the same document repeatedly can lead to partition key contention. Ensure your document design allows for high-frequency writes or consider a buffer layer if the throughput is exceptionally high.
Implementing the Change Feed Processor
To implement aggregation persistence, you need a .NET application (or another supported language) that acts as the listener. Below is a step-by-step approach to setting up a basic processor.
Step 1: Dependencies
You will need the Microsoft.Azure.Cosmos NuGet package. This package contains the Change Feed Processor library, which simplifies the management of lease documents.
Step 2: Configure the Lease Container
The Change Feed Processor needs a place to store "lease" documents. These documents track which instances are processing which parts of the feed and where they are in the timeline. You should create a separate, low-throughput container in your Cosmos DB account specifically for these leases.
Step 3: Define the Processor
The processor requires two main components: a Container instance for the source data and a Container instance for the leases. You will also define a delegate that handles the actual aggregation logic.
// Setting up the Change Feed Processor
Container leaseContainer = database.GetContainer("leases");
Container sourceContainer = database.GetContainer("orders");
ChangeFeedProcessor processor = sourceContainer
.GetChangeFeedProcessorBuilder("orderAggregationProcessor", HandleChangesAsync)
.WithInstanceName("ProcessorInstance1")
.WithLeaseContainer(leaseContainer)
.Build();
await processor.StartAsync();
Step 4: The Aggregation Logic (The HandleChangesAsync Method)
This is where the magic happens. Your delegate receives a list of changes. You must iterate through these changes, calculate the new state, and update your aggregation document.
async Task HandleChangesAsync(
IReadOnlyCollection<Order> changes,
CancellationToken cancellationToken)
{
foreach (var order in changes)
{
// 1. Retrieve the existing aggregate document
var aggregateDoc = await GetAggregateDocument(order.UserId);
// 2. Perform the aggregation
aggregateDoc.TotalSpent += order.Amount;
aggregateDoc.OrderCount++;
// 3. Persist the updated aggregate
await summaryContainer.UpsertItemAsync(aggregateDoc);
}
}
Handling Concurrency and Consistency
When multiple instances of your processor are running, or when multiple orders for the same user arrive simultaneously, you run the risk of race conditions. If two instances read the same aggregate document and attempt to update it, the last one to write will overwrite the changes of the first.
Optimistic Concurrency Control (OCC)
Cosmos DB supports ETag-based optimistic concurrency. Every document has an _etag property. When you read a document, you get its current ETag. When you save it back, you send that ETag in the AccessCondition header. If the document has changed since you read it, the write will fail with a 412 Precondition Failed error.
Tip: In your aggregation logic, always use the
ItemRequestOptionsto specify theIfMatchEtagcondition. If the update fails due to a conflict, you should implement a retry loop that re-reads the latest version of the aggregate document, re-applies the calculation, and attempts the write again.
Transactional Batches
If you are updating multiple related documents (e.g., updating a user summary and a global company sales total), you can use the TransactionalBatch feature. This ensures that either both updates succeed or neither does, maintaining total integrity across your aggregated data.
| Strategy | Pros | Cons |
|---|---|---|
| Optimistic Locking | Simple, low overhead, prevents overwrites. | Requires handling retries in code. |
| Transactional Batch | Ensures atomic updates across multiple docs. | Limited to documents in the same partition key. |
| Asynchronous Queue | Decouples processing, handles spikes well. | Adds architectural complexity and latency. |
Best Practices for Production
Building a system that runs reliably in production requires more than just functional code. You must account for failures, scaling, and monitoring.
1. Idempotency is Mandatory
The Change Feed guarantees "at-least-once" delivery. This means that under certain conditions, such as a network hiccup or a worker restart, your HandleChangesAsync method might receive the same document twice. Your aggregation logic must be idempotent. Instead of aggregateDoc.TotalSpent += order.Amount, consider maintaining a list of processed OrderIds within the aggregate document to ensure you never count the same order twice.
2. Monitoring the Lease Container
The lease container is the heartbeat of your Change Feed Processor. If the lease container is under-provisioned, the processor will lag, and your aggregates will fall behind. Use Azure Monitor to track the RU consumption of your lease container and set alerts for high latency or throttling.
3. Scaling Your Processors
The Change Feed Processor automatically balances load across multiple instances. If your processing volume increases, you can simply spin up more instances of your application. The library will detect the new instances and redistribute the lease documents accordingly. Do not manually assign work; let the library handle the orchestration.
4. Handling Poison Messages
What happens if a specific document is malformed and causes your HandleChangesAsync logic to throw an exception? The processor will retry the batch repeatedly, potentially stalling the entire feed. Implement a try-catch block within your delegate. If an error is unrecoverable, log the offending document ID to a "dead-letter" container and move on, ensuring the pipeline continues to flow.
Warning: Avoid long-running tasks inside your
HandleChangesAsyncmethod. The Change Feed Processor expects this method to complete quickly. If you have complex tasks, such as sending emails or calling external APIs, push those tasks to a secondary queue (like Azure Service Bus or Storage Queues) and return from the processor immediately.
Common Pitfalls and How to Avoid Them
Pitfall 1: Ignoring the Partition Key
When designing your aggregation documents, ensure they are partitioned in a way that aligns with your query patterns. If your aggregation document is partitioned by UserId, and you are constantly updating it, make sure the UserId is also part of your query path. Avoid cross-partition queries for your aggregate lookups, as they are significantly more expensive.
Pitfall 2: Over-provisioning the Feed Container
Many developers make the mistake of setting the throughput on the source container and the aggregation container to be the same. Often, the aggregation container is read-heavy while the source is write-heavy. Use auto-scale throughput on your containers to handle unpredictable traffic spikes without paying for idle capacity.
Pitfall 3: Neglecting "Deleted" Documents
By default, the Change Feed includes inserts and updates. If you need to handle deletions (e.g., if a user cancels an order, you need to subtract that amount from the total), you must explicitly configure the ChangeFeedProcessorOptions to include deletes.
ChangeFeedProcessorOptions options = new ChangeFeedProcessorOptions
{
StartFromBeginning = true,
PeekMode = false,
// Ensure we process deletions to keep aggregates accurate
// Note: This requires specific configuration on the ChangeFeedProcessorBuilder
};
Step-by-Step Implementation Checklist
- Analyze Data Flow: Identify the source container and the target aggregate structure.
- Define Schema: Design the JSON structure for the aggregate document. Include fields for metadata like
LastUpdatedTimestampandETag. - Provision Infrastructure: Create the source, lease, and target containers. Ensure they are in the same region for lowest latency.
- Develop Consumer: Write the .NET logic using the
ChangeFeedProcessorBuilder. - Implement Idempotency: Add logic to check if a specific change has already been applied.
- Add Error Handling: Use try-catch blocks and log failures to a dedicated diagnostic store.
- Performance Test: Use a load testing tool to simulate high-volume updates and observe how the processor handles lag.
- Monitor: Set up alerts for "Change Feed Lag," which indicates the time difference between the latest change in the source and the current position of the processor.
Comparison of Aggregation Techniques
To choose the right approach for your specific scenario, consider the trade-offs in the table below:
| Approach | Latency | Cost | Complexity | Use Case |
|---|---|---|---|---|
| On-Demand Query | High | High | Low | Infrequent, ad-hoc reports |
| Change Feed Aggregate | Low | Low | Medium | Real-time dashboards, counters |
| Materialized View (SQL) | Low | Medium | High | Complex multi-join reporting |
| Stream Processing (Stream Analytics) | Low | High | High | Complex windowing/pattern matching |
As shown, the Change Feed approach strikes an excellent balance between cost and performance for most standard aggregation tasks. It is significantly cheaper than running an always-on stream processing engine while providing far lower latency than on-demand queries.
Advanced Topic: Managing Throughput with Feed Lag
One of the most important metrics to watch in a production Change Feed implementation is "Feed Lag." This metric represents the delay between when a document is written to the source container and when it is processed by your function. If your lag starts to grow indefinitely, it means your consumer cannot keep up with the rate of incoming changes.
To mitigate this, you have three primary levers:
- Scale Out: Add more instances of your processor. The Change Feed Processor will automatically distribute the work.
- Optimize the Delegate: If your
HandleChangesAsyncis doing too much work, simplify it. Move non-critical tasks out of the loop. - Increase Throughput: If your database is being throttled, you may need to increase the Request Units (RUs) on your source or lease containers.
Callout: The "Lease Container" Secret Many developers view the lease container as a simple implementation detail. In reality, it is your primary tool for monitoring. By inspecting the lease documents, you can see exactly which instance is responsible for which partition and how far behind they are. If you ever need to "reset" the processing, you can delete the leases (with caution) to force the processor to start from the beginning of the feed.
Testing Your Implementation
You cannot rely on unit tests alone for Change Feed implementations. You need integration tests that actually push documents into a test container and verify that the aggregate document is updated correctly.
Use the following strategy for testing:
- Sequential Tests: Push documents one by one and verify the aggregate.
- Burst Tests: Push 100 documents simultaneously and verify the aggregate reflects the total.
- Failure Simulation: Stop your processor mid-stream, wait, and restart it to ensure it resumes correctly without missing or duplicating data.
When writing your test code, use the CosmosClient to clean up the containers between runs. A clean slate is essential for verifying that your StartFromBeginning logic works as expected.
// Example integration test snippet
[Fact]
public async Task Aggregation_ShouldUpdateTotal_WhenOrderIsCreated()
{
// Arrange
var order = new Order { Id = Guid.NewGuid().ToString(), UserId = "U1", Amount = 100 };
await sourceContainer.CreateItemAsync(order);
// Act
// Give the processor a moment to react
await Task.Delay(2000);
// Assert
var aggregate = await summaryContainer.ReadItemAsync<UserSummary>("U1", new PartitionKey("U1"));
Assert.Equal(100, aggregate.Resource.TotalSpent);
}
Key Takeaways for Success
Implementing aggregation persistence with the Cosmos DB Change Feed is a foundational skill for building scalable, responsive cloud applications. By following these principles, you ensure your system remains performant as it grows.
- Shift from Pull to Push: Stop calculating aggregates on read. Use the Change Feed to maintain pre-calculated states incrementally.
- Design for Idempotency: Because the Change Feed guarantees "at-least-once" delivery, your code must handle duplicate events without corrupting the state.
- Use Optimistic Concurrency: Protect your aggregate documents from race conditions by using ETags and
IfMatchheaders. - Monitor Feed Lag: Keep a close eye on the latency between the source update and the aggregate update. If lag increases, scale your processor instances.
- Separate Lease Containers: Always use a dedicated, low-throughput container for lease management to prevent interference with your primary data.
- Handle Poison Messages: Implement robust error handling within your processor to ensure that one bad document doesn't block the entire pipeline.
- Keep Logic Lean: The Change Feed Processor delegate should be fast. Offload heavy lifting or external service calls to background queues to maintain throughput.
By adopting these patterns, you turn your database into a dynamic engine capable of handling complex state management with minimal overhead. As you move forward, continue to refine your partition strategies and monitor your RU usage, as these will be the primary drivers of your operational costs and system performance.
Reach the last section to complete this lesson and earn points — you're on section 1 of 11.
- Introduction to Cosmos DB Data Modeling
- Introduction to Cosmos DB Data Modeling Quiz5q
- Multiple Entity Types in Same Container
- Multiple Entity Types in Same Container Quiz5q
- Storing Related Entities in Same Document
- Storing Related Entities in Same Document Quiz5q
- Denormalizing Data Across Documents
- Denormalizing Data Across Documents Quiz5q
- Referencing Between Documents
- Referencing Between Documents Quiz5q
- Partition Keys and Document IDs
- Partition Keys and Document IDs Quiz5q
- Time to Live (TTL) Configuration
- Time to Live (TTL) Configuration Quiz5q
- Document Versioning Strategies
- Document Versioning Strategies Quiz5q
- Schema Versioning Patterns
- Schema Versioning Patterns Quiz5q
- Choosing Partition Strategies
- Choosing Partition Strategies Quiz5q
- Partition Key Selection Best Practices
- Partition Key Selection Best Practices Quiz5q
- Transactions and Partition Keys
- Transactions and Partition Keys Quiz5q
- Cross-Partition Query Costs
- Cross-Partition Query Costs Quiz5q
- Data Distribution Analysis
- Data Distribution Analysis Quiz5q
- Throughput Distribution Planning
- Throughput Distribution Planning Quiz5q
- Synthetic Partition Keys
- Synthetic Partition Keys Quiz5q
- Hierarchical Partition Keys
- Hierarchical Partition Keys Quiz5q
- Throughput and Storage Requirements
- Throughput and Storage Requirements Quiz5q
- Serverless vs Provisioned Throughput
- Serverless vs Provisioned Throughput Quiz5q
- Database-Level Provisioned Throughput
- Database-Level Provisioned Throughput Quiz5q
- Granular Scale Units
- Granular Scale Units Quiz5q
- Global Distribution Costs
- Global Distribution Costs Quiz5q
- Configuring Throughput in Portal
- Configuring Throughput in Portal Quiz5q
- Gateway vs Direct Connectivity Mode
- Gateway vs Direct Connectivity Mode Quiz5q
- Creating Database Connections
- Creating Database Connections Quiz5q
- Azure Cosmos DB Emulator
- Azure Cosmos DB Emulator Quiz5q
- Connection Error Handling
- Connection Error Handling Quiz5q
- Singleton Pattern for Clients
- Singleton Pattern for Clients Quiz5q
- Global Distribution Regions
- Global Distribution Regions Quiz5q
- Threading and Parallelism
- Threading and Parallelism Quiz5q
- Arrays and Nested Objects Queries
- Arrays and Nested Objects Queries Quiz5q
- Correlated Subqueries
- Correlated Subqueries Quiz5q
- Array and Type-Checking Functions
- Array and Type-Checking Functions Quiz5q
- Mathematical and String Functions
- Mathematical and String Functions Quiz5q
- Date Functions in Queries
- Date Functions in Queries Quiz5q
- Point Operations vs Query Operations
- Point Operations vs Query Operations Quiz5q
- CRUD Point Operations
- CRUD Point Operations Quiz5q
- Patch Operations for Updates
- Patch Operations for Updates Quiz5q
- Transactional Batch Operations
- Transactional Batch Operations Quiz5q
- Bulk Operations with SDK
- Bulk Operations with SDK Quiz5q
- Optimistic Concurrency with ETags
- Optimistic Concurrency with ETags Quiz5q
- Query Pagination and Continuation
- Query Pagination and Continuation Quiz5q
- Cosmos DB Mirroring for Fabric
- Cosmos DB Mirroring for Fabric Quiz5q
- Mirroring vs Spark Connector
- Mirroring vs Spark Connector Quiz5q
- Enabling Analytical Store
- Enabling Analytical Store Quiz5q
- Synapse Spark and SQL Queries
- Synapse Spark and SQL Queries Quiz5q
- Change Data Capture in Analytical Store
- Change Data Capture in Analytical Store Quiz5q
- Azure Functions and Event Hubs Integration
- Azure Functions and Event Hubs Integration Quiz5q
- Denormalization with Change Feed
- Denormalization with Change Feed Quiz5q
- Referential Integrity with Change Feed
- Referential Integrity with Change Feed Quiz5q
- Azure AI Search Integration
- Azure AI Search Integration Quiz5q
- Azure Functions Change Feed Trigger
- Azure Functions Change Feed Trigger Quiz5q
- Consuming Change Feed with SDK
- Consuming Change Feed with SDK Quiz5q
- Change Feed Estimator
- Change Feed Estimator Quiz5q
- Denormalization via Change Feed
- Denormalization via Change Feed Quiz5q
- Aggregation Persistence with Change Feed
- Aggregation Persistence with Change Feed Quiz5q
- Read-Heavy vs Write-Heavy Indexing
- Read-Heavy vs Write-Heavy Indexing Quiz5q
- Index Type Selection
- Index Type Selection Quiz5q
- Custom Indexing Policies
- Custom Indexing Policies Quiz5q
- Composite Index Implementation
- Composite Index Implementation Quiz5q
- Index Performance Optimization
- Index Performance Optimization Quiz5q
- Response Status Codes and Metrics
- Response Status Codes and Metrics Quiz5q
- Normalized RU Consumption Monitoring
- Normalized RU Consumption Monitoring Quiz5q
- Server-Side Latency Metrics
- Server-Side Latency Metrics Quiz5q
- Data Replication Monitoring
- Data Replication Monitoring Quiz5q
- Azure Monitor Alerts Configuration
- Azure Monitor Alerts Configuration Quiz5q
- Resource Logs Implementation
- Resource Logs Implementation Quiz5q
- Partition Throughput Monitoring
- Partition Throughput Monitoring Quiz5q
- Encryption Key Management
- Encryption Key Management Quiz5q
- Network-Level Access Control
- Network-Level Access Control Quiz5q
- Data Encryption Configuration
- Data Encryption Configuration Quiz5q
- Azure RBAC for Control Plane
- Azure RBAC for Control Plane Quiz5q
- Microsoft Entra ID for Data Plane
- Microsoft Entra ID for Data Plane Quiz5q
- CORS Settings Configuration
- CORS Settings Configuration Quiz5q
- Customer-Managed Keys
- Customer-Managed Keys Quiz5q
- Always Encrypted Implementation
- Always Encrypted Implementation Quiz5q
- Data Movement Strategy Selection
- Data Movement Strategy Selection Quiz5q
- SDK Bulk Operations for Data Movement
- SDK Bulk Operations for Data Movement Quiz5q
- Azure Data Factory Pipelines
- Azure Data Factory Pipelines Quiz5q
- Kafka Connector Integration
- Kafka Connector Integration Quiz5q
- Azure Stream Analytics Integration
- Azure Stream Analytics Integration Quiz5q
- Cosmos DB Spark Connector
- Cosmos DB Spark Connector Quiz5q
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles the points you earn on every lesson and quiz so you progress twice as fast, unlocks half of every practice exam — plus full case studies — with the Learn & Exam study modes, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× points per lesson & quiz
- ✓ 50% of every exam unlocked
- ✓ Learn & Exam modes
- ✓ Distraction-free lessons