Microservices Patterns

Master microservices architecture patterns including service boundaries, communication strategies, data management, and resilience patterns for building scalable distributed systems

Architecture
Microservices
Distributed Systems
Patterns
Scalability
Resilience
35 min read

Overview

Microservices architecture is crucial for building scalable, resilient systems. Understanding the patterns, trade-offs, and implementation strategies is essential for senior engineering roles. This topic frequently appears in system design interviews.

Core Concepts

Defining Service Boundaries

Service boundaries should align with business capabilities using Domain-Driven Design principles.

// BAD - Technology-based boundaries
- DatabaseService
- EmailService
- LoggingService

// GOOD - Business capability boundaries
- OrderService (manages orders)
- CustomerService (manages customers)
- InventoryService (manages stock)
- PaymentService (processes payments)
- NotificationService (sends notifications)

Bounded Contexts

Each service represents a bounded context with its own domain model:

// In Order Service
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address ShippingAddress { get; set; }
    // Order-specific customer data
}

// In Customer Service (different model!)
public class Customer
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public List<Address> Addresses { get; set; }
    public CustomerType Type { get; set; }
    public DateTime CreatedAt { get; set; }
    // Full customer management data
}

Communication Patterns

Pattern 1: Synchronous Communication (REST/gRPC)

// Order Service calls Product Service synchronously
public class OrderService
{
    private readonly HttpClient _productClient;
    private readonly ILogger<OrderService> _logger;

    public async Task<Result<Order>> CreateOrderAsync(CreateOrderRequest request)
    {
        try
        {
            // Call Product Service to validate and get prices
            var response = await _productClient.PostAsJsonAsync(
                "/api/products/validate",
                request.ProductIds);

            if (!response.IsSuccessStatusCode)
                return Result<Order>.Failure("Failed to validate products");

            var products = await response.Content.ReadFromJsonAsync<List<ProductDto>>();

            // Create order with validated data
            var order = new Order
            {
                CustomerId = request.CustomerId,
                Items = products.Select(p => new OrderItem
                {
                    ProductId = p.Id,
                    Price = p.Price,
                    Quantity = request.Items.First(i => i.ProductId == p.Id).Quantity
                }).ToList()
            };

            // Save to database
            await _dbContext.Orders.AddAsync(order);
            await _dbContext.SaveChangesAsync();

            return Result<Order>.Success(order);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to communicate with Product Service");
            return Result<Order>.Failure("Service temporarily unavailable");
        }
    }
}

// Using Refit for cleaner HTTP clients
public interface IProductServiceApi
{
    [Post("/api/products/validate")]
    Task<ApiResponse<List<ProductDto>>> ValidateProductsAsync([Body] List<int> productIds);

    [Get("/api/products/{id}")]
    Task<ProductDto> GetProductAsync(int id);
}

// Registration
builder.Services
    .AddRefitClient<IProductServiceApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri(builder.Configuration["Services:ProductService"]))
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

Pattern 2: Asynchronous Communication (Messaging)

// Publishing events
public class OrderService
{
    private readonly IMessageBus _messageBus;

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        var order = new Order { /* ... */ };
        await _dbContext.Orders.AddAsync(order);
        await _dbContext.SaveChangesAsync();

        // Publish event - fire and forget
        await _messageBus.PublishAsync(new OrderCreatedEvent
        {
            OrderId = order.Id,
            CustomerId = order.CustomerId,
            Items = order.Items.Select(i => new OrderItemDto
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity
            }).ToList(),
            CreatedAt = DateTime.UtcNow
        });

        return order;
    }
}

// Consuming events in Inventory Service
public class OrderCreatedEventHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IInventoryService _inventoryService;
    private readonly ILogger<OrderCreatedEventHandler> _logger;

    public async Task HandleAsync(OrderCreatedEvent @event)
    {
        _logger.LogInformation("Processing order {OrderId} for inventory", @event.OrderId);

        try
        {
            // Reserve inventory for each item
            foreach (var item in @event.Items)
            {
                await _inventoryService.ReserveStockAsync(item.ProductId, item.Quantity);
            }

            _logger.LogInformation("Inventory reserved for order {OrderId}", @event.OrderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to reserve inventory for order {OrderId}", @event.OrderId);
            // Could publish a compensation event here
        }
    }
}

// RabbitMQ implementation
public class RabbitMqMessageBus : IMessageBus
{
    private readonly IConnection _connection;
    private readonly IModel _channel;

    public RabbitMqMessageBus(IConfiguration configuration)
    {
        var factory = new ConnectionFactory
        {
            HostName = configuration["RabbitMQ:Host"],
            UserName = configuration["RabbitMQ:Username"],
            Password = configuration["RabbitMQ:Password"]
        };

        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();
    }

    public Task PublishAsync<T>(T message) where T : class
    {
        var eventName = typeof(T).Name;
        _channel.ExchangeDeclare(exchange: "events", type: ExchangeType.Topic);

        var json = JsonSerializer.Serialize(message);
        var body = Encoding.UTF8.GetBytes(json);

        _channel.BasicPublish(
            exchange: "events",
            routingKey: eventName,
            basicProperties: null,
            body: body);

        return Task.CompletedTask;
    }
}

Data Management Patterns

Database per Service

Each service owns its database - no sharing.

// Order Service - OrderDbContext
public class OrderDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderItem> OrderItems { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new OrderConfiguration());
        modelBuilder.ApplyConfiguration(new OrderItemConfiguration());
    }
}

