EngineeringSenior DeveloperArchitecture and Patterns

05 - Event-Driven Architecture Blueprint

Kiến trúc xây trên nền tảng sự kiện (events) thay vì gọi trực tiếp. Blueprint này dùng stack: .NET 9 + Next.js + Azure Service Bus + Azure Functions + SQL Server + Azure Entra ID (SSO).


Tổng Quan


Event Flow Chi Tiết


Khi Nào Dùng / Không Dùng

✅ Dùng khi❌ Không dùng khi
Workflows nhiều bước, nhiều side effectsSimple request-response CRUD
Cần decouple producer ↔ consumerMọi thứ cần response NGAY LẬP TỨC
Scale consumers ĐỘC LẬP (email vs inventory)Team nhỏ, không có infra cho message broker
Eventual consistency chấp nhận đượcStrong consistency BẮT BUỘC mọi lúc
Serverless workload (pay-per-execution)Luôn cần compute 24/7

Dự án hay dùng: eCommerce (đặt hàng → xử lý kho → gửi mail), IoT telemetry, Logistics tracking, Fintech (fraud detection pipeline).


Project Structure (Full Stack)

eventdriven-app/
├── frontend/
│   └── next-app/
│       ├── app/
│       │   ├── layout.tsx
│       │   ├── page.tsx
│       │   ├── orders/
│       │   │   └── page.tsx
│       │   └── api/
│       │       └── auth/[...nextauth]/route.ts
│       ├── lib/
│       │   ├── auth.ts            ← MSAL.js config (Azure Entra ID)
│       │   └── api-client.ts      ← Fetch wrapper with Bearer Token
│       ├── next.config.js
│       └── package.json
│
├── backend/
│   ├── src/
│   │   ├── OrderApi/              ← .NET 9 Minimal API
│   │   │   ├── Program.cs
│   │   │   ├── Features/
│   │   │   │   ├── Orders/
│   │   │   │   │   ├── CreateOrder.cs
│   │   │   │   │   └── GetOrders.cs
│   │   │   │   └── Products/
│   │   │   ├── Infrastructure/
│   │   │   │   ├── AppDbContext.cs
│   │   │   │   ├── Outbox/
│   │   │   │   │   ├── OutboxMessage.cs
│   │   │   │   │   └── OutboxProcessor.cs   ← Background Service
│   │   │   │   └── Auth/
│   │   │   │       └── EntraIdExtensions.cs
│   │   │   ├── Dockerfile
│   │   │   └── appsettings.json
│   │   │
│   │   └── Functions/             ← Azure Functions (Isolated Worker)
│   │       ├── InventoryProcessor.cs
│   │       ├── EmailNotifier.cs
│   │       ├── AnalyticsIngestion.cs
│   │       ├── Program.cs
│   │       └── host.json
│   │
│   └── shared/
│       └── Contracts/
│           ├── Events/
│           │   ├── OrderCreatedEvent.cs
│           │   └── OrderCancelledEvent.cs
│           └── DTOs/
│               └── OrderDto.cs
│
├── infra/
│   ├── bicep/
│   │   ├── main.bicep
│   │   ├── servicebus.bicep
│   │   └── function.bicep
│   └── docker-compose.yml
│
└── eventdriven-app.sln

NuGet & NPM Checklist

Backend (.NET 9)

xml
<!-- Order API -->
<PackageReference Include="Microsoft.Identity.Web" Version="3.*" />  <!-- Entra ID -->
<PackageReference Include="MassTransit.Azure.ServiceBus.Core" Version="8.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.*" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.*" />
<PackageReference Include="Serilog.AspNetCore" Version="8.*" />

<!-- Azure Functions (Isolated) -->
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.ServiceBus" Version="5.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.*" />

Frontend (Next.js)

json
{
  "dependencies": {
    "next": "14.*",
    "@azure/msal-browser": "^3.0",
    "@azure/msal-react": "^2.0",
    "next-auth": "^5.0"
  }
}

Code Blueprint - Key Components

1. Azure Entra ID Setup (.NET API)

csharp
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

// appsettings.json
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-api-client-id",
    "Audience": "api://your-api-client-id"
  }
}

2. Next.js MSAL Integration

typescript
// lib/auth.ts
import { PublicClientApplication } from "@azure/msal-browser";

export const msalConfig = {
  auth: {
    clientId: process.env.NEXT_PUBLIC_AZURE_CLIENT_ID!,
    authority: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_AZURE_TENANT_ID}`,
    redirectUri: "http://localhost:3000",
  },
};

export const loginRequest = {
  scopes: ["api://your-api-client-id/.default"],
};

// lib/api-client.ts
export async function fetchWithAuth(url: string, msalInstance: PublicClientApplication) {
  const account = msalInstance.getActiveAccount();
  const token = await msalInstance.acquireTokenSilent({
    ...loginRequest,
    account: account!,
  });

  return fetch(url, {
    headers: { Authorization: `Bearer ${token.accessToken}` },
  });
}

3. Outbox Pattern (Reliable Event Publishing)

csharp
// Features/Orders/CreateOrder.cs
app.MapPost("/api/orders", async (CreateOrderRequest req, AppDbContext db) =>
{
    await using var tx = await db.Database.BeginTransactionAsync();

    var order = new Order { /* ... */ };
    db.Orders.Add(order);

    // Save event to Outbox (SAME transaction!)
    db.OutboxMessages.Add(new OutboxMessage
    {
        Id = Guid.NewGuid(),
        Type = nameof(OrderCreatedEvent),
        Payload = JsonSerializer.Serialize(new OrderCreatedEvent(order.Id, order.ProductId, order.Quantity)),
        CreatedAt = DateTime.UtcNow
    });

    await db.SaveChangesAsync();
    await tx.CommitAsync();

    return Results.Accepted($"/api/orders/{order.Id}");
});

4. Azure Function Consumer

csharp
// Functions/InventoryProcessor.cs
public class InventoryProcessor(IDbContextFactory<AppDbContext> dbFactory)
{
    [Function(nameof(ProcessOrderCreated))]
    public async Task ProcessOrderCreated(
        [ServiceBusTrigger("order-events", "inventory-processor",
            Connection = "ServiceBusConnection")]
        OrderCreatedEvent evt,
        FunctionContext context)
    {
        await using var db = await dbFactory.CreateDbContextAsync();

        var product = await db.Products.FindAsync(evt.ProductId);
        if (product is null || product.Stock < evt.Quantity)
        {
            // Publish compensation event
            throw new InvalidOperationException("Insufficient stock - will retry/DLQ");
        }

        product.Stock -= evt.Quantity;
        await db.SaveChangesAsync();
    }
}

5. Azure Service Bus Topic/Subscription


Infrastructure as Code (Bicep)

bicep
// infra/bicep/servicebus.bicep
resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
  name: 'myapp-sb-${environment}'
  location: location
  sku: { name: 'Standard', tier: 'Standard' }
}

resource topic 'Microsoft.ServiceBus/namespaces/topics@2022-10-01-preview' = {
  parent: serviceBusNamespace
  name: 'order-events'
  properties: { maxSizeInMegabytes: 1024 }
}

resource inventorySub 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2022-10-01-preview' = {
  parent: topic
  name: 'inventory-processor'
  properties: {
    maxDeliveryCount: 3
    deadLetteringOnMessageExpiration: true
  }
}