Sarıkaya Dev Logo

Building Event‑Driven Microservices with .NET 8 Minimal APIs and NATS JetStream

Mahmut Sarıkaya 5 min read 2 Views 0
Building Event‑Driven Microservices with .NET 8 Minimal APIs and NATS JetStream

Why event‑driven microservices matter in 2024

Enterprises that migrated more than 30% of their workloads to microservices between 2021 and 2023 reported a 22% reduction in time‑to‑market for new features. The secret ingredient is often an event‑driven architecture that decouples producers from consumers, enabling independent scaling and fault isolation.

When you combine that model with the lean syntax of .NET 8 Minimal APIs and the high‑throughput guarantees of NATS JetStream, you get a stack that can handle millions of messages per second while keeping codebases under 200 lines per service.

Minimal APIs in .NET 8: the new baseline

.NET 8 introduced first‑class support for Minimal APIs, letting developers define HTTP endpoints without Controllers, Startup classes or explicit routing tables. A typical program.cs now fits on a single page, which reduces cognitive load and speeds up onboarding.

Key features include top‑level statements, implicit DI registration, and built‑in OpenAPI generation. For a microservice that only needs to publish an event, the whole API can be expressed in less than 50 lines of C#.

NATS JetStream fundamentals for reliable messaging

NATS JetStream adds persistence, message replay, and at‑least‑once delivery on top of the lightweight NATS core. It stores messages in streams, each identified by a subject pattern, and allows consumers to pull or push messages with configurable ack policies.

Performance benchmarks from the official NATS team show sub‑millisecond latency for 1 M messages/sec on a 4‑core VM, making it a solid choice for high‑frequency event pipelines.

Connecting Minimal APIs to JetStream

The first step is to add the NATS client package. Run:

dotnet add package NATS.Client

Then configure a singleton connection in the DI container:

builder.Services.AddSingleton<IConnection>(sp => {
    var opts = Options.CreateBuilder()
        .WithUrl("nats://localhost:4222")
        .WithConnectionName("order‑service")
        .Build();
    return new ConnectionFactory().CreateConnection(opts);
});

Notice the use of top‑level statements; no explicit Program class is required. The Minimal API endpoint publishes an "order.created" event to a JetStream stream named "ORDERS".

var app = builder.Build();

app.MapPost("/orders", async (OrderDto order, IConnection nc) => {
    var js = nc.CreateJetStreamContext();
    var data = JsonSerializer.SerializeToUtf8Bytes(order);
    var msg = new Msg("ORDERS.order.created", data);
    await js.PublishAsync(msg);
    return Results.Accepted();
});

app.Run();

The endpoint returns HTTP 202, signalling that the order is accepted for asynchronous processing.

Building a consumer microservice

A separate service can pull messages from the same stream. Minimal APIs are still useful for health checks and admin endpoints, but the core logic lives in a background worker.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<OrderProcessor>();
var app = builder.Build();
app.MapGet("/health", () => Results.Ok("Consumer running"));
app.Run();

public class OrderProcessor : BackgroundService {
    private readonly IConnection _nc;
    public OrderProcessor(IConnection nc) { _nc = nc; }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        var js = _nc.CreateJetStreamContext();
        var pull = await js.CreatePullSubscriptionAsync("ORDERS.order.created", "order‑processor");
        while (!stoppingToken.IsCancellationRequested) {
            var msgs = await pull.FetchAsync(10, 1000, stoppingToken);
            foreach (var msg in msgs) {
                var order = JsonSerializer.Deserialize<OrderDto>(msg.Data);
                // Process order (e.g., reserve inventory)
                msg.Ack();
            }
        }
    }
}

public record OrderDto(Guid Id, string CustomerId, decimal Total);

The pull‑based consumer fetches up to 10 messages every second, acknowledges each after successful processing, and automatically retries on failure because JetStream retains un‑acked messages.

Practical deployment considerations

Containerise both services with Docker. The official NATS image can be started with a persistent volume for JetStream state:

docker run -d \
  -p 4222:4222 \
  -p 8222:8222 \
  -v nats-data:/data \
  nats:latest \
  -js -sd /data

Set the environment variable NATS_URL=nats://nats:4222 in your .NET containers and use Docker Compose to orchestrate the three services (API, consumer, NATS). Health checks can be expressed as Minimal API endpoints, allowing Kubernetes liveness probes to monitor each pod.

Security best practices include enabling TLS on the NATS server, using JWT‑based user authentication, and restricting stream access with NATS permissions. The .NET client supports TLS by adding .WithTlsOptions(...) to the connection builder.

Testing and observability

Integration tests can spin up an in‑memory NATS server using the NATS.Server NuGet package. Verify that a POST request to /orders results in a message stored in the "ORDERS" stream.

using var nats = new NatsServer();
var client = new HttpClient { BaseAddress = new Uri("http://localhost:5000") };
var response = await client.PostAsJsonAsync("/orders", new { Id = Guid.NewGuid(), CustomerId = "C123", Total = 99.95M });
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);

For observability, expose JetStream metrics via the NATS monitoring endpoint (port 8222) and scrape them with Prometheus. Correlate HTTP request IDs with JetStream message IDs using a custom header, which simplifies root‑cause analysis in distributed tracing tools like Jaeger.

Conclusion

By leveraging .NET 8 Minimal APIs, you write concise, testable endpoints that focus on business intent rather than boilerplate. Pairing them with NATS JetStream gives you durable, high‑throughput messaging without the operational overhead of traditional brokers. The result is a microservice ecosystem that scales horizontally, recovers gracefully from failures, and stays easy to maintain.

Sources

Microsoft Docs – .NET 8 Minimal APIs
NATS Documentation – JetStream Overview
InfoQ – Event‑Driven Architecture trends 2023

Author: Mahmut Sarıkaya — sarikayadev.com

Tags: #.NET 8 #Minimal APIs #NATS JetStream #event‑driven architecture #microservices
Share:
M

Written by

Mahmut Sarıkaya

Software Developer

Comments

No comments yet. Be the first to share your thoughts!

Leave a Comment

6 + 1 =