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
-
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
-
How do you handle distributed transactions?
- Saga pattern (orchestration or choreography), eventual consistency, compensating transactions, idempotency
-
How do you maintain data consistency across services?
- Event-driven architecture, eventual consistency, saga pattern, event sourcing
-
What's the difference between orchestration and choreography?
- Orchestration: Central coordinator (saga orchestrator) controls workflow
- Choreography: Services react to events independently, no central control
-
How do you handle service discovery?
- Client-side: Netflix Eureka, Consul
- Server-side: Kubernetes services, AWS ELB
- Service mesh: Istio, Linkerd
-
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
-
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