// Product Service - ProductDbContext
public class ProductDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new ProductConfiguration());
        modelBuilder.ApplyConfiguration(new CategoryConfiguration());
    }
}

// Different connection strings
{
  "ConnectionStrings": {
    "OrderDatabase": "Server=order-db;Database=Orders;",
    "ProductDatabase": "Server=product-db;Database=Products;",
    "InventoryDatabase": "Server=inventory-db;Database=Inventory;"
  }
}

Handling Distributed Transactions: Saga Pattern

// Saga Orchestrator for Order Placement
public class OrderPlacementSaga
{
    private readonly IOrderService _orderService;
    private readonly IPaymentService _paymentService;
    private readonly IInventoryService _inventoryService;
    private readonly IShippingService _shippingService;

    public async Task<SagaResult> ExecuteAsync(PlaceOrderCommand command)
    {
        var sagaState = new OrderSagaState { OrderId = Guid.NewGuid() };

        try
        {
            // Step 1: Create order
            sagaState.Order = await _orderService.CreateOrderAsync(command);
            sagaState.StepsCompleted.Add("OrderCreated");

            // Step 2: Reserve inventory
            sagaState.ReservationId = await _inventoryService.ReserveItemsAsync(
                sagaState.Order.Items);
            sagaState.StepsCompleted.Add("InventoryReserved");

            // Step 3: Process payment
            sagaState.PaymentId = await _paymentService.ChargeAsync(
                command.CustomerId,
                sagaState.Order.Total);
            sagaState.StepsCompleted.Add("PaymentProcessed");

            // Step 4: Create shipment
            sagaState.ShipmentId = await _shippingService.CreateShipmentAsync(
                sagaState.Order.Id,
                command.ShippingAddress);
            sagaState.StepsCompleted.Add("ShipmentCreated");

            // Success!
            await _orderService.ConfirmOrderAsync(sagaState.Order.Id);
            return SagaResult.Success(sagaState.Order.Id);
        }
        catch (Exception ex)
        {
            // Compensate - rollback completed steps in reverse order
            await CompensateAsync(sagaState);
            return SagaResult.Failure(ex.Message);
        }
    }

    private async Task CompensateAsync(OrderSagaState state)
    {
        // Reverse order of operations
        if (state.StepsCompleted.Contains("ShipmentCreated"))
            await _shippingService.CancelShipmentAsync(state.ShipmentId);

        if (state.StepsCompleted.Contains("PaymentProcessed"))
            await _paymentService.RefundAsync(state.PaymentId);

        if (state.StepsCompleted.Contains("InventoryReserved"))
            await _inventoryService.ReleaseReservationAsync(state.ReservationId);

        if (state.StepsCompleted.Contains("OrderCreated"))
            await _orderService.CancelOrderAsync(state.Order.Id);
    }
}

Resilience Patterns

Circuit Breaker

// Using Polly for circuit breaker
public static class ResiliencePolicies
{
    public static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
    {
        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .CircuitBreakerAsync(
                handledEventsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromSeconds(30),
                onBreak: (outcome, timespan) =>
                {
                    Console.WriteLine($"Circuit breaker opened for {timespan.TotalSeconds}s");
                },
                onReset: () =>
                {
                    Console.WriteLine("Circuit breaker reset");
                });
    }

    public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
    {
        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt =>
                    TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    Console.WriteLine($"Retry {retryCount} after {timespan.TotalSeconds}s");
                });
    }
}

// Apply policies to HTTP clients
builder.Services
    .AddHttpClient<IProductServiceClient, ProductServiceClient>(client =>
    {
        client.BaseAddress = new Uri(builder.Configuration["Services:ProductService"]);
        client.Timeout = TimeSpan.FromSeconds(10);
    })
    .AddPolicyHandler(ResiliencePolicies.GetRetryPolicy())
    .AddPolicyHandler(ResiliencePolicies.GetCircuitBreakerPolicy());

Interview Tips

Tip 1: Always discuss trade-offs. Microservices solve distribution problems but introduce complexity. Be ready to explain when monoliths are better.

Tip 2: Focus on business value. Don't split services just for technical reasons - align with business capabilities and team structure.

Tip 3: Know the fallacies of distributed computing: network is reliable, latency is zero, bandwidth is infinite, network is secure, etc.

Common Interview Questions

  1. When should you use microservices vs a monolith?

    • Microservices: Large teams, need independent scaling, different technology requirements, complex domain
    • Monolith: Small team, simple domain, tight coupling acceptable, faster development initially
  2. How do you handle distributed transactions?

    • Saga pattern (orchestration or choreography), eventual consistency, compensating transactions, idempotency
  3. How do you maintain data consistency across services?

    • Event-driven architecture, eventual consistency, saga pattern, event sourcing
  4. What's the difference between orchestration and choreography?

    • Orchestration: Central coordinator (saga orchestrator) controls workflow
    • Choreography: Services react to events independently, no central control
  5. How do you handle service discovery?

    • Client-side: Netflix Eureka, Consul
    • Server-side: Kubernetes services, AWS ELB
    • Service mesh: Istio, Linkerd
  6. What's an API Gateway and why use it?

    • Single entry point for clients, handles routing, authentication, rate limiting, request aggregation
    • Examples: Ocelot, YARP, Kong, AWS API Gateway
  7. How do you test microservices?

    • Unit tests: Individual service logic
    • Integration tests: Service with its database
    • Contract tests: API contracts between services
    • E2E tests: Full workflow across services