Microservices

Intro

Microservices are an architecture style where a system is split into independently deployable services, each aligned to a business capability and owning its own data. They matter because they let teams release changes independently, scale only hot paths, and use technology choices per domain when needed. You usually reach for microservices when team count grows, deployment independence becomes a bottleneck, and domains have different scaling or availability needs. The tradeoff is distributed-systems complexity: network latency, partial failures, eventual consistency, and heavier operational tooling.

Core principles

flowchart LR
    Client[Client App] --> APIGW[API Gateway]
    APIGW --> Orders[Orders Service]
    APIGW --> Payments[Payments Service]

    Orders --> OrdersDb[(Orders DB)]
    Inventory[Inventory Service] --> InventoryDb[(Inventory DB)]
    Payments --> PaymentsDb[(Payments DB)]
    Shipping[Shipping Service] --> ShippingDb[(Shipping DB)]

    Orders -- REST or gRPC --> Inventory
    Orders -- OrderCreated --> Broker[(Event Broker)]
    Broker -- OrderCreated --> Inventory
    Broker -- PaymentCaptured --> Shipping

Communication patterns

Synchronous calls

Asynchronous messaging

Rule of thumb

.NET implementation notes

Service boundaries with ASP.NET Core Minimal APIs

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
    .AddCheck("database", () => HealthCheckResult.Healthy(), tags: new[] { "ready" });

var app = builder.Build();

app.MapGet("/orders/{id:guid}", async (Guid id, IOrderRepository repo) =>
{
    var order = await repo.GetByIdAsync(id);
    return order is null ? Results.NotFound() : Results.Ok(order);
});

app.MapPost("/orders", async (CreateOrderRequest request, IOrderService service) =>
{
    var result = await service.CreateAsync(request);
    return Results.Created($"/orders/{result.OrderId}", result);
});

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live")
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live") || check.Tags.Contains("ready")
});

app.Run();
dotnet add package Microsoft.AspNetCore.OpenApi

Service discovery

Health checks and readiness

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orders-service
  template:
    metadata:
      labels:
        app: orders-service
    spec:
      containers:
        - name: orders
          image: myregistry/orders-service:1.0.0
          env:
            - name: ASPNETCORE_HTTP_PORTS
              value: "8080"
          ports:
            - containerPort: 8080
          startupProbe:
            httpGet:
              path: /health/live
              port: 8080
            periodSeconds: 5
            failureThreshold: 12
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080

Containers and orchestration

Microservices vs monolith vs modular monolith

Dimension Monolith Modular Monolith Microservices
Deployments Single unit Single unit with strict module boundaries Independent service deployments
Team model Shared ownership Team ownership by module Team ownership by service
Data model Shared database Shared database with modular access rules Database per service
Runtime calls In-process In-process Network calls
Operational complexity Low Low to medium High
Best fit Small team, early product Growing product, clear domains, limited ops capacity Large org, high release velocity, independent scaling needs

Monolith Architecture is usually the best starting point when boundaries are still evolving and operational maturity is limited.

Migration path: start monolith, then extract

  1. Start with a modular monolith and enforce boundaries internally.
  2. Measure release bottlenecks, scale asymmetry, and team contention.
  3. Extract one bounded context with clear API contracts and isolated data ownership.
  4. Keep extraction incremental; avoid big-bang rewrites.
  5. Repeat only when another boundary has clear business pressure.

Pitfalls

1) Distributed monolith

2) Data consistency across services

3) Operational complexity explosion

4) Network is not reliable

Questions

References


Whats next