Microservices and Architecture

Microservices architecture: API gateways, inter-service communication, resilience patterns, distributed data, service discovery, and migration strategies.

Microservices are an architectural style where an application is built as a collection of small, independent services that communicate over network protocols. Each service is self-contained, focuses on a specific business capability, and can be deployed independently.

Advantages:

  • Independent Deployment: Services can be deployed without affecting others
  • Technology Flexibility: Each service can use different tech stacks
  • Scalability: Scale individual services based on demand
  • Fault Isolation: Failures in one service don't crash the entire system
  • Team Autonomy: Small teams can own and develop services independently
  • Faster Development: Parallel development across multiple teams

Disadvantages:

  • Complexity: Distributed systems are inherently complex
  • Network Latency: Inter-service communication overhead
  • Data Consistency: Maintaining consistency across services is challenging
  • Testing Difficulty: End-to-end testing becomes more complex
  • Operational Overhead: More services to monitor, deploy, and maintain
  • Distributed Transactions: Harder to implement ACID transactions

Example in .NET Core:

// Product Service
public class ProductService
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public async Task GetProductWithInventory(int productId)
    {
        var product = await _productRepository.GetByIdAsync(productId);
        
        // Call inventory microservice
        var client = _httpClientFactory.CreateClient("InventoryService");
        var inventory = await client.GetFromJsonAsync($"/api/inventory/{productId}");
        
        product.StockLevel = inventory.Quantity;
        return product;
    }
}

Monolithic Architecture:

  • Single, unified codebase and deployment unit
  • All components tightly coupled within one application
  • Shared database for all modules
  • Scaling requires scaling the entire application
  • Simple deployment but limited flexibility

Microservices Architecture:

  • Multiple independent services
  • Loosely coupled with clear boundaries
  • Each service has its own database (database per service pattern)
  • Individual services can be scaled independently
  • Complex deployment but high flexibility

Comparison Table:

AspectMonolithicMicroservices
DeploymentSingle unitMultiple independent services
DatabaseShared databaseDatabase per service
ScalingVertical (entire app)Horizontal (per service)
TechnologySingle stackPolyglot architecture
DevelopmentSimple initiallyComplex from start
Team StructureSingle large teamMultiple small teams

Monolithic Example (.NET Core):

// All in one application
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped();
        services.AddScoped();
        services.AddScoped();
        services.AddScoped();
        // All services in one application
    }
}

Microservices Example (.NET Core):

// Product Service - Separate application
public class ProductServiceStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped();
    }
}

// Order Service - Separate application
public class OrderServiceStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped();
        services.AddHttpClient("ProductService", c => 
            c.BaseAddress = new Uri("http://product-service"));
    }
}

The API Gateway pattern provides a single entry point for all clients to access microservices. It acts as a reverse proxy, routing requests to appropriate microservices and aggregating responses.

Key Responsibilities:

  • Request routing and composition
  • Authentication and authorization
  • Rate limiting and throttling
  • Load balancing
  • Protocol translation
  • Response aggregation
  • Caching

Implementation with Ocelot in .NET Core:

// Install: Install-Package Ocelot

// ocelot.json configuration
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/products/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "product-service",
          "Port": 5001
        }
      ],
      "UpstreamPathTemplate": "/products/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ]
    },
    {
      "DownstreamPathTemplate": "/api/orders/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "order-service",
          "Port": 5002
        }
      ],
      "UpstreamPathTemplate": "/orders/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer"
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000"
  }
}

// Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        
        builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
        builder.Services.AddOcelot(builder.Configuration);
        
        var app = builder.Build();
        app.UseOcelot().Wait();
        app.Run();
    }
}

Custom API Gateway:

[ApiController]
[Route("api/gateway")]
public class ApiGatewayController : ControllerBase
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public ApiGatewayController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    [HttpGet("product/{id}/details")]
    public async Task GetProductDetails(int id)
    {
        var productClient = _httpClientFactory.CreateClient("ProductService");
        var inventoryClient = _httpClientFactory.CreateClient("InventoryService");
        var reviewClient = _httpClientFactory.CreateClient("ReviewService");
        
        // Aggregate responses from multiple services
        var productTask = productClient.GetFromJsonAsync($"/api/products/{id}");
        var inventoryTask = inventoryClient.GetFromJsonAsync($"/api/inventory/{id}");
        var reviewsTask = reviewClient.GetFromJsonAsync<List>($"/api/reviews/product/{id}");
        
        await Task.WhenAll(productTask, inventoryTask, reviewsTask);
        
        return Ok(new {
            Product = productTask.Result,
            Inventory = inventoryTask.Result,
            Reviews = reviewsTask.Result
        });
    }
}

Related Resources

Inter-service communication can be synchronous or asynchronous.

Synchronous Communication (Request/Response):

1. HTTP/REST with HttpClient:

// Startup.cs
services.AddHttpClient("OrderService", c =>
{
    c.BaseAddress = new Uri("http://order-service:5000");
    c.DefaultRequestHeaders.Add("Accept", "application/json");
});

// Service implementation
public class ProductService
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public ProductService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public async Task CheckOrderStatus(int orderId)
    {
        var client = _httpClientFactory.CreateClient("OrderService");
        var response = await client.GetAsync($"/api/orders/{orderId}/status");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync();
    }
}

2. gRPC (High-performance RPC):

// order.proto
syntax = "proto3";

service OrderService {
  rpc GetOrder (OrderRequest) returns (OrderResponse);
  rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
}

message OrderRequest {
  int32 order_id = 1;
}

