01 Kiến trúc là gì — và ai làm nó
Kiến trúc của một hệ thống phần mềm là HÌNH DẠNG (shape) mà những người xây dựng nó tạo ra — qua cách chia hệ thống thành component, cách sắp xếp, và cách chúng giao tiếp.
Robert C. Martin · Chương 15Có thể bạn nghĩ mục tiêu của kiến trúc là "làm hệ thống chạy đúng". Tất nhiên ta muốn nó chạy đúng — nhưng architecture rất ít chi phối behavior. Điều quan trọng nhất kiến trúc làm được cho hành vi là phơi bày ý đồ (intent) của hệ thống ra cấp độ cấu trúc, để lập trình viên nhìn là thấy.
Shape = (cách chia component) × (cách sắp xếp) × (cách chúng giao tiếp). Mục tiêu tối thượng: tối thiểu hóa chi phí vòng đời & tối đa hóa năng suất lập trình viên — không phải số tính năng.
Kiến trúc sư vẫn là lập trình viên
Đừng tin lời nói dối rằng kiến trúc sư "rút khỏi code để lo việc cấp cao". Họ không hề. Kiến trúc sư là những lập trình viên giỏi nhất và tiếp tục nhận task lập trình. Lý do: họ không thể làm tốt việc của mình nếu không tự nếm trải những vấn đề mà các quyết định kiến trúc của họ gây ra cho phần còn lại của đội.
02 Để mở các lựa chọn: Policy vs Detail
Phần mềm được phát minh để "soft"-ware — dễ đổi hành vi máy móc. Giữ cho nó mềm nghĩa là để mở càng nhiều lựa chọn càng tốt, càng lâu càng tốt (leaving options open), bằng cách trì hoãn các quyết định về những chi tiết không quan trọng.
Policy
Mọi quy tắc và quy trình nghiệp vụ — nơi chứa giá trị thật của hệ thống. Đây là cái bạn phải bảo vệ.
Detail
Thứ cần để con người/hệ thống khác giao tiếp với policy, nhưng không ảnh hưởng hành vi của policy: database, web server, framework, giao thức, IO.
Mục tiêu của kiến trúc sư: tạo shape sao cho detail trở nên không liên quan (irrelevant) với policy — nhờ vậy quyết định về DB, web framework, IO… được trì hoãn tới phút chót. Một kiến trúc sư giỏi tối đa hóa số quyết định CHƯA phải đưa ra (maximizes the number of decisions not made).
Không cần chọn loại database sớm: policy cấp cao lẽ ra không quan tâm nó là relational, distributed, hierarchical hay flat file. Không cần chọn web server sớm: policy không nên biết mình đang được phân phối qua web. Lỡ công ty đã "chốt" một DB/framework rồi? Kiến trúc sư giỏi vờ như chưa chốt, và shape hệ thống sao cho quyết định ấy vẫn có thể đổi càng lâu càng tốt.
Device independence (thập niên 1960): OS trừu tượng hóa thiết bị IO thành "unit-record" ảo. Chương trình viết một lần, test trên line printer, rồi "in" ra băng từ. Shape ấy tách policy (định dạng bản ghi) khỏi detail (thiết bị) — chính là nguyên lý hôm nay áp dụng ở quy mô lớn: tách policy khỏi đĩa cứng, khỏi loại DB.
03 Bốn thứ một kiến trúc tốt phải hỗ trợ
Một kiến trúc tốt phải đồng thời nâng đỡ vòng đời hệ thống trên bốn mặt — và cân bằng tất cả bằng một cấu trúc component thỏa mãn lẫn nhau. Nghe thì dễ; làm mới khó.
| Khía cạnh | Kiến trúc làm gì |
|---|---|
| Use cases & operation | Hỗ trợ intent của hệ thống (giỏ hàng phải hỗ trợ use case giỏ hàng) — ưu tiên số một; và đáp ứng throughput / response time mà vận hành đòi hỏi. |
| Operation | Giữ component cô lập tốt để dễ chuyển giữa monolith ↔ đa luồng ↔ đa tiến trình ↔ micro-services khi nhu cầu đổi. Hình thái thực thi là một lựa chọn để mở. |
| Development | Phân vùng thành component cô lập, độc lập phát triển — nhiều đội làm song song không cản nhau (Conway's law). |
| Deployment | Build xong triển khai được ngay (immediately deployable), không cần kịch bản cấu hình rườm rà hay tạo thủ công thư mục/file. |
Maintenance là khía cạnh tốn kém nhất. Tách hệ thống thành component và cô lập qua giao diện ổn định (stable interfaces) sẽ soi sáng đường cho tính năng mới và giảm mạnh rủi ro vỡ dây chuyền.
04 Decoupling: tầng ngang vs use case dọc
Kiến trúc tốt khiến hệ thống dễ thay đổi theo mọi hướng nó phải đổi, bằng cách để mở các lựa chọn. Có hai trục tách rời, và một câu hỏi thứ ba về mức độ tách.
Decoupling Layers
Tách theo SRP + CCP: UI · application-specific rules · application-independent rules · database. Mỗi tầng đổi với tốc độ và lý do khác nhau.
Decoupling Use Cases
Mỗi use case là lát cắt dọc xuyên qua các tầng (addOrder ≠ deleteOrder). Tách dọc ⟶ thêm use case mới không động vào cái cũ.
flowchart TB
subgraph L["Tách NGANG · Decoupling Layers"]
direction TB
UI["UI"]:::core --> APP["App-specific rules"]:::mid --> DOM["App-independent rules"]:::mid --> DB["Database"]:::out
end
subgraph V["Tách DỌC · Decoupling Use Cases"]
direction LR
U1["addOrder
(lát cắt)"]:::slice
U2["deleteOrder
(lát cắt)"]:::slice
U3["viewReport
(lát cắt)"]:::slice
end
L --> V
classDef core fill:#dce7f1,stroke:#2e5e8c,color:#14233b;
classDef mid fill:#e2edf3,stroke:#2f6d93,color:#14233b;
classDef out fill:#e2edf3,stroke:#2f6d93,color:#14233b;
classDef slice fill:#dce7f1,stroke:#2e5e8c,color:#14233b;
Decoupling Modes — tách tới mức nào?
Source level
Kiểm soát phụ thuộc giữa module nguồn để đổi một module không buộc biên dịch lại module khác (vd Ruby Gems). Tất cả chạy cùng một address space, gọi nhau bằng function call — đây chính là monolith.
Deployment level
Kiểm soát phụ thuộc giữa các đơn vị triển khai (jar, DLL, shared library) để đổi một module không buộc rebuild/redeploy module khác. Nhiều component vẫn có thể chung một tiến trình.
Service level
Hạ phụ thuộc xuống mức cấu trúc dữ liệu, giao tiếp qua network packet (services / micro-services). Mỗi đơn vị thực thi hoàn toàn độc lập với thay đổi source & binary của đơn vị khác.
Đừng mặc định service-level. Mặc định micro-services thì đắt (thời gian dev + tài nguyên) và đẩy về tách thô (coarse-grained). Ưu tiên của Martin: đẩy việc tách tới điểm CÓ THỂ tạo service nếu cần, nhưng giữ component trong cùng address space lâu nhất có thể. Hệ thống có thể sinh ra là monolith → lớn thành deployable units → thành services → rồi trượt ngược về monolith. Decoupling mode là một lựa chọn để mở, sẽ đổi theo thời gian.
RAG · use case dọc & ranh giới service
RAG Hai use case "ingest tài liệu" và "hỏi đáp" đổi vì lý do khác nhau → tách thành hai lát dọc riêng, mỗi lát có pipeline riêng:
# Lát dọc 1 · INGEST — đổi khi format tài liệu / chiến lược chunk đổi
class IngestUseCase:
def run(self, doc) -> None:
chunks = self.splitter.split(doc) # UI/intake riêng
self.vector_store.upsert(self.embed(chunks))
# Lát dọc 2 · ASK — đổi khi cách retrieve / prompt trả lời đổi
class AskUseCase:
def run(self, question) -> str: # độc lập với Ingest
ctx = self.vector_store.search(question)
return self.llm.generate(self.prompt(ctx, question))
RAG Ranh giới PHP (web) ↔ Python (pipeline) đi qua network = service-level decoupling sẵn có. Nhưng đừng vì thế mà chẻ tiếp pipeline Python thành 5 micro-service "cho hiện đại":
// PHP CHỈ ở ranh giới — không nhúng logic pipeline
$res = Http::post('http://rag-svc/ask', ['q' => $question]);
return $res->json('answer'); // service-level: độc lập source & binary
// Bên trong rag-svc, Ingest & Ask vẫn chung 1 address space (source-level)
// → giữ option "tách thành service riêng" để mở, chưa tốn ngay.
05 Trùng lặp THẬT vs trùng lặp ngẫu nhiên
Kiến trúc sư hay sa vào một cái bẫy: sợ trùng lặp. Nhưng có hai loại trùng lặp rất khác nhau — và gộp nhầm loại thứ hai sẽ tạo coupling không đáng có.
True duplication
- Mọi thay đổi ở bản này BẮT BUỘC lặp lại y hệt ở bản kia.
- Chúng thay đổi cùng nhau, vì cùng một lý do nghiệp vụ.
- Đây mới là trùng lặp đáng gộp (unify).
Accidental / false duplication
- Hai đoạn trông giống nhau nhưng tiến hóa theo đường khác nhau — đổi với tốc độ & lý do khác nhau.
- Vài năm sau quay lại sẽ thấy chúng đã rất khác nhau.
- Gộp vội ⟶ tách lại sau này là cực hình.
Hai use case có màn hình giống nhau, hay cấu trúc database record giống hệt screen view — phần lớn là trùng lặp ngẫu nhiên. Đừng tuồn thẳng record DB lên UI; hãy tạo view model riêng. Chống lại cám dỗ "knee-jerk elimination of duplication" — chỉ gộp khi chắc chắn là true duplication.
RAG · cùng format prompt ≠ phải gộp
RAG Use case "ingest" và "ask" tình cờ cùng dựng một prompt header trông giống nhau. Đừng vội rút ra hàm dùng chung — chúng sẽ tiến hóa khác hướng:
# Trông giống nhau HÔM NAY, nhưng đổi vì lý do khác nhau → KHÔNG gộp vội
def ingest_prompt(chunk): # đổi khi tinh chỉnh cách mô tả tài liệu để embed
return f"Tài liệu:\n{chunk}"
def ask_prompt(ctx, q): # đổi khi tinh chỉnh giọng trả lời, chống hallucination
return f"Ngữ cảnh:\n{ctx}\n\nCâu hỏi: {q}"
# Gộp lại = ép hai use case coupling; chỉ unify nếu CHẮC là true duplication.
06 Ghi nhớ nhanh
Kiến trúc = SHAPE (cách chia + sắp xếp + cách component giao tiếp). Kiến trúc sư giỏi nhất vẫn là lập trình viên — phải tự nếm vấn đề mình tạo ra.
Để mở các lựa chọn: tách policy (giá trị nghiệp vụ) khỏi detail (DB/web/framework/IO), trì hoãn quyết định chi tiết tới phút chót — tối đa hóa số quyết định CHƯA phải đưa ra.
Tách hai trục: layers ngang (UI/rules/DB) + use cases dọc (lát cắt). Tách tốt ⟶ phát triển & triển khai độc lập, hot-swap được.
Decoupling mode (source → deployment → service) là lựa chọn để mở: đẩy tới điểm CÓ THỂ tạo service, nhưng giữ cùng address space lâu nhất có thể. Mode sẽ đổi theo thời gian.
Đừng gộp trùng lặp ngẫu nhiên. Chỉ unify khi chắc là true duplication (luôn đổi cùng nhau vì cùng lý do). Giống tạm thời ⟶ giữ tách.