Back to Blog
2 min read

Building Event-Driven Architectures with Azure Event Grid and Functions

Event-driven architecture became the dominant pattern for scalable cloud applications in 2025. Here’s how to build robust event-driven systems using Azure Event Grid and Functions.

Architecture Overview

Source Events -> Event Grid Topic -> Subscriptions -> Azure Functions -> Downstream Services
                                        |
                              Dead Letter Storage
                                        |
                              Retry Handler

Defining Custom Events

public record OrderPlacedEvent
{
    public string EventType => "Order.Placed";
    public string Subject => $"/orders/{OrderId}";
    public required string OrderId { get; init; }
    public required string CustomerId { get; init; }
    public required decimal TotalAmount { get; init; }
    public required List<OrderItem> Items { get; init; }
    public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
}

public record OrderItem
{
    public required string ProductId { get; init; }
    public required int Quantity { get; init; }
    public required decimal UnitPrice { get; init; }
}

Publishing Events

using Azure.Messaging.EventGrid;

public class EventPublisher
{
    private readonly EventGridPublisherClient _client;

    public EventPublisher(string topicEndpoint, string topicKey)
    {
        _client = new EventGridPublisherClient(
            new Uri(topicEndpoint),
            new AzureKeyCredential(topicKey));
    }

    public async Task PublishOrderPlacedAsync(OrderPlacedEvent orderEvent)
    {
        var cloudEvent = new CloudEvent(
            source: "/orders/service",
            type: orderEvent.EventType,
            jsonSerializableData: orderEvent)
        {
            Subject = orderEvent.Subject,
            Time = orderEvent.OccurredAt
        };

        await _client.SendEventAsync(cloudEvent);
    }
}

Event Handler Function

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class OrderEventHandlers
{
    private readonly ILogger<OrderEventHandlers> _logger;
    private readonly IInventoryService _inventory;
    private readonly INotificationService _notifications;

    public OrderEventHandlers(
        ILogger<OrderEventHandlers> logger,
        IInventoryService inventory,
        INotificationService notifications)
    {
        _logger = logger;
        _inventory = inventory;
        _notifications = notifications;
    }

    [Function("ProcessOrderPlaced")]
    public async Task ProcessOrderPlaced(
        [EventGridTrigger] CloudEvent cloudEvent)
    {
        var order = cloudEvent.Data.ToObjectFromJson<OrderPlacedEvent>();

        _logger.LogInformation("Processing order {OrderId}", order.OrderId);

        // Reserve inventory
        foreach (var item in order.Items)
        {
            await _inventory.ReserveAsync(item.ProductId, item.Quantity);
        }

        // Send confirmation
        await _notifications.SendOrderConfirmationAsync(
            order.CustomerId,
            order.OrderId);

        _logger.LogInformation("Order {OrderId} processed successfully", order.OrderId);
    }
}

Dead Letter Handling

[Function("ProcessDeadLetter")]
public async Task ProcessDeadLetter(
    [BlobTrigger("deadletter/{name}", Connection = "StorageConnection")]
    Stream deadLetterBlob,
    string name,
    ILogger logger)
{
    using var reader = new StreamReader(deadLetterBlob);
    var content = await reader.ReadToEndAsync();
    var failedEvent = JsonSerializer.Deserialize<CloudEvent>(content);

    logger.LogWarning(
        "Dead letter event: {EventType}, Subject: {Subject}",
        failedEvent?.Type,
        failedEvent?.Subject);

    // Analyze failure and take action
    await AlertOperationsTeam(failedEvent);
}

Best Practices

  1. Use CloudEvents format for interoperability
  2. Implement idempotency - events may be delivered multiple times
  3. Set appropriate retry policies based on handler complexity
  4. Monitor dead letter queues actively
  5. Version your events to support evolution

Event-driven architecture requires careful design but delivers superior scalability and resilience for distributed systems.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.