Mục tiêu: Hiểu microservices principles, decomposition strategies, và communication patterns (sync/async).
1. Microservices Principles
┌────────────────────────────────────────────────────────────┐
│ Microservices Principles │
├────────────────────────────────────────────────────────────┤
│ 1. Single Responsibility: 1 service = 1 business capability│
│ 2. Autonomy: deploy, scale, fail independently │
│ 3. Decentralized Data: mỗi service có DB riêng │
│ 4. Smart Endpoints, Dumb Pipes: logic trong service │
│ 5. Design for Failure: circuit breaker, retry, fallback │
│ 6. Infrastructure Automation: CI/CD, containers │
│ 7. Evolutionary Design: thay thế > refactor │
└────────────────────────────────────────────────────────────┘
Decomposition Strategies
1. By Business Capability (phổ biến nhất):
eCommerce → Orders, Catalog, Payment, Shipping, Notification
2. By Subdomain (DDD):
Core: Orders (competitive advantage)
Supporting: Catalog (cần nhưng không unique)
Generic: Notification (có thể outsource)
3. By Verb/Use Case:
Search Service, Checkout Service, Reporting Service
4. By Data Ownership:
Customer Service (owns customer data)
Product Service (owns product data)
2. Synchronous Communication
REST
csharp// Order Service gọi Catalog Service
public class CatalogServiceClient(HttpClient httpClient) : ICatalogServiceClient
{
public async Task<ProductDto?> GetProductAsync(Guid productId, CancellationToken ct)
{
var response = await httpClient.GetAsync($"/api/products/{productId}", ct);
if (response.StatusCode == HttpStatusCode.NotFound) return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ProductDto>(ct);
}
}
// Register với IHttpClientFactory
builder.Services.AddHttpClient<ICatalogServiceClient, CatalogServiceClient>(client =>
{
client.BaseAddress = new Uri("https://catalog-service");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddStandardResilienceHandler(); // .NET 8: built-in Polly
gRPC (Service-to-Service)
protobuf// catalog.proto syntax = "proto3"; package catalog; service CatalogService { rpc GetProduct (GetProductRequest) returns (ProductResponse); rpc GetProducts (GetProductsRequest) returns (stream ProductResponse); rpc CheckStock (CheckStockRequest) returns (StockResponse); } message GetProductRequest { string product_id = 1; } message ProductResponse { string id = 1; string name = 2; double price = 3; int32 stock = 4; }
csharp// .NET gRPC Server
public class CatalogGrpcService(CatalogDbContext db) : CatalogService.CatalogServiceBase
{
public override async Task<ProductResponse> GetProduct(
GetProductRequest request, ServerCallContext context)
{
var product = await db.Products.FindAsync(Guid.Parse(request.ProductId));
if (product is null)
throw new RpcException(new Status(StatusCode.NotFound, "Product not found"));
return new ProductResponse
{
Id = product.Id.ToString(),
Name = product.Name,
Price = (double)product.Price,
Stock = product.Stock
};
}
}
// .NET gRPC Client
builder.Services.AddGrpcClient<CatalogService.CatalogServiceClient>(options =>
{
options.Address = new Uri("https://catalog-service:5001");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true // Dev only!
});
REST vs gRPC
| Feature | REST | gRPC |
|---|---|---|
| Protocol | HTTP/1.1, JSON | HTTP/2, Protobuf |
| Performance | ~100μs overhead | ~10μs overhead |
| Streaming | SSE (one-way) | Bidirectional |
| Browser | ✅ Native | ⚠️ grpc-web |
| Contract | OpenAPI (loose) | .proto (strict) |
| Use case | Public APIs | Service-to-service |
3. Asynchronous Communication
Message Queue (Point-to-Point)
Producer → ┌────────────┐ → Consumer
│ Queue │
│ ──────────│ 1 message → 1 consumer
└────────────┘
Pub/Sub (One-to-Many)
Publisher → ┌────────────┐ → Subscriber A
│ Topic │ → Subscriber B
│ │ → Subscriber C
└────────────┘
1 message → N subscribers
Event Streaming (Kafka-style)
Producer → ┌────────────────────────────────┐
│ Topic Partition │
│ [msg1][msg2][msg3][msg4][msg5] │
└──┬─────────────────────────────┘
│
Consumer Group A: reads from offset 3
Consumer Group B: reads from offset 1 (different pace)
→ Messages RETAINED (replay possible)
4. Resilience Patterns (Polly / .NET 8 Resilience)
Circuit Breaker
csharp// .NET 8 built-in resilience
builder.Services.AddHttpClient<ICatalogClient, CatalogClient>(client =>
{
client.BaseAddress = new Uri("https://catalog-service");
})
.AddResilienceHandler("catalog", builder =>
{
// Retry: 3 attempts with exponential backoff
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => r.StatusCode == HttpStatusCode.ServiceUnavailable)
.Handle<HttpRequestException>()
});
// Circuit Breaker: open after 5 failures
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
SamplingDuration = TimeSpan.FromSeconds(30),
FailureRatio = 0.5,
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(30)
});
// Timeout
builder.AddTimeout(TimeSpan.FromSeconds(5));
});
// Circuit Breaker states:
// CLOSED → requests flowing normally
// OPEN → requests immediately rejected (fail fast)
// HALF-OPEN → allow 1 test request to check if service recovered
Health Checks
csharp// Producer service
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString, name: "database")
.AddRedis(redisConnection, name: "cache")
.AddRabbitMQ(rabbitConnection, name: "messaging")
.AddUrlGroup(new Uri("https://catalog-service/health"), name: "catalog-service");
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false // Chỉ check app running
});
5. Service Discovery
csharp// Option 1: DNS-based (Kubernetes / Docker Compose)
// Service name = DNS name
builder.Services.AddHttpClient("catalog", client =>
{
client.BaseAddress = new Uri("http://catalog-service:8080"); // DNS resolve
});
// Option 2: Azure Service Discovery (.NET 9 / Aspire)
builder.Services.AddServiceDiscovery();
builder.Services.AddHttpClient<ICatalogClient>(client =>
{
client.BaseAddress = new Uri("https+http://catalog"); // Resolved by Aspire
});
// Option 3: Configuration-based
builder.Services.AddHttpClient<ICatalogClient>(client =>
{
client.BaseAddress = new Uri(
builder.Configuration["Services:Catalog:BaseUrl"]!);
});
6. Ví Dụ: Logistics Tracking System
┌──────────┐ ┌───────────┐ ┌──────────────┐
│ Order │───→│ Routing │───→│ Tracking │
│ Service │ │ Service │ │ Service │
│ │ │ │ │ │
│ Tạo đơn │ │ Tính route│ │ GPS realtime │
│ hàng │ │ tối ưu │ │ tracking │
└──────┬───┘ └─────┬─────┘ └──────┬───────┘
│ │ │
│ Events │ Events │
└──────────────┴─────────────────┘
│
┌───────▼────────┐
│ Notification │
│ Service │
│ │
│ SMS/Email/Push │
└────────────────┘
Communication:
Order → Routing: gRPC (sync, cần response)
Order → Notification: Event (async, fire-and-forget)
Tracking → All: Event Streaming (continuous GPS data)
Tổng Kết Session 15
| Concept | Key Takeaway |
|---|---|
| Decomposition | By business capability (phổ biến nhất) |
| Sync: REST | Public APIs, simple CRUD |
| Sync: gRPC | Service-to-service, high performance |
| Async: Queue | Point-to-point, guaranteed delivery |
| Async: Pub/Sub | One-to-many, loose coupling |
| Circuit Breaker | Fail fast khi downstream unavailable |
| Health Checks | Liveness + readiness + dependency checks |
Bài tập: Tạo 2 .NET microservices (Orders, Catalog) communicate qua gRPC. Add circuit breaker + retry. Implement health checks cho cả 2 services.