Chuyển tới nội dung chính

16.12 — Bổ sung (đối chiếu roadmap.sh)

Tóm tắt

Đối chiếu Module 16 với nhánh architectural patterns của roadmap backend. Điểm khác biệt về cách tổ chức: roadmap thường liệt kê danh sách mẫu thiết kế để học thuộc, còn module này tổ chức theo vấn đề cần giải — mỗi mẫu xuất hiện kèm câu hỏi "nó giải quyết cái gì" và "khi nào không cần nó". Lý do rất thực tế: kiến trúc là chi phí, và mẫu thiết kế áp dụng khi chưa có vấn đề tương ứng là chi phí thuần — bạn trả ngay mà không nhận được gì. Đó cũng là nội dung của bài 16.11: "kiến trúc lớn hơn bài toán" là một anti-pattern thật sự.

Mục tiêu bài học​

Sau bài này bạn có thể:

  • Đối chiếu nội dung module với roadmap bạn đang theo.
  • Biết phần nào module cố tình không dạy và vì sao.
  • Lập thứ tự học tiếp phù hợp với công việc hiện tại.

Nội dung bài học​

16.12.1 — Đối chiếu​

Mục trong roadmapModule 16Bài
Layered architectureĐầy đủ16.3
Clean / Hexagonal / OnionĐầy đủ16.4
Vertical Slice ArchitectureĐầy đủ16.5
DDD chiến thuậtĐầy đủ16.6
Mediator patternĐầy đủ16.7
ValidationĐầy đủ16.8
Domain eventsĐầy đủ16.9
Testing strategyĐầy đủ16.10
Anti-patternĐầy đủ16.11
Repository, Unit of WorkỞ Module 1313.10
CQRS, Event sourcingChỉ nêu điều kiện16.14
DDD chiến lược (context mapping)Ngắn gọn16.13
MicroservicesỞ Module 1818.2
GoF design patternsKhông có—

16.12.2 — Khác biệt về cách tổ chức​

Roadmap thường có dạng danh sách:

Architectural Patterns
- Monolithic
- Layered
- Clean Architecture
- Hexagonal
- CQRS
- Event Sourcing

Module này tổ chức theo vấn đề:

16.2  Van de: CRM to ra, thay doi ngay cang dat
16.3 Giải pháp 1: Layered — đủ cho nhiều trường hợp
16.4 Giai phap 2: Clean — khi can doi ha tang va test doc lap
16.5 Giai phap 3: VSA — khi tinh nang doc lap nhau
16.11 Và khi nào MỌI GIẢI PHÁP TRÊN đều là thừa

Khác biệt này quan trọng vì kiến trúc là đầu tư có thể lỗ. Biết tên sáu kiểu kiến trúc mà không biết cái nào phù hợp với bài toán trước mặt thì kiến thức đó không chuyển thành quyết định đúng.

16.12.3 — Phần cố tình bỏ qua​

GoF design patterns. Factory, Strategy, Observer, Decorator — chúng là công cụ ở tầng lớp, không phải ở tầng kiến trúc. Chúng cũng xuất hiện tự nhiên khi bạn viết code tốt: pipeline behavior của MediatR là Decorator (bài 16.7), IEventPublisher là Strategy. Học tên gọi sau khi đã dùng thì dễ hơn học tên trước rồi đi tìm chỗ áp dụng.

Repository và Unit of Work. Nằm ở Module 13, vì bàn về chúng tách khỏi EF Core sẽ bỏ qua điểm quan trọng nhất: DbContext đã là cả hai.

DDD chiến lược chi tiết. Context mapping, ngôn ngữ chung, event storming — đó là công việc của cả đội và cần workshop với người phụ trách nghiệp vụ, không học được qua một bài viết. Module chỉ đưa phần chiến thuật (aggregate, value object, domain event) vì đó là phần backend engineer áp dụng trực tiếp.

CQRS và event sourcing chi tiết. Chỉ nêu điều kiện trong 16.14 và 17.12. Phần lớn CRM không cần chúng, và event sourcing là quyết định gần như không quay lại được.

16.12.4 — Thư viện nhắc tới​

