Mục tiêu: Thiết kế Modular Monolith - bước trung gian tối ưu trước khi chuyển sang Microservices.
1. Tại Sao Modular Monolith?
Monolith (Big Ball of Mud) → Modular Monolith → Microservices
┌─────────────────────┐ ┌─────────────────────────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ Tất cả tightly │ │ ┌──────┐ ┌──────┐ │ │Svc A │ │Svc B │ │Svc C │
│ coupled lại │ │ │Mod A │ │Mod B │ │ │ │ │ │ │ │
│ với nhau │ │ │ │ │ │ │ │ DB A │ │ DB B │ │ DB C │
│ │ │ │ DB │ │ DB │ │ └──────┘ └──────┘ └──────┘
│ DB │ │ └──────┘ └──────┘ │
└─────────────────────┘ │ Shared hosting │
│ 1 deployment │
└─────────────────────────┘
✅ Modular Monolith cho bạn:
- Boundaries rõ ràng giữa modules (dễ hiểu, dễ maintain)
- 1 deployment unit (simple ops)
- Tốc độ development như monolith
- Dễ extract thành microservice SAU NÀY khi cần
- Transaction đơn giản (same process)
- Không cần distributed tracing, service mesh, etc.
⚠️ Microservices chỉ nên dùng khi:
- Team quá lớn (>20 devs, cần deploy independent)
- Scale requirements RẤT khác nhau giữa modules
- Technology diversity (module X dùng Python, module Y dùng .NET)
2. Module Structure
src/
├── Bootstrapper/ ← Host application
│ ├── Program.cs ← DI registration cho tất cả modules
│ ├── appsettings.json
│ └── Bootstrapper.csproj ← Reference tất cả modules
│
├── Modules/
│ ├── Orders/
│ │ ├── Orders.Application/
│ │ │ ├── Commands/
│ │ │ ├── Queries/
│ │ │ ├── IntegrationEvents/ ← Events gửi cho modules khác
│ │ │ └── ModuleInstaller.cs ← DI cho module này
│ │ ├── Orders.Domain/
│ │ │ ├── Entities/
│ │ │ └── Interfaces/
│ │ ├── Orders.Infrastructure/
│ │ │ ├── Persistence/
│ │ │ │ ├── OrdersDbContext.cs ← DB Context RIÊNG
│ │ │ │ └── Migrations/
│ │ │ └── ModuleInstaller.cs
│ │ └── Orders.Api/
│ │ ├── Controllers/
│ │ └── ModuleInstaller.cs
│ │
│ ├── Catalog/
│ │ ├── Catalog.Application/
│ │ ├── Catalog.Domain/
│ │ ├── Catalog.Infrastructure/
│ │ └── Catalog.Api/
│ │
│ └── Shipping/
│ └── ...
│
└── Shared/
├── Shared.Abstractions/ ← Interfaces chung
│ ├── IIntegrationEvent.cs
│ ├── IModuleInstaller.cs
│ └── IEventBus.cs
└── Shared.Infrastructure/ ← Implementation chung
├── InMemoryEventBus.cs
└── Outbox/
3. Module Boundaries & Contracts
Rule: Modules Communicate qua Contracts, KHÔNG Reference Trực Tiếp
csharp// ❌ BAD: Orders module reference trực tiếp Catalog domain
using Catalog.Domain.Entities;
public class OrderService
{
public void CreateOrder(Product product) // 💥 Tight coupling!
{
// ...
}
}
// ✅ GOOD: Communication qua contracts
// Shared/Shared.Abstractions/Modules/Catalog/
public interface ICatalogModule
{
Task<ProductDto?> GetProductAsync(Guid productId);
Task<bool> ReserveStockAsync(Guid productId, int quantity);
}
// Catalog module implements contract:
internal class CatalogModule(CatalogDbContext db) : ICatalogModule
{
public async Task<ProductDto?> GetProductAsync(Guid productId)
{
var product = await db.Products.FindAsync(productId);
return product?.ToDto();
}
public async Task<bool> ReserveStockAsync(Guid productId, int quantity)
{
var product = await db.Products.FindAsync(productId);
if (product is null || product.Stock < quantity) return false;
product.ReserveStock(quantity);
await db.SaveChangesAsync();
return true;
}
}
// Orders module consumes contract:
public class CreateOrderHandler(ICatalogModule catalogModule)
{
public async Task<Result<Guid>> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var product = await catalogModule.GetProductAsync(cmd.ProductId);
if (product is null) return Result.Failure<Guid>("Product not found");
var reserved = await catalogModule.ReserveStockAsync(cmd.ProductId, cmd.Quantity);
if (!reserved) return Result.Failure<Guid>("Insufficient stock");
// Create order...
}
}
4. Integration Events - Async Communication
csharp// Shared event interface
public interface IIntegrationEvent
{
Guid Id { get; }
DateTime OccurredOn { get; }
}
// Event definition (trong Shared hoặc publisher module)
public record OrderSubmittedIntegrationEvent(
Guid OrderId,
Guid CustomerId,
decimal TotalAmount,
List<OrderItemEvent> Items) : IIntegrationEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
// In-Process Event Bus (Modular Monolith)
public class InMemoryEventBus(IServiceProvider serviceProvider) : IEventBus
{
public async Task PublishAsync<T>(T @event, CancellationToken ct = default)
where T : IIntegrationEvent
{
using var scope = serviceProvider.CreateScope();
var handlers = scope.ServiceProvider.GetServices<IIntegrationEventHandler<T>>();
foreach (var handler in handlers)
{
try
{
await handler.HandleAsync(@event, ct);
}
catch (Exception ex)
{
// Log error, optionally retry or send to dead letter
// QUAN TRỌNG: 1 handler fail KHÔNG ảnh hưởng handlers khác
}
}
}
}
// Handler trong Shipping module:
internal class OrderSubmittedHandler(ShippingDbContext db)
: IIntegrationEventHandler<OrderSubmittedIntegrationEvent>
{
public async Task HandleAsync(OrderSubmittedIntegrationEvent evt, CancellationToken ct)
{
var shipment = Shipment.CreateFromOrder(evt.OrderId, evt.Items);
db.Shipments.Add(shipment);
await db.SaveChangesAsync(ct);
}
}
5. Database per Module
csharp// Mỗi module có DbContext RIÊNG
// Cùng 1 SQL Server instance, nhưng schema/prefix khác nhau
// Orders module
public class OrdersDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("orders"); // Schema prefix
modelBuilder.ApplyConfigurationsFromAssembly(typeof(OrdersDbContext).Assembly);
}
}
// Catalog module
public class CatalogDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("catalog");
modelBuilder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly);
}
}
// SQL tables:
// [orders].[Orders]
// [orders].[OrderItems]
// [catalog].[Products]
// [catalog].[Categories]
// ⚠️ KHÔNG cross-schema joins!
// Modules communicate qua contracts hoặc integration events
6. Evolution: Monolith → Modular → Microservices
Step 1: Monolith (start here)
- 1 project, 1 DB, 1 deployment
- Nhanh để develop MVP
Step 2: Modular Monolith (khi codebase lớn lên)
- Tách modules với clear boundaries
- Database schemas riêng
- In-process event bus
- VẪN 1 deployment
Step 3: Extract Microservice (chỉ khi cần)
- Module nào cần scale riêng → extract thành service
- Thay InMemoryEventBus → RabbitMQ/Azure Service Bus
- Thay ICatalogModule call → gRPC/HTTP call
- Module contract KHÔNG đổi → extraction smooth
Ví dụ thực tế:
- Orders module: 80% traffic → Extract để scale riêng
- Catalog module: ít traffic → Giữ trong monolith
- Notification module: burst traffic → Extract + Azure Functions
csharp// Khi extract: chỉ đổi implementation, interface giữ nguyên
// Trước (in-process):
internal class CatalogModule : ICatalogModule
{
public Task<ProductDto?> GetProductAsync(Guid id)
=> _db.Products.FindAsync(id).Map(p => p.ToDto());
}
// Sau (HTTP call to microservice):
internal class CatalogModuleClient(HttpClient httpClient) : ICatalogModule
{
public async Task<ProductDto?> GetProductAsync(Guid id)
{
var response = await httpClient.GetAsync($"/api/products/{id}");
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<ProductDto>();
}
}
// → KHÔNG CẦN sửa OrderService vì nó depend vào ICatalogModule interface!
Tổng Kết Session 14
| Concept | Key Takeaway |
|---|---|
| Modular Monolith | Best of both: boundaries + simple deployment |
| Module Contracts | Interface-based communication, KHÔNG direct reference |
| Integration Events | Async, in-process event bus |
| DB per Module | Schema separation, no cross-schema joins |
| Evolution Path | Monolith → Modular → Extract microservice khi cần |
Bài tập: Tách 1 monolith project thành 2 modules (Orders, Catalog) với schema riêng, in-process event bus, và module contracts. Verify cross-module communication hoạt động.