Patterns cho hệ thống enterprise thực chiến: Repository, Unit of Work, Outbox, CQRS, Event Sourcing, Circuit Breaker, Retry, Saga. Đây là patterns Senior/Architect dùng HÀNG NGÀY.
Repository Pattern
csharp// Interface (Application layer)
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<List<Order>> GetByCustomerAsync(Guid customerId, CancellationToken ct = default);
Task AddAsync(Order order, CancellationToken ct = default);
}
// Implementation (Infrastructure layer)
public class EfOrderRepository(AppDbContext db) : IOrderRepository
{
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
=> await db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task AddAsync(Order order, CancellationToken ct)
=> await db.Orders.AddAsync(order, ct);
}
Tranh luận: "Cần Repository khi đã có DbContext?"
- ✅ Cần: khi muốn swap ORM (hiếm), khi cần custom query abstraction, khi cần mock cho test
- ❌ Không cần: Simple CRUD, DbContext ĐÃ LÀ Repository + Unit of Work
Unit of Work
csharp// DbContext chính là Unit of Work!
public interface IUnitOfWork
{
Task<int> CommitAsync(CancellationToken ct = default);
}
// AppDbContext implements UoW
public class AppDbContext : DbContext, IUnitOfWork
{
public async Task<int> CommitAsync(CancellationToken ct)
=> await SaveChangesAsync(ct);
// SaveChanges = 1 transaction cho TẤT CẢ changes
}
Outbox Pattern (Reliable Event Publishing)
csharp// Outbox table
public class OutboxMessage
{
public Guid Id { get; set; }
public string Type { get; set; } = "";
public string Payload { get; set; } = "";
public DateTime CreatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
}
// Background Worker
public class OutboxProcessor(AppDbContext db, IPublishEndpoint bus) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var messages = await db.OutboxMessages
.Where(m => m.PublishedAt == null)
.OrderBy(m => m.CreatedAt)
.Take(50)
.ToListAsync(ct);
foreach (var msg in messages)
{
var @event = JsonSerializer.Deserialize(msg.Payload, Type.GetType(msg.Type)!);
await bus.Publish(@event!, ct);
msg.PublishedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync(ct);
await Task.Delay(1000, ct);
}
}
}
Giải quyết: Dual-write problem (save DB + publish event phải atomic).
Circuit Breaker & Retry
csharp// .NET 8+: Microsoft.Extensions.Http.Resilience
builder.Services.AddHttpClient("PaymentApi")
.AddStandardResilienceHandler();
// ↑ Bao gồm: Retry, Circuit Breaker, Timeout, Rate Limiter - tự động!
// Custom configuration
builder.Services.AddHttpClient("ExternalApi")
.AddResilienceHandler("custom", pipeline =>
{
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromMilliseconds(500)
});
pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5, // 50% fails → open circuit
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(15)
});
pipeline.AddTimeout(TimeSpan.FromSeconds(10));
});
Saga Pattern (Distributed Transaction)
csharp// MassTransit State Machine Saga
public class OrderSaga : MassTransitStateMachine<OrderSagaState>
{
public OrderSaga()
{
InstanceState(x => x.CurrentState);
Event(() => OrderCreated, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => StockReserved, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PaymentProcessed, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PaymentFailed, x => x.CorrelateById(m => m.Message.OrderId));
Initially(
When(OrderCreated)
.Then(ctx => ctx.Saga.OrderId = ctx.Message.OrderId)
.Publish(ctx => new ReserveStock(ctx.Saga.OrderId))
.TransitionTo(AwaitingStock));
During(AwaitingStock,
When(StockReserved)
.Publish(ctx => new ProcessPayment(ctx.Saga.OrderId))
.TransitionTo(AwaitingPayment));
During(AwaitingPayment,
When(PaymentProcessed)
.Publish(ctx => new ScheduleShipping(ctx.Saga.OrderId))
.TransitionTo(Completed),
When(PaymentFailed)
.Publish(ctx => new ReleaseStock(ctx.Saga.OrderId)) // Compensate!
.Publish(ctx => new CancelOrder(ctx.Saga.OrderId))
.TransitionTo(Failed));
}
}
Event Sourcing (Advanced)
csharp// Thay vì lưu STATE hiện tại, lưu TẤT CẢ EVENTS
// Rebuild state bằng cách replay events
public class Order
{
private readonly List<IDomainEvent> _events = [];
public void Apply(OrderCreatedEvent e) { Id = e.OrderId; Status = OrderStatus.Draft; }
public void Apply(ItemAddedEvent e) { Items.Add(new OrderItem(e.ProductId, e.Quantity)); }
public void Apply(DiscountAppliedEvent e) { Discount = e.Percentage; }
// Rebuild from events
public static Order FromHistory(IEnumerable<IDomainEvent> events)
{
var order = new Order();
foreach (var e in events) order.Apply((dynamic)e);
return order;
}
}
Dùng khi: Audit trail BẮT BUỘC (finance, insurance), time-travel debugging, undo/redo.
KHÔNG dùng khi: CRUD đơn giản, team chưa hiểu rõ eventual consistency.
Decision Matrix - Chọn Pattern Nào?
| Pattern | Complexity | When to Learn | Used In |
|---|---|---|---|
| Repository | ⭐⭐ | Day 1 | Mọi project |
| Unit of Work | ⭐⭐ | Day 1 | Mọi project |
| Outbox | ⭐⭐⭐ | Event-driven projects | eCommerce, Fintech |
| Circuit Breaker | ⭐⭐ | External integrations | Microservices |
| Saga | ⭐⭐⭐⭐ | Distributed workflows | Booking, Payment flows |
| Event Sourcing | ⭐⭐⭐⭐⭐ | Audit-heavy domains | Banking, Insurance |