This episode covers microservice architecture in .NET: cloud-native and container principles, minimal APIs and gRPC, resilience patterns with Polly, messaging with MassTransit, and the observability patterns every microservice must have.

A giant monolithic application eventually becomes hard to manage: one broken feature can hold up the release of the entire application. Microservice architecture breaks the application into small, independent services — each can be developed, deployed, and scaled on its own.
But microservices aren't free. Inter-service networks can fail, finding services becomes a problem, and monitoring dozens of processes is harder. .NET provides a complete ecosystem for handling all of that.
Episode 17 covers cloud-native principles, minimal APIs and gRPC, resilience patterns, messaging with MassTransit, and the observability every service must have.
Each microservice should have:
With these principles, teams can move fast and failures stay isolated to a single service.
For simple HTTP APIs, minimal APIs are far more concise than controllers:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/api/produk/{id:int}", (int id) =>
Results.Ok(new { Id = id, Nama = "Kopi Gayo" }));
app.MapPost("/api/produk", (Produk produk) =>
Results.Created($"/api/produk/{produk.Id}", produk));
app.Run();Lambda handlers in MapGet and MapPost receive parameters that are automatically bound from the route and body. For internal services that need high performance, choose gRPC, which uses HTTP/2 and binary serialization:
public class ProdukService : ProdukServiceBase
{
public override Task<ProdukReply> GetProduk(
ProdukRequest request, ServerCallContext context)
{
return Task.FromResult(new ProdukReply
{
Id = request.Id,
Nama = "Kopi Gayo"
});
}
}A rule of thumb: gRPC for internal inter-service communication (called service-to-service), REST/minimal API for external consumption like browsers and mobile.
In a networked world, failure is normal, not the exception. Polly is a resilience library for .NET — retry with backoff, circuit breaker, and timeout:
dotnet add src/OrderService package Microsoft.Extensions.Http.Resiliencebuilder.Services.AddHttpClient<OrderClient>(c =>
c.BaseAddress = new Uri("http://payment-service:8080"))
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.CircuitBreaker.MinimumThroughput = 10;
});AddStandardResilienceHandler combines retry, circuit breaker, and timeout in one pipeline. The service discovery pattern ensures OrderClient always finds the correct payment-service address — in Kubernetes, the service name already acts as a DNS address that resolves automatically.
When two services must work together without waiting on each other, use messaging. MassTransit is a popular messaging bus that supports RabbitMQ and Azure Service Bus:
public class PesananDibuat
{
public Guid OrderId { get; set; }
public decimal Total { get; set; }
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMassTransit(x =>
{
x.UsingRabbitMq((ctx, cfg) =>
cfg.Host("rabbitmq://localhost"));
});
var bus = app.Services.GetRequiredService<IPublishEndpoint>();
await bus.Publish(new PesananDibuat
{
OrderId = Guid.NewGuid(),
Total = 250_000m
});IPublishEndpoint.Publish sends an event to the bus without waiting for a consumer. Other services can subscribe to the same event, so services are connected through events, not direct calls.
Before using a bus, make sure the appropriate transport package is installed. For RabbitMQ, dotnet add package MassTransit.RabbitMQ adds the transport used by the code above; for Azure Service Bus, replace it with MassTransit.Azure.ServiceBus.Core. The cfg.Host("rabbitmq://localhost") configuration is enough for local development.
Every microservice must emit three signals: logs, metrics, and traces. Without these, tracing a request that passes through ten services is nearly impossible. The correlation ID concept links a single request across all services:
app.Use(async (context, next) =>
{
var traceId = Activity.Current?.TraceId.ToString()
?? Guid.NewGuid().ToString();
context.Response.Headers["X-Trace-Id"] = traceId;
await next();
});The same X-Trace-Id header is propagated to outgoing calls, so logs from different services can be joined by the same ID. We'll build the complete observability tooling in episode 20.
Key takeaways:
In the next episode 18 we streamline the development process: modern tooling and build automation — the dotnet CLI and MSBuild, SDK-style projects, build automation with GitHub Actions, source generators and Roslyn analyzers, and code formatting and reproducible builds.