message OrderResponse {
  int32 order_id = 1;
  string status = 2;
  double total = 3;
}
// gRPC Server
public class OrderServiceImpl : OrderService.OrderServiceBase
{
    public override async Task GetOrder(OrderRequest request, ServerCallContext context)
    {
        var order = await _orderRepository.GetByIdAsync(request.OrderId);
        return new OrderResponse
        {
            OrderId = order.Id,
            Status = order.Status,
            Total = order.Total
        };
    }
}

// gRPC Client
public class ProductService
{
    private readonly OrderService.OrderServiceClient _orderClient;
    
    public async Task GetOrderDetails(int orderId)
    {
        var request = new OrderRequest { OrderId = orderId };
        return await _orderClient.GetOrderAsync(request);
    }
}

Asynchronous Communication (Message-based):

1. RabbitMQ:

// Install: Install-Package RabbitMQ.Client

// Message Publisher
public class OrderCreatedPublisher
{
    private readonly IConnection _connection;
    
    public void PublishOrderCreated(OrderCreatedEvent orderEvent)
    {
        using var channel = _connection.CreateModel();
        
        channel.ExchangeDeclare("orders", ExchangeType.Topic, durable: true);
        
        var message = JsonSerializer.Serialize(orderEvent);
        var body = Encoding.UTF8.GetBytes(message);
        
        channel.BasicPublish(
            exchange: "orders",
            routingKey: "order.created",
            basicProperties: null,
            body: body);
    }
}

// Message Consumer
public class InventoryService : BackgroundService
{
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var channel = _connection.CreateModel();
        channel.QueueDeclare("inventory-queue", durable: true, exclusive: false);
        channel.QueueBind("inventory-queue", "orders", "order.created");
        
        var consumer = new EventingBasicConsumer(channel);
        consumer.Received += (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var message = Encoding.UTF8.GetString(body);
            var orderEvent = JsonSerializer.Deserialize(message);
            
            // Process the order
            ProcessOrder(orderEvent);
        };
        
        channel.BasicConsume("inventory-queue", autoAck: true, consumer);
        return Task.CompletedTask;
    }
}

2. Azure Service Bus:

// Install: Install-Package Azure.Messaging.ServiceBus

public class ServiceBusPublisher
{
    private readonly ServiceBusClient _client;
    private readonly ServiceBusSender _sender;
    
    public async Task PublishAsync(T message)
    {
        var messageBody = JsonSerializer.Serialize(message);
        var serviceBusMessage = new ServiceBusMessage(messageBody);
        await _sender.SendMessageAsync(serviceBusMessage);
    }
}

public class ServiceBusConsumer : BackgroundService
{
    private readonly ServiceBusProcessor _processor;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _processor.ProcessMessageAsync += MessageHandler;
        _processor.ProcessErrorAsync += ErrorHandler;
        
        await _processor.StartProcessingAsync(stoppingToken);
    }
    
    private async Task MessageHandler(ProcessMessageEventArgs args)
    {
        var body = args.Message.Body.ToString();
        var order = JsonSerializer.Deserialize(body);
        
        // Process message
        await ProcessOrderAsync(order);
        await args.CompleteMessageAsync(args.Message);
    }
}

The Circuit Breaker pattern prevents an application from repeatedly trying to execute an operation that's likely to fail, allowing it to continue without waiting for the fault to be fixed or wasting CPU cycles.

States:

  • Closed: Requests flow normally, failures are counted
  • Open: Requests fail immediately without attempting the call
  • Half-Open: Limited requests are allowed to test if the issue is resolved

Implementation with Polly:

// Install: Install-Package Polly
// Install: Install-Package Microsoft.Extensions.Http.Polly

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("OrderService", c =>
    {
        c.BaseAddress = new Uri("http://order-service:5000");
    })
    .AddPolicyHandler(GetCircuitBreakerPolicy())
    .AddPolicyHandler(GetRetryPolicy());
}

private IAsyncPolicy GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(
            handledEventsAllowedBeforeBreaking: 3,
            durationOfBreak: TimeSpan.FromSeconds(30),
            onBreak: (result, timespan) =>
            {
                // Log circuit breaker opened
                Console.WriteLine($"Circuit breaker opened for {timespan.TotalSeconds}s");
            },
            onReset: () =>
            {
                // Log circuit breaker reset
                Console.WriteLine("Circuit breaker reset");
            },
            onHalfOpen: () =>
            {
                // Log circuit breaker half-open
                Console.WriteLine("Circuit breaker half-open");
            });
}

private IAsyncPolicy 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");
            });
}

Custom Circuit Breaker:

public class CircuitBreaker
{
    private readonly int _threshold;
    private readonly TimeSpan _timeout;
    private int _failureCount;
    private DateTime _lastFailureTime;
    private CircuitBreakerState _state = CircuitBreakerState.Closed;
    
    public async Task ExecuteAsync(Func<Task> operation)
    {
        if (_state == CircuitBreakerState.Open)
        {
            if (DateTime.UtcNow - _lastFailureTime > _timeout)
            {
                _state = CircuitBreakerState.HalfOpen;
            }
            else
            {
                throw new CircuitBreakerOpenException();
            }
        }
        
        try
        {
            var result = await operation();
            
            if (_state == CircuitBreakerState.HalfOpen)
            {
                _state = CircuitBreakerState.Closed;
                _failureCount = 0;
            }
            
            return result;
        }
        catch (Exception)
        {
            _failureCount++;
            _lastFailureTime = DateTime.UtcNow;
            
            if (_failureCount >= _threshold)
            {
                _state = CircuitBreakerState.Open;
            }
            
            throw;
        }
    }
}

public enum CircuitBreakerState
{
    Closed,
    Open,
    HalfOpen
}