4 min read
The Serverless Evolution: Where We Landed in 2021
Serverless has evolved beyond simple functions. In 2021, we saw serverless patterns mature and enterprises embrace them for real workloads. Let’s explore where serverless stands today.
Azure Functions Durable Entities
Stateful serverless became practical with Durable Entities:
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using System.Threading.Tasks;
// Define a counter entity
[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
switch (ctx.OperationName.ToLowerInvariant())
{
case "add":
ctx.SetState(ctx.GetState<int>() + ctx.GetInput<int>());
break;
case "reset":
ctx.SetState(0);
break;
case "get":
ctx.Return(ctx.GetState<int>());
break;
}
}
// Orchestrator using the entity
[FunctionName("ProcessOrderOrchestrator")]
public static async Task<string> ProcessOrder(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var order = context.GetInput<Order>();
var entityId = new EntityId("Counter", $"orders-{order.CustomerId}");
// Increment order count atomically
await context.CallEntityAsync(entityId, "add", 1);
// Get current count
var count = await context.CallEntityAsync<int>(entityId, "get");
if (count >= 10)
{
// Apply loyalty discount
await context.CallActivityAsync("ApplyLoyaltyDiscount", order);
}
return $"Order processed. Customer has {count} orders.";
}
The Serverless Data Processing Pattern
Processing data at scale without managing infrastructure:
import azure.functions as func
import json
from azure.storage.blob import BlobServiceClient
import pandas as pd
import io
def main(event: func.EventHubEvent, outputBlob: func.Out[bytes]):
"""Process IoT events and write aggregated results"""
events = []
for e in event:
body = e.get_body().decode('utf-8')
data = json.loads(body)
events.append({
'device_id': data['deviceId'],
'temperature': data['temperature'],
'humidity': data['humidity'],
'timestamp': data['timestamp'],
'partition_key': e.partition_key
})
# Create DataFrame for processing
df = pd.DataFrame(events)
# Aggregate by device
aggregated = df.groupby('device_id').agg({
'temperature': ['mean', 'min', 'max'],
'humidity': ['mean', 'min', 'max'],
'timestamp': 'count'
}).round(2)
aggregated.columns = ['_'.join(col).strip() for col in aggregated.columns]
aggregated = aggregated.reset_index()
# Output to blob storage
output = io.BytesIO()
aggregated.to_parquet(output, index=False)
outputBlob.set(output.getvalue())
Serverless Containers with Azure Container Apps
The gap between functions and containers narrowed:
# Azure Container Apps - serverless containers
apiVersion: 2022-01-01-preview
kind: ContainerApp
metadata:
name: api-service
spec:
configuration:
activeRevisionsMode: Multiple
ingress:
external: true
targetPort: 8080
traffic:
- latestRevision: true
weight: 80
- revisionName: api-service--v1
weight: 20
secrets:
- name: connection-string
value: ${CONNECTION_STRING}
dapr:
enabled: true
appId: api-service
appPort: 8080
template:
containers:
- name: api
image: myregistry.azurecr.io/api:latest
resources:
cpu: 0.5
memory: 1Gi
env:
- name: DB_CONNECTION
secretRef: connection-string
scale:
minReplicas: 0
maxReplicas: 10
rules:
- name: http-scaling
http:
metadata:
concurrentRequests: "100"
- name: queue-scaling
azureQueue:
queueName: orders
queueLength: 50
Event-Driven Patterns Solidified
The combination of Event Grid, Functions, and Logic Apps became powerful:
using Azure.Messaging.EventGrid;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Microsoft.Extensions.Logging;
public static class OrderEventProcessor
{
[FunctionName("ProcessOrderCreated")]
public static async Task ProcessOrderCreated(
[EventGridTrigger] EventGridEvent eventGridEvent,
[CosmosDB(
databaseName: "orders",
collectionName: "events",
ConnectionStringSetting = "CosmosDBConnection")] IAsyncCollector<dynamic> documents,
[ServiceBus(
"notifications",
Connection = "ServiceBusConnection")] IAsyncCollector<string> notifications,
ILogger log)
{
var data = eventGridEvent.Data.ToObjectFromJson<OrderCreatedEvent>();
// Store event for event sourcing
await documents.AddAsync(new
{
id = eventGridEvent.Id,
eventType = eventGridEvent.EventType,
subject = eventGridEvent.Subject,
data = data,
timestamp = eventGridEvent.EventTime,
partitionKey = data.CustomerId
});
// Trigger downstream notifications
if (data.TotalAmount > 1000)
{
await notifications.AddAsync(JsonSerializer.Serialize(new
{
Type = "HighValueOrder",
OrderId = data.OrderId,
CustomerId = data.CustomerId,
Amount = data.TotalAmount
}));
}
log.LogInformation($"Processed order {data.OrderId} for ${data.TotalAmount}");
}
}
Cold Start Optimization
Cold starts remained a challenge, but we found solutions:
// Premium plan with pre-warmed instances
// host.json configuration
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": "api",
"maxOutstandingRequests": 200,
"maxConcurrentRequests": 100
}
},
"functionTimeout": "00:10:00",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond": 20
}
}
}
}
// Minimize cold start with lazy loading
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
// Only import what's needed at module level
let cosmosClient: CosmosClient | null = null;
const getCosmosClient = async () => {
if (!cosmosClient) {
const { CosmosClient } = await import("@azure/cosmos");
cosmosClient = new CosmosClient(process.env.COSMOS_CONNECTION!);
}
return cosmosClient;
};
const httpTrigger: AzureFunction = async function (
context: Context,
req: HttpRequest
): Promise<void> {
const client = await getCosmosClient();
const { database } = await client.databases.createIfNotExists({ id: "mydb" });
const { container } = await database.containers.createIfNotExists({ id: "items" });
const { resources } = await container.items.query({
query: "SELECT * FROM c WHERE c.status = @status",
parameters: [{ name: "@status", value: req.query.status || "active" }]
}).fetchAll();
context.res = {
body: resources
};
};
export default httpTrigger;
Lessons from 2021
- Serverless Isn’t Always Cheaper: At scale, the cost model can flip
- Cold Starts Matter: For latency-sensitive workloads, plan accordingly
- Stateful Patterns Work: Durable Functions and entities are production-ready
- Vendor Lock-in is Real: Abstract where possible, accept where necessary
Looking to 2022
- More sophisticated scaling algorithms
- Better tooling for local development
- Improved observability and debugging
- Serverless databases gaining traction
Serverless in 2021 proved it’s not just for simple APIs. Complex, stateful, event-driven applications are running successfully in production. The technology has matured - now it’s about choosing the right tool for each job.