Change Feed Patterns: Materialized Views and Estimator
Implement advanced change feed patterns β materialized views, denormalization propagation, event-driven aggregation, the change feed estimator for lag monitoring, and triggering downstream actions.
Change feed as an event backbone
The change feed is like a conveyor belt in a factory. Module 15 taught you how to set up the belt (Azure Functions, processors). Now weβre building the machines at the end of the belt β machines that create summary reports, update other databases, send alerts, and keep everything in sync.
Amaraβs patterns at SensorFlow
π‘ Amara at SensorFlow uses the change feed to power three critical patterns:
- Dashboard materialized view β pre-computed βlatest reading per deviceβ for fast dashboard loads
- Alert propagation β when a reading exceeds threshold, write to an alerts container
- Lag monitoring β ensure processing stays within 30 seconds of real-time
Pattern 1: Materialized views
A materialized view is a pre-computed, query-optimised container thatβs kept in sync via the change feed.
Source container: "readings" Target container: "device-latest"
partitioned by /deviceId partitioned by /region
500M events/day 10,000 documents (one per device)
Change feed processor: on each new reading,
upsert into device-latest with the latest values
static async Task HandleChangesAsync(
ChangeFeedProcessorContext context,
IReadOnlyCollection<SensorReading> changes,
CancellationToken ct)
{
foreach (SensorReading reading in changes)
{
// Upsert into the materialized view container
var latest = new DeviceLatest
{
id = reading.DeviceId, // one doc per device
Region = reading.Region, // partition key for view
DeviceId = reading.DeviceId,
Temperature = reading.Temperature,
LastUpdated = reading.Timestamp
};
await latestContainer.UpsertItemAsync(latest,
new PartitionKey(latest.Region));
}
}
Why this is powerful:
- The dashboard reads 10,000 docs from
device-latest(cheap, single-partition queries by region) instead of scanning 500M docs inreadings - Different partition key on the view container enables query patterns not possible on the source
Exam tip: materialized views reduce query cost
Materialized views trade write cost (change feed processing + upserts) for read cost savings (cheap reads from the pre-computed view). This is ideal when data is written once but read many times (high read-to-write ratio).
The exam may present a scenario where a complex, expensive query runs frequently. The answer is often: βUse the change feed to maintain a materialized view.β
Pattern 2: Denormalization propagation
When you embed (denormalize) data across containers, the change feed keeps copies in sync:
"products" container (source of truth)
{ id: "P1", name: "Sensor-X", price: 29.99 }
"orders" container (denormalized copy)
{ id: "O1", productName: "Sensor-X", productPrice: 29.99, ... }
When product P1's price changes:
Change feed processor updates ALL orders referencing P1
static async Task PropagateProductChanges(
ChangeFeedProcessorContext context,
IReadOnlyCollection<Product> changes,
CancellationToken ct)
{
foreach (Product product in changes)
{
// Find all orders with this product and update denormalized fields
var query = new QueryDefinition(
"SELECT * FROM c WHERE c.productId = @pid")
.WithParameter("@pid", product.Id);
using FeedIterator<Order> iterator =
ordersContainer.GetItemQueryIterator<Order>(query);
while (iterator.HasMoreResults)
{
FeedResponse<Order> orders = await iterator.ReadNextAsync();
foreach (Order order in orders)
{
order.ProductName = product.Name;
order.ProductPrice = product.Price;
await ordersContainer.UpsertItemAsync(order,
new PartitionKey(order.CustomerId));
}
}
}
}
Pattern 3: Event-driven aggregation
Maintain running aggregates without expensive queries:
// Maintain a running average temperature per device
static async Task UpdateAggregates(
ChangeFeedProcessorContext context,
IReadOnlyCollection<SensorReading> changes,
CancellationToken ct)
{
foreach (SensorReading reading in changes)
{
// Read current aggregate
var response = await aggregatesContainer.ReadItemAsync<DeviceAggregate>(
reading.DeviceId, new PartitionKey(reading.DeviceId));
var agg = response.Resource;
agg.ReadingCount++;
agg.TotalTemperature += reading.Temperature;
agg.AverageTemperature = agg.TotalTemperature / agg.ReadingCount;
agg.MaxTemperature = Math.Max(agg.MaxTemperature, reading.Temperature);
await aggregatesContainer.UpsertItemAsync(agg,
new PartitionKey(agg.DeviceId));
}
}
The change feed estimator
The estimator monitors processing lag β how far behind your processor is from the latest changes:
ChangeFeedProcessor estimator = monitoredContainer
.GetChangeFeedEstimatorBuilder(
processorName: "dashboardUpdater",
estimationDelegate: HandleEstimation)
.WithLeaseContainer(leaseContainer)
.Build();
await estimator.StartAsync();
static async Task HandleEstimation(
long remainingWork,
CancellationToken ct)
{
Console.WriteLine($"Estimated lag: {remainingWork} changes behind");
if (remainingWork > 10000)
{
// Alert: processor is falling behind!
// Consider scaling up instances or investigating bottlenecks
}
}
Key details:
remainingWorkis the estimated number of unprocessed changes across all partitions- The estimator reads from the same lease container as the processor
- It does NOT process changes β it only monitors
- Use it to trigger scaling decisions or alerting
Exam tip: estimator vs processor
The change feed estimator and processor are separate components:
- The processor reads and processes changes
- The estimator monitors how far behind the processor is
They share the same lease container β the estimator reads lease checkpoints to calculate lag. You can run the estimator in a separate process (e.g., a monitoring Azure Function).
Pattern 4: Triggering downstream actions
The change feed can trigger actions in external systems:
| Downstream Action | Example |
|---|---|
| Push notifications | Alert mobile app when sensor threshold exceeded |
| Search index update | Sync changes to Azure Cognitive Search |
| Analytics pipeline | Stream changes to Event Hubs or Kafka |
| Cache invalidation | Clear Redis or CDN cache when data changes |
| Audit log | Write change history to a separate container |
π¬ Video walkthrough
π¬ Video coming soon
Change Feed Patterns β DP-420 Module 21
Change Feed Patterns β DP-420 Module 21
~16 minFlashcards
Knowledge Check
Amara's dashboard query scans 500M readings to find the latest value per device, costing 50,000 RU per execution. The dashboard refreshes every 30 seconds. What's the best optimisation?
The change feed estimator reports remainingWork of 50,000 changes. What does this mean?
GlobeCart embeds product names in order documents. When a product is renamed, which pattern keeps order documents in sync?
Next up: Monitoring β metrics, logs, alerts, and Azure Monitor insights for keeping your Cosmos DB solution healthy.