Mục đíchThư việnBài
Mediator, pipelineMediatR16.7
ValidationFluentValidation16.8
Kiến trúc testNetArchTest.Rules16.11
Integration testTestcontainers, WebApplicationFactory16.10
Mutation testingStryker.NET16.10
Result patternArdalis.Result, FluentResults16.14
SpecificationArdalis.Specification16.14

Hai thứ đáng thêm ngay vào dự án hiện tại: NetArchTest.Rules (chặn vi phạm ranh giới trong CI, rẻ và hiệu quả) và Testcontainers (test với database thật).

Lưu ý về giấy phép: kiểm tra điều khoản của phiên bản MediatR bạn định dùng trước khi cam kết cho dự án dài hạn, tương tự như với thư viện messaging (bài 17.9).

16.12.5 — Học tiếp theo thứ tự nào​

Nếu codebase đang rối và bạn muốn cải thiện: 16.11 trước để nhận diện vấn đề, rồi 16.15 cho cách gỡ dần.

Nếu đang bắt đầu dự án mới: 16.3 và 16.4 để chọn kiến trúc, rồi 16.14 cho hai mẫu nên làm từ đầu là Result và strongly-typed ID.

Nếu muốn cải thiện chất lượng test: 16.10, và thêm kiến trúc test ngay trong tuần.

Nếu muốn hiểu DDD: 16.6 rồi 16.13 cho ví dụ cụ thể về ranh giới aggregate.

Nếu chuẩn bị phỏng vấn senior: 16.4 (dependency rule và cái giá thật của nó), 16.11 (Anemic Domain Model — câu hỏi rất hay gặp), và 16.10 (vì sao coverage nói dối).

Sau module này là Module 17 — Distributed Systems, nơi domain event in-process mở rộng thành integration event xuyên service.

16.12.6 — Rà lại code của bạn​

Danh sách rà soát kiến thức Module 16

  • •Giải thích được dependency rule và ba cái giá của nó.
  • •Chọn được giữa Layered, Clean và VSA theo bài toán cụ thể.
  • •Nêu được khi nào kiến trúc là thừa.
  • •Xác định được ranh giới aggregate theo nhu cầu nhất quán.
  • •Biết pipeline behavior giải quyết gì và khi nào không cần MediatR.
  • •Phân biệt validation đầu vào và bất biến domain.
  • •Biết domain event nên dispatch sau khi commit và vì sao.
  • •Giải thích được vì sao coverage nói dối.
  • •Nhận ra Anemic Domain Model và giải thích vì sao nó nguy hiểm.

Bài tập áp dụng​

Bài 1 — Tự đánh giá theo danh sách rà soát​

Đi qua danh sách rà soát ở trên và quay lại đúng bài cho mục nào chưa tự tin.

Tiêu chí hoàn thành: bạn phân biệt được "đã đọc" với "đã dùng được", và có một cách kiểm chứng cho từng mục thay vì tự chấm điểm.

Gợi ý và lời giải — Bài 1

Gợi ý. "Tôi hiểu Clean Architecture" và "tôi giải thích được cho người khác vì sao dự án này chọn nó" là hai mức khác nhau.

Lời giải — bốn mức nắm vững, và cách kiểm chứng từng mức:

MứcNghĩaCách kiểm chứng
1. BiếtNhận ra tên gọiNghe qua
2. HiểuGiải thích được cho người khácViết ba câu, không nhìn tài liệu
3. Dùng đượcÁp dụng vào code thậtCó một commit trong repo thật
4. Đánh giá đượcBiết khi nào KHÔNG nên dùngNêu được hai tình huống nên tránh

Mức 4 là mức quan trọng nhất với một người làm senior, và nó là mức mà đọc tài liệu không bao giờ đạt được — nó đến từ việc đã trả giá cho một lựa chọn sai.

Bảng tự kiểm chứng, với câu hỏi cụ thể cho từng chủ đề:

| Chủ đề | Mức | Câu hỏi kiểm chứng | Bài |
|---|:-:|---|---|
| Dependency rule | ? | Vì sao Domain không được biết EF Core? Cái giá là gì? | 16.3 |
| Layered architecture | ? | Bốn tầng làm gì? Ranh giới transaction ở đâu? | 16.2 |
| Vertical slice | ? | Khi nào VSA tốt hơn layered? Nó đánh đổi gì? | 16.4 |
| Aggregate | ? | Cái gì quyết định ranh giới aggregate? | 16.5 |
| Value object | ? | Vì sao dùng Money thay cho decimal? | 16.5 |
| MediatR | ? | Giá trị thật của nó là gì? Khi nào không cần? | 16.6 |
| Validation vs bất biến | ? | Quy tắc nào đặt ở validator, quy tắc nào ở entity? | 16.7 |
| Domain event | ? | Khi nào dùng event, khi nào gọi tường minh? | 16.8 |
| Testing | ? | Test hành vi khác test cài đặt ở chỗ nào? | 16.9 |
| Anti-pattern | ? | Ba dấu hiệu God Service? | 16.10 |

Cách tự chấm trung thực — với mỗi dòng, thử trả lời mà KHÔNG mở tài liệu:

Trả lời trôi chảy, có ví dụ từ dự án của mình  -> mức 3–4
Trả lời được ý chính, không có ví dụ cụ thể -> mức 2
Nhớ mang máng, phải mở lại bài -> mức 1

Nếu phần lớn dòng ở mức 1–2, đừng đọc lại tuần tự. Đọc lại phần bài tập của đúng những bài đó — vì bài tập là nơi buộc bạn áp dụng, còn phần lý thuyết thì đọc lần hai cũng không khác lần một bao nhiêu.

Ba câu hỏi ở mức 4 — nếu trả lời được, bạn đã nắm chắc:

1. Nêu một dự án mà Clean Architecture là lựa chọn SAI.

Ví dụ trả lời tốt:
"Một API báo cáo đọc dữ liệu từ data warehouse, không có logic ghi,
40 endpoint đều là truy vấn. Phân tầng thêm 6 file cho mỗi endpoint
mà không bảo vệ bất biến nào — vì không có bất biến nào.
Minimal API gọi thẳng Dapper là đủ."

2. Nêu một tình huống mà anemic domain model là đúng.

"Bảng danh mục tỉnh thành. Mọi tổ hợp giá trị đều hợp lệ,
không có trạng thái nào cần bảo vệ. Thêm private setter và
phương thức domain là chi phí thuần tuý."

3. Nêu một chỗ mà bạn cố tình vi phạm dependency rule, và vì sao.

"Đường đọc gọi DbContext trực tiếp thay vì qua repository.
Đo được nhanh hơn 280 lần, và đường đọc không thay đổi trạng thái
nên không có bất biến nào cần bảo vệ."

Câu 3 là câu phân biệt rõ nhất giữa người áp dụng máy móc và người hiểu. Nếu câu trả lời là "tôi không bao giờ vi phạm", đó thường là dấu hiệu chưa gặp đủ tình huống thật.

Và một cách kiểm chứng khách quan hơn tự chấm: mở dự án hiện tại và trả lời năm câu hỏi này bằng code, không bằng trí nhớ.

# 1. Domain có sạch không?
dotnet list src/Crm.Domain package --include-transitive | grep -v "^ *$"

# 2. Quy tắc nghiệp vụ có bị nhân bản không?
grep -rn "500_000_000\|NguongDuyet" --include="*.cs" src/ | wc -l

# 3. Entity có phương thức nghiệp vụ không?
grep -c "public.*Result \|public void " src/Crm.Domain/Entities/Lead.cs

# 4. Có bao nhiêu đường ghi đi vòng qua domain?
grep -rn "\.Status\s*=" --include="*.cs" src/ | grep -v "==" | wc -l

# 5. Service lớn nhất có bao nhiêu dependency?
grep -c "private readonly" $(ls -S src/Crm.Application/Services/*.cs | head -1)

Năm con số này nói về mức nắm vững thật của bạn chính xác hơn bất kỳ bảng tự chấm nào — vì chúng phản ánh những quyết định bạn đã thực sự đưa ra.


Bài 2 — Thêm kiến trúc test vào dự án thật​

Cài NetArchTest.Rules và viết một test chặn Domain phụ thuộc EF Core. Chạy trên dự án thật.

Tiêu chí hoàn thành: test chạy được trên dự án thật của bạn, và bạn có một kế hoạch cụ thể cho số vi phạm tìm được.

Gợi ý và lời giải — Bài 2

Gợi ý. Phần kỹ thuật đã có ở bài 16.10. Phần khó là làm gì sau khi thấy kết quả.

Lời giải — thiết lập:

dotnet new xunit -o tests/Crm.ArchitectureTests
dotnet sln add tests/Crm.ArchitectureTests
dotnet add tests/Crm.ArchitectureTests package NetArchTest.Rules
dotnet add tests/Crm.ArchitectureTests package FluentAssertions
dotnet add tests/Crm.ArchitectureTests reference src/Crm.Domain src/Crm.Application src/Crm.Infrastructure
public class KienTrucTests(ITestOutputHelper output)
{
private const int NguongViPham = 999; // đặt tạm, sẽ sửa sau lần chạy đầu

[Fact]
public void Domain_khong_duoc_phu_thuoc_EF_Core()
{
var kq = Types.InAssembly(typeof(Lead).Assembly)
.ShouldNot().HaveDependencyOn("Microsoft.EntityFrameworkCore")
.GetResult();

var viPham = (kq.FailingTypeNames ?? []).ToList();
foreach (var v in viPham) output.WriteLine(v);

viPham.Count.Should().BeLessThanOrEqualTo(NguongViPham);
}
}
dotnet test tests/Crm.ArchitectureTests --logger "console;verbosity=detailed"

Kế hoạch theo số vi phạm tìm được:

Trường hợp A — 0 vi phạm:

Chúc mừng, nhưng đừng dừng ở đó.
-> Đặt NguongViPham = 0 để test thật sự bảo vệ
-> Thêm quy tắc tiếp theo (Application không phụ thuộc Infrastructure)
-> Mục tiêu: 5–7 quy tắc trong một tháng

Trường hợp B — dưới 10 vi phạm:

Sửa hết trong một PR.
-> Phần lớn thường là attribute [Table], [Column] -> chuyển sang Fluent API
-> Đặt NguongViPham = 0
-> Thời gian: một buổi

Trường hợp C — 10 tới 50 vi phạm:

Ratchet, giảm theo sprint.
-> Đặt NguongViPham = số hiện tại
-> Mỗi sprint giảm 10–15, cập nhật ngưỡng trong cùng PR
-> Về 0 sau 3–4 sprint

Trường hợp D — trên 50 vi phạm:

Ratchet, nhưng đặt mục tiêu theo quý thay vì theo sprint.
-> Và trước tiên PHÂN LOẠI: attribute ánh xạ hay code gọi EF Core?
-> attribute: sửa cơ học, nhanh
-> code gọi EF Core trong Domain: cần thiết kế lại, chậm

Phân loại:

[Fact]
public void Phan_loai_vi_pham()
{
var domain = typeof(Lead).Assembly;

var doAttribute = domain.GetTypes()
.Where(t => t.GetCustomAttributes(false)
.Any(a => a.GetType().Namespace?.StartsWith("Microsoft.EntityFrameworkCore") == true
|| a.GetType().Namespace?.StartsWith("System.ComponentModel.DataAnnotations.Schema") == true))
.Select(t => t.Name).ToList();

output.WriteLine($"Do attribute ánh xạ ({doAttribute.Count}): {string.Join(", ", doAttribute)}");
}
Do attribute ánh xạ (74): Lead, Customer, Order, Invoice, ...
Còn lại (13): LeadQueryHelper, BaoCaoCu, ImportMapper, ...

74 chỗ sửa cơ học, 13 chỗ cần suy nghĩ. Đây là thông tin đủ để lập kế hoạch thật.

Viết kế hoạch ra, đừng giữ trong đầu:

## Kiến trúc test — kế hoạch

Chạy lần đầu: 2026-09-25, 87 vi phạm

### Phân loại
- 74 do attribute ánh xạ ([Table], [Column], [Key]) -> sửa cơ học
- 13 do code gọi EF Core trong Domain -> cần thiết kế lại

### Kế hoạch
| Sprint | Mục tiêu | Ngưỡng |
|---|---|---|
| 42 | Chuyển attribute của 6 entity cốt lõi sang Fluent API | 87 -> 60 |
| 43 | Chuyển attribute của phần còn lại | 60 -> 13 |
| 44 | Thiết kế lại 5 chỗ gọi EF Core trong Domain | 13 -> 8 |
| 45 | Phần còn lại | 8 -> 0 |

### Quy tắc trong lúc chờ
- Entity MỚI không được dùng attribute ánh xạ (review chặn)
- Ngưỡng chỉ được giảm, không được tăng

### Người phụ trách: <tên>

Bốn lý do kế hoạch viết ra hiệu quả hơn ý định:

  1. Số vi phạm là sự thật kiểm chứng được, không phải ý kiến về chất lượng code.
  2. Ngưỡng trong code làm hồi quy không thể lọt qua.
  3. Bảng sprint biến một việc mơ hồ thành các bước có kích thước biết trước.
  4. Có người phụ trách nghĩa là có người nhớ.

Và điều quan trọng nhất: đưa test vào CI ngay từ ngày đầu, kể cả khi ngưỡng đang là 87.

- name: Kiến trúc test
run: dotnet test tests/Crm.ArchitectureTests --no-build

Giá trị lớn nhất không phải là 87 vi phạm hiện có — mà là vi phạm thứ 88 không bao giờ xuất hiện. Đó là lợi ích bạn nhận được ngay hôm nay, trước khi sửa dòng nào.


Bài 3 — Viết lập luận kiến trúc​

Với dự án hiện tại, viết một trang giải thích vì sao chọn kiểu kiến trúc đang dùng, kèm chi phí và lợi ích cụ thể.

Tiêu chí hoàn thành: tài liệu của bạn nêu được cái giá chứ không chỉ lợi ích, và có ít nhất một con số đo được.

Gợi ý và lời giải — Bài 3

Gợi ý. Một tài liệu chỉ nói về lợi ích là quảng cáo, không phải lập luận.

Lời giải — dùng định dạng ADR (Architecture Decision Record):

# ADR-001: Clean Architecture cho đường ghi, truy vấn thẳng cho đường đọc

- **Trạng thái:** Đã chấp nhận
- **Ngày:** 2026-09-25
- **Người quyết định:** Nhóm backend CRM
- **Bối cảnh liên quan:** ADR-002 (chọn EF Core), ADR-005 (bỏ IRepository generic)

## Bối cảnh

Hệ thống CRM phục vụ 50 tenant, khoảng 200.000 lead. Hiện tại:

- Quy tắc nghiệp vụ nằm rải ở controller, service và job. Đo được ngưỡng
duyệt lead xuất hiện ở **7 file với 3 phiên bản khác nhau** — trong đó
một phiên bản dùng `>` còn một phiên bản dùng `>=`.
- Dữ liệu vi phạm bất biến đã tồn tại: **1.284 lead** ở trạng thái Won
mà không có `ClosedUtc`.
- `LeadService` có **687 dòng, 14 dependency**, và có **31 merge conflict**
trong 6 tháng qua.
- Có **4 đường ghi** vào bảng Leads: API, job import, message consumer,
và một job SQL Agent.

Dự kiến 18 tháng tới: thêm module báo giá, module hợp đồng, và tích hợp
với hệ thống kế toán.

## Quyết định

**Đường ghi** đi qua Clean Architecture đầy đủ:
- Quy tắc nghiệp vụ nằm trong entity, `private set` cho mọi thuộc tính
có bất biến.
- Handler điều phối; `Crm.Domain` không tham chiếu gói nào ngoài BCL.

**Đường đọc** gọi `DbContext` trực tiếp trong handler, trả về DTO.

## Lý do

Đường ghi và đường đọc có nhu cầu ngược nhau:

- Ghi thay đổi trạng thái -> có bất biến phải bảo vệ -> lớp trừu tượng
đảm bảo mọi đường ghi tuân thủ cùng một quy tắc.
- Đọc không thay đổi trạng thái -> không có bất biến -> lớp trừu tượng
chỉ là chi phí.

Đo được: truy vấn danh sách có phân trang qua repository thuần khiết mất
**3,4 giây và 420 MB**; qua `DbContext` trực tiếp mất **12 ms và 40 KB**.

## Cái giá — chúng tôi chấp nhận những điều sau

1. **Thêm một trường vào response tốn 5–7 file** thay vì 1 file.
Đo được: 7 file cho trường `SoNgayTonTai`.
Chấp nhận vì thay đổi này xảy ra khoảng 3 lần mỗi tuần, còn thay đổi
quy tắc nghiệp vụ — thứ đắt hơn nhiều nếu làm sai — xảy ra hằng tháng.

2. **Người mới cần khoảng 2 tuần** để quen với cấu trúc, so với khoảng
3 ngày cho kiến trúc phẳng.
Giảm thiểu bằng: tài liệu này, một slice mẫu có chú thích đầy đủ, và
kèm cặp trong hai tuần đầu.

3. **Domain không dùng được `Include`, `AsNoTracking`, `ExecuteUpdate`.**
Chấp nhận vì những tính năng đó thuộc đường đọc, và đường đọc không
đi qua Domain.

4. **Chúng tôi cố tình KHÔNG dùng `IRepository<T>` generic** — xem ADR-005.
Nó chặn quá nhiều tính năng EF Core mà không bảo vệ thêm bất biến nào.

5. **Chi phí ban đầu khoảng 3 sprint** để gỡ `LeadService` và `OrderService`,
làm dần theo quy tắc "chạm đâu gỡ đó", không có sprint riêng.

## Các phương án đã cân nhắc

| Phương án | Vì sao không chọn |
|---|---|
| Giữ nguyên hiện trạng | 4 đường ghi với 3 hành vi khác nhau; dữ liệu sai đang tích tụ |
| Vertical Slice hoàn toàn | Không giải quyết được việc quy tắc bị nhân bản qua các slice |
| Clean Architecture cho CẢ đường đọc | Đo được chậm hơn 280 lần; không bảo vệ thêm bất biến nào |
| Microservices | Nhóm 6 người, một database; chi phí vận hành không xứng — xem ADR-008 |

## Cách chúng tôi biết quyết định này đúng hay sai

Đo lại sau 6 tháng (2027-03-25):

| Chỉ số | Hiện tại | Mục tiêu |
|---|---:|---:|
| Số file phải sửa khi đổi một quy tắc nghiệp vụ | 7 | **1** |
| Dòng vi phạm bất biến trong database | 1.284 | **0** |
| Merge conflict mỗi tuần | 5 | **dưới 2** |
| Thời gian chạy unit test của Domain | — | **dưới 10 giây** |
| Dependency trung bình mỗi handler | 14 | **dưới 5** |

**Nếu sau 6 tháng ba chỉ số đầu không cải thiện**, quyết định này sai và
chúng tôi sẽ xem lại — có thể chuyển hẳn sang Vertical Slice với domain
model mỏng hơn.

Năm thứ làm một ADR tốt — và bốn trong số đó thường bị thiếu:

1. Có con số, không chỉ tính từ.

Kém:  "code hiện tại khó bảo trì"
Tốt: "ngưỡng duyệt xuất hiện ở 7 file với 3 phiên bản khác nhau"

2. Có mục "cái giá". Đây là phần bị thiếu thường xuyên nhất, và là phần làm tài liệu đáng tin. Một ADR chỉ nói lợi ích khiến người đọc nghi ngờ mọi điều còn lại.

3. Nêu các phương án đã loại, kèm lý do. Sáu tháng sau sẽ có người hỏi "sao không dùng X?" — và nếu X đã nằm trong bảng, cuộc trò chuyện kết thúc trong một phút thay vì một cuộc họp.

4. Có tiêu chí biết mình sai. Đây là phần phân biệt một quyết định kỹ thuật với một niềm tin. Nếu không có cách nào biết quyết định sai, thì nó không phải quyết định mà là sở thích.

5. Ghi rõ những gì cố tình KHÔNG làm. Mục 4 trong "cái giá" ở trên ngăn được việc ai đó thêm IRepository<T> vào sáu tháng sau vì "đó là chuẩn Clean Architecture".

Nơi đặt và cách bảo trì:

docs/architecture-decisions/
0001-clean-architecture-cho-duong-ghi.md
0002-chon-ef-core.md
0005-bo-irepository-generic.md
0008-khong-dung-microservices.md
README.md <- danh sách và trạng thái
| # | Tiêu đề | Trạng thái | Ngày |
|---|---|---|---|
| 001 | Clean Architecture cho đường ghi | Đã chấp nhận | 2026-09-25 |
| 002 | Chọn EF Core | Đã chấp nhận | 2025-03-10 |
| 005 | Bỏ IRepository generic | Đã chấp nhận | 2026-06-14 |
| 008 | Không dùng microservices | Đã chấp nhận | 2026-08-02 |
| 003 | Dùng MongoDB cho log | **Đã thay thế bởi 009** | 2025-11-20 |

Quy tắc quan trọng: ADR là bất biến. Khi quyết định đổi, không sửa ADR cũ — viết ADR mới và đánh dấu cái cũ là "đã thay thế". Lý do: giá trị của ADR không nằm ở việc nó mô tả hiện tại, mà ở việc nó ghi lại bối cảnh tại thời điểm quyết định. Sửa nó là xoá mất lý do vì sao người ta từng nghĩ khác.

Và một cách đơn giản để bắt đầu nếu dự án chưa có ADR nào: viết ADR cho ba quyết định đã được đưa ra trong quá khứ. Nó buộc bạn phải diễn đạt được lý do — và bạn sẽ phát hiện ít nhất một quyết định mà không ai còn nhớ vì sao.

Tự kiểm tra​

Câu hỏi thường gặp

Vì sao module tổ chức theo vấn đề thay vì theo danh sách mẫu?

Vì kiến trúc là chi phí và có thể lỗ. Biết tên sáu kiểu kiến trúc mà không biết cái nào phù hợp với bài toán trước mặt thì kiến thức đó không chuyển thành quyết định đúng.

Vì sao module bỏ qua GoF design patterns?

Vì chúng là công cụ ở tầng lớp chứ không phải tầng kiến trúc, và chúng xuất hiện tự nhiên khi viết code tốt. Pipeline behavior chính là Decorator, IEventPublisher chính là Strategy. Học tên sau khi đã dùng thì dễ hơn.

Vì sao Repository và Unit of Work nằm ở Module 13?

Vì bàn về chúng tách khỏi EF Core sẽ bỏ qua điểm quan trọng nhất là DbContext đã là cả repository lẫn unit of work rồi, nên bọc thêm một lớp thường chỉ là trung gian vô ích.

Vì sao DDD chiến lược chỉ được nói ngắn gọn?

Vì context mapping, ngôn ngữ chung và event storming là công việc của cả đội, cần workshop với người phụ trách nghiệp vụ, không học được qua một bài viết. Module đưa phần chiến thuật vì đó là phần backend engineer áp dụng trực tiếp.

Hai thư viện nào đáng thêm ngay vào dự án?

NetArchTest.Rules để chặn vi phạm ranh giới ngay trong CI, và Testcontainers để test với database thật thay vì provider InMemory. Cả hai đều rẻ và cho hiệu quả rõ ràng.

Nếu codebase đang rối thì nên đọc bài nào trước?

Bài 16.11 để nhận diện đúng anti-pattern đang mắc phải, rồi bài 16.15 cho cách gỡ dần mà không phải dừng phát triển tính năng.

Kết luận​

Ba điều đáng nhớ nhất:

  1. Module tổ chức theo vấn đề, vì kiến trúc là đầu tư có thể lỗ.
  2. GoF pattern xuất hiện tự nhiên khi viết code tốt — không cần học thuộc trước.
  3. Kiến trúc test là thứ rẻ nhất có hiệu quả rõ nhất — thêm ngay trong tuần.

Tham khảo​

Điều hướng​