EngineeringSolution ArchitectData and Decisions
Session 03 - Data Modeling Strategies
3 min read8 lessons in this module
Database Paradigm Comparison
| Tiêu chí | Relational (SQL Server) | Document (Cosmos DB) | Graph (Gremlin) |
|---|
| Data model | Tables, rows, columns | JSON documents | Vertices + Edges |
| Schema | Fixed schema, migrations | Schema-flexible | Schema-flexible |
| Relationships | JOINs, foreign keys | Embedding, referencing | Native traversal |
| Query language | T-SQL | SQL-like, LINQ | Gremlin traversal |
| Transactions | ACID across tables | ACID within partition | ACID within partition |
| Scaling | Vertical (scale up) | Horizontal (partitioning) | Horizontal |
| Best for | Complex queries, reporting | High throughput, flexible schema | Complex relationships |
| Worst for | Extreme scale, schema changes | Complex JOINs, aggregations | Simple CRUD |
| Cost model | License/DTU | RU/s (pay per throughput) | RU/s |
| .NET support | EF Core (mature) | Cosmos SDK / EF Core | Gremlin.NET |
Khi nào chọn gì?
Normalization vs Denormalization
Normalization (1NF → 3NF)
| Normal Form | Rule | Ví dụ |
|---|
| 1NF | Atomic values, no repeating groups | Tách Tags = "c#,dotnet" thành bảng riêng |
| 2NF | 1NF + no partial dependencies | Tách OrderItems.ProductName ra bảng Products |
| 3NF | 2NF + no transitive dependencies | Tách Customer.CityZipCode ra bảng Cities |
Ví dụ: Normalized Design
sql
CREATE TABLE Customers (
Id INT PRIMARY KEY,
Name NVARCHAR(200) NOT NULL,
Email NVARCHAR(200) NOT NULL
);
CREATE TABLE Orders (
Id INT PRIMARY KEY,
CustomerId INT FOREIGN KEY REFERENCES Customers(Id),
OrderDate DATETIME2 NOT NULL,
Status NVARCHAR(50) NOT NULL
);
CREATE TABLE OrderItems (
Id INT PRIMARY KEY,
OrderId INT FOREIGN KEY REFERENCES Orders(Id),
ProductId INT FOREIGN KEY REFERENCES Products(Id),
Quantity INT NOT NULL,
UnitPrice DECIMAL(18,2) NOT NULL
);
CREATE TABLE Products (
Id INT PRIMARY KEY,
Name NVARCHAR(200) NOT NULL,
CategoryId INT FOREIGN KEY REFERENCES Categories(Id),
Price DECIMAL(18,2) NOT NULL
);
sql
CREATE TABLE OrderSummary (
OrderId INT PRIMARY KEY,
CustomerName NVARCHAR(200),
OrderDate DATETIME2,
Status NVARCHAR(50),
TotalAmount DECIMAL(18,2),
ItemCount INT,
TopProductName NVARCHAR(200)
);
Trade-offs Table
| Aspect | Normalized | Denormalized |
|---|
| Write performance | Tốt - update 1 chỗ | Kém - update nhiều chỗ |
| Read performance | Cần JOINs | Nhanh - single table |
| Data consistency | Single source of truth | Có thể inconsistent |
| Storage | Ít duplicate | Nhiều duplicate |
| Schema flexibility | Dễ thay đổi | Khó refactor |
| Best for | OLTP, write-heavy | OLAP, read-heavy |
Event Sourcing Data Model
Concept
Thay vì lưu current state, lưu sequence of events đã xảy ra.
Implementation
csharp
public abstract record DomainEvent
{
public Guid EventId { get; init; } = Guid.NewGuid();
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
public required string AggregateId { get; init; }
public int Version { get; init; }
}
public record OrderCreated : DomainEvent
{
public required string CustomerId { get; init; }
public required decimal TotalAmount { get; init; }
}
public record OrderItemAdded : DomainEvent
{
public required string ProductId { get; init; }
public required int Quantity { get; init; }
public required decimal UnitPrice { get; init; }
}
public record OrderPaymentReceived : DomainEvent
{
public required decimal Amount { get; init; }
public required string PaymentMethod { get; init; }
}
public class Order
{
public string Id { get; private set; } = string.Empty;
public string CustomerId { get; private set; } = string.Empty;
public OrderStatus Status { get; private set; }
public List<OrderItem> Items { get; } = new();
public decimal TotalAmount { get; private set; }
public int Version { get; private set; }
public void Apply(DomainEvent @event)
{
switch (@event)
{
case OrderCreated e:
Id = e.AggregateId;
CustomerId = e.CustomerId;
TotalAmount = e.TotalAmount;
Status = OrderStatus.Created;
break;
case OrderItemAdded e:
Items.Add(new OrderItem(e.ProductId, e.Quantity, e.UnitPrice));
break;
case OrderPaymentReceived:
Status = OrderStatus.Paid;
break;
}
Version = @event.Version;
}
}
CQRS - Command Query Responsibility Segregation
Architecture
Implementation
csharp
public class CreateOrderCommand : IRequest<OrderResult>
{
public required string CustomerId { get; init; }
public required List<OrderItemDto> Items { get; init; }
}
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderResult>
{
private readonly AppDbContext _writeDb;
private readonly IEventBus _eventBus;
public async Task<OrderResult> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId, cmd.Items);
_writeDb.Orders.Add(order);
await _writeDb.SaveChangesAsync(ct);
await _eventBus.PublishAsync(new OrderCreatedEvent(order));
return new OrderResult(order.Id);
}
}
public class GetOrderSummaryQuery : IRequest<OrderSummaryDto>
{
public required string OrderId { get; init; }
}
public class GetOrderSummaryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto?>
{
private readonly IDistributedCache _cache;
private readonly CosmosClient _cosmosClient;
public async Task<OrderSummaryDto?> Handle(GetOrderSummaryQuery query, CancellationToken ct)
{
var container = _cosmosClient.GetContainer("ReadDb", "OrderSummaries");
var response = await container.ReadItemAsync<OrderSummaryDto>(
query.OrderId, new PartitionKey(query.OrderId), cancellationToken: ct);
return response.Resource;
}
}
Polyglot Persistence
Concept
Dùng database phù hợp nhất cho từng use case thay vì ép mọi thứ vào một database.
| Data Type | Database | Lý do |
|---|
| Transactional data | SQL Server | ACID, complex queries |
| Product catalog | Cosmos DB | Flexible schema, fast reads |
| User sessions | Redis | In-memory, fast TTL |
| Full-text search | Elasticsearch | Inverted index, ranking |
| Audit logs | Cosmos DB / Blob | Append-only, cheap storage |
| Social graph | Cosmos Gremlin | Relationship traversal |
| Time-series metrics | Azure Data Explorer | Time-series optimized |
Challenges
| Challenge | Mitigation |
|---|
| Distributed transactions | Saga pattern, eventual consistency |
| Data consistency | Event-driven sync via Service Bus |
| Operational complexity | Managed services (PaaS), IaC |
| Team expertise | Training, documentation, shared libraries |
| Debugging | Distributed tracing (OpenTelemetry) |
Trade-offs Summary
| Quyết định | Pro | Con |
|---|
| Event Sourcing | Full audit trail, temporal queries | Complexity, eventual consistency |
| CQRS | Optimized read/write independently | Two models to maintain, sync delay |
| Polyglot persistence | Right tool for each job | Operational complexity, consistency challenges |
| Single database | Simple, ACID everywhere | Scaling bottleneck, forced compromise |
| Denormalized reads | Fast queries, no JOINs | Data duplication, update complexity |
| Normalized writes | Data integrity, single truth | Slow reads with many JOINs |