Software Architecture

Kiến trúc là gì? – Tại sao mọi quyết định kiến trúc đều là một sự đánh đổi (Trade-off)

7/19/2026 · 13p đọc


title: "Kiến trúc là gì? – Tại sao mọi quyết định kiến trúc đều là một sự đánh đổi (Trade-off)"
series: "Software Architecture Masterclass"
part: "Phần 1 — Nguyên lý & Triết lý Kiến trúc"
order: 1
audience: "Solutions Architect & Technical Lead"
reading_time: "13 phút"
tags: ["software-architecture", "trade-off", "architecture-principles", "technical-leadership", "decision-making"]

Kiến trúc là gì? – Tại sao mọi quyết định kiến trúc đều là một sự đánh đổi (Trade-off)

Một team backend mười hai người, sáu tháng trước, ngồi họp kiến trúc cho sản phẩm mới. Không ai tranh luận về nghiệp vụ — họ tranh luận về việc có nên tách microservices ngay từ đầu hay không. Phe A trích dẫn một bài blog của một công ty unicorn: "monolith là nợ kỹ thuật, tách sớm để tránh trả giá sau." Phe B trích một bài khác, cũng của một kiến trúc sư có tiếng: "đừng bao giờ bắt đầu bằng microservices, đó là early optimization." Cả hai phe đều đúng, và cả hai phe đều sai — vì cả hai đang tìm một câu trả lời tuyệt đối cho một câu hỏi vốn dĩ không có câu trả lời tuyệt đối.

Đây không phải chuyện hiếm. Tôi đã ngồi trong hàng chục cuộc họp kiến trúc như vậy, ở nhiều tổ chức khác nhau, và mẫu số chung luôn là: người ta tranh luận "cái nào tốt hơn" thay vì "cái nào phù hợp hơn cho bối cảnh của chúng ta, và ta chấp nhận mất gì để có được nó." Sự khác biệt giữa hai câu hỏi này chính là ranh giới giữa một kỹ sư giỏi công cụ và một kiến trúc sư thực thụ. Bài đầu tiên của series này không nói về một pattern cụ thể nào cả — nó thiết lập một cách nhìn sẽ xuyên suốt 39 bài còn lại: kiến trúc, xét đến cùng, là khoa học và nghệ thuật của sự đánh đổi có chủ đích.

Nếu bạn chỉ nhớ một điều sau khi đọc xong series này, hãy để đó là điều này: không có kiến trúc "tốt", chỉ có kiến trúc "phù hợp với ràng buộc hiện tại và được đánh đổi một cách có ý thức."

The Problem

Vấn đề không nằm ở việc thiếu kiến thức. Ngày nay, tài liệu về DDD, microservices, event-driven, CQRS, hexagonal architecture... đều sẵn có, chất lượng cao, miễn phí. Vấn đề nằm ở cách các đội kỹ thuật tiêu thụ kiến thức đó: họ biến các pattern thành giáo điều.

Biểu hiện cụ thể mà tôi quan sát được lặp đi lặp lại:

  • Một senior engineer từng làm ở công ty X thấy microservices "chạy tốt" ở đó, nên mặc định áp dụng cho mọi dự án tiếp theo, bất kể quy mô team hay độ phức tạp domain.
  • Một tech lead đọc "Clean Architecture" của Robert Martin, rồi ép toàn bộ codebase vào bốn lớp đồng tâm — kể cả một CRUD admin panel nội bộ có tuổi thọ dự kiến sáu tháng.
  • Một kiến trúc sư coi CQRS là "best practice cho hệ thống lớn", áp nó vào một module có traffic đọc/ghi gần như 1:1, và giờ team phải maintain hai model dữ liệu cho một nghiệp vụ đơn giản.

Cái sai không nằm ở bản thân pattern — microservices, Clean Architecture, CQRS đều là công cụ hợp lệ, được sinh ra để giải quyết những vấn đề thật. Cái sai là coi chúng như chân lý phổ quát thay vì giải pháp có điều kiện. Một "best practice" luôn đi kèm một tập giả định ngầm về bối cảnh nó được sinh ra: quy mô team, tốc độ tăng trưởng traffic, mức độ chín muồi của domain, ngân sách vận hành, khẩu vị rủi ro của tổ chức. Khi bối cảnh của bạn khác, best practice đó không tự động chuyển thành best practice cho bạn — nó có thể trở thành một khoản nợ kỹ thuật đắt đỏ.

Hệ quả thực tế của tư duy "chân lý tuyệt đối" này rất cụ thể: over-engineering ở nơi cần sự đơn giản, under-engineering ở nơi cần khả năng mở rộng, và tệ nhất — các cuộc tranh luận kiến trúc biến thành tranh luận tôn giáo (religious debate) thay vì phân tích kỹ thuật. Không ai hỏi "chúng ta đang đánh đổi cái gì lấy cái gì", người ta chỉ hỏi "cách nào đúng."

The Theory

Để thoát khỏi cái bẫy giáo điều đó, ta cần một định nghĩa chính xác về kiến trúc phần mềm — không phải định nghĩa để trang trí slide, mà định nghĩa có tính vận hành (operational definition), tức là dùng được để ra quyết định hàng ngày.

Kiến trúc phần mềm là tập hợp các quyết định cấu trúc có chi phí thay đổi cao (high cost of change) — những quyết định định hình cách các thành phần của hệ thống giao tiếp, phụ thuộc lẫn nhau và tiến hóa theo thời gian.

Ba từ khóa trong định nghĩa này đáng để mổ xẻ:

1. "Cấu trúc" — kiến trúc quan tâm đến ranh giới (boundaries) và mối quan hệ (relationships), không phải logic nghiệp vụ bên trong từng thành phần. Việc bạn viết một hàm tính thuế thế nào là chi tiết cài đặt (implementation detail); việc module tính thuế có được tách khỏi module đơn hàng hay không, giao tiếp qua đồng bộ hay bất đồng bộ, là quyết định kiến trúc.

2. "Chi phí thay đổi cao" — tiêu chí phân loại quan trọng nhất, và cũng hay bị bỏ qua nhất. Không phải mọi quyết định kỹ thuật đều là quyết định kiến trúc. Đổi tên biến, refactor một function, thay thư viện logging — đó là quyết định thiết kế (design decision), sửa trong vài giờ, ảnh hưởng cục bộ. Nhưng chọn giao thức giao tiếp giữa hai service, chọn mô hình dữ liệu chính (relational vs document vs graph), chọn ranh giới bounded context — đó là quyết định kiến trúc, vì đảo ngược nó đòi hỏi rewrite, migration dữ liệu, downtime, hoặc phối hợp nhiều team trong nhiều tháng. Câu hỏi kiểm tra nhanh: "Nếu sáu tháng nữa phát hiện sai, chi phí sửa là bao nhiêu?" Trả lời "một buổi chiều" — không phải quyết định kiến trúc. Trả lời "một quý và rủi ro downtime" — chắc chắn là.

3. "Giao tiếp, phụ thuộc, tiến hóa" — kiến trúc không tĩnh. Một quyết định đúng tại thời điểm T (team 5 người, traffic thấp, domain chưa rõ) có thể trở thành sai tại thời điểm T+2 năm (team 50 người, traffic x100, domain đã ổn định). Đây là lý do bài 5 của series này sẽ nói riêng về "cửa sổ thời gian của kiến trúc" — nhưng ngay từ bây giờ, cần khắc cốt: kiến trúc là quyết định gắn với một bối cảnh cụ thể tại một thời điểm cụ thể, không phải một cấu trúc lý tưởng vĩnh cửu.

Từ định nghĩa này suy ra hệ quả trung tâm của toàn bộ series: vì mọi quyết định kiến trúc đều là sự phân bổ nguồn lực hữu hạn (thời gian, độ phức tạp, chi phí vận hành, năng lực đội ngũ) cho một tập mục tiêu thường xung khắc nhau (tốc độ ra thị trường, khả năng mở rộng, chi phí, độ tin cậy, tốc độ phát triển tính năng), nên không tồn tại một kiến trúc tối ưu mọi tiêu chí cùng lúc. Đây không phải một giới hạn của kỹ năng kiến trúc sư — đây là một định lý toán học về tối ưu hóa đa mục tiêu (multi-objective optimization): khi hai mục tiêu xung khắc, tối ưu một mục tiêu bắt buộc phải hy sinh mục tiêu kia ở một mức độ nào đó. Công việc của kiến trúc sư không phải là "giải" bài toán này để tìm một điểm hoàn hảo không tồn tại, mà là chọn có ý thức điểm nào trên đường biên đánh đổi (trade-off frontier) là phù hợp nhất với bối cảnh hiện tại.

The Trade-off

Đây là phần quan trọng nhất của bài — và cũng là phần bị bỏ qua nhiều nhất trong các buổi review kiến trúc thực tế. Hãy mổ xẻ một cặp đánh đổi kinh điển nhất, cặp đánh đổi mà mọi kiến trúc sư chạm phải trong tuần đầu tiên đi làm: Simplicity (đơn giản) vs. Flexibility (linh hoạt).

Chọn đơn giản — được gì, mất gì?

Một kiến trúc đơn giản — ví dụ một monolith với schema database chuẩn hóa chặt, ít abstraction layer, gọi hàm trực tiếp thay vì qua message queue — mang lại:

  • Được: onboard developer mới nhanh (một codebase, không phải trace qua năm service); debug nhanh (stack trace một tiến trình, không phải distributed tracing); chi phí vận hành thấp (một deployment unit, không cần service mesh hay quản lý N connection pool); tốc độ ra tính năng ban đầu cao vì không mất công dựng hạ tầng giao tiếp liên service.
  • Mất: khi một phần hệ thống cần scale độc lập (module tìm kiếm cần 20 instance, module quản trị chỉ cần 1), bạn buộc phải scale toàn bộ monolith — lãng phí tài nguyên. Hai team muốn deploy độc lập theo nhịp riêng thì bị khóa chung một release train — bug nhỏ ở module A có thể chặn release của module B. Khi domain phức tạp lên, ranh giới module bên trong monolith dễ bị xói mòn (module A gọi thẳng vào internal của module B) nếu thiếu kỷ luật nghiêm ngặt — điều bài 11 (Monolith truyền thống) sẽ mổ xẻ sâu hơn.

Chọn linh hoạt (ví dụ tách service theo bounded context, giao tiếp qua event) — được gì, mất gì?

  • Được: từng phần hệ thống scale độc lập theo đúng tải thực tế; team sở hữu service riêng, deploy độc lập, chọn stack phù hợp bài toán riêng (Go cho service cần throughput cao, Node cho service cần tốc độ phát triển); ranh giới domain được cưỡng chế bởi network boundary — khó xói mòn hơn ranh giới trong cùng một process.
  • Mất: độ phức tạp vận hành tăng theo cấp số nhân — cần service discovery, distributed tracing, retry/circuit breaker, quản lý eventual consistency giữa các service (bài 22 sẽ nói kỹ), và một đội DevOps đủ trưởng thành để vận hành hạ tầng đó. Debug một request đi qua bốn service khó hơn nhiều so với một process. Chi phí hạ tầng cao hơn (nhiều container, database riêng, message broker). Quan trọng nhất — nếu ranh giới bounded context chọn sai (rất dễ xảy ra khi domain chưa chín), bạn trả giá bằng distributed refactoring, đắt hơn refactor trong monolith nhiều lần vì động đến network contract giữa các team.

Điểm mấu chốt: không có phe nào "thắng" một cách tuyệt đối. Một startup 5 người xây MVP để tìm product-market fit mà chọn tách microservices ngay từ đầu là đang trả trước một khoản chi phí vận hành khổng lồ cho một sự linh hoạt mà họ chưa cần — vì domain còn đang thay đổi hàng tuần, ranh giới context nào cũng có thể sai trong ba tháng tới. Ngược lại, một hệ thống thanh toán ở quy mô hàng triệu giao dịch/ngày mà vẫn cố giữ monolith với lý do "đơn giản dễ maintain" đang chịu rủi ro vận hành nghiêm trọng hơn — một bug ở module không liên quan cũng có thể kéo sập luồng thanh toán, và team không thể scale độc lập phần chịu tải cao nhất.

Cùng logic đó áp dụng cho mọi cặp đánh đổi khác mà series này sẽ đi qua: consistency vs. availability (bài 21 – CAP theorem), tốc độ phát triển vs. an toàn kiểu dữ liệu, chi phí hạ tầng vs. độ trễ (bài 23 – caching), tốc độ ra tính năng vs. nợ kỹ thuật (bài 37). Không có công thức nào cho ra câu trả lời đúng tuyệt đối — chỉ có phân tích ngữ cảnh nghiêm túc.

The Architecture Diagram

Sơ đồ dưới đây minh họa cách một kiến trúc sư nên tư duy khi đứng trước một điểm rẽ nhánh: mỗi lựa chọn không dẫn đến "đúng" hay "sai", mà dẫn đến một tập hợp cụ thể "được gì / mất gì" cần cân nhắc với bối cảnh thực tế.

flowchart TD
    A["Quyết định kiến trúc:<br/>Monolith hay tách service theo domain?"]

    A -->|"Chọn: giữ Monolith"| B["Nhánh Simplicity"]
    A -->|"Chọn: tách Microservices"| C["Nhánh Flexibility"]

    B --> B1["✅ Được:<br/>- Onboard nhanh, 1 codebase<br/>- Debug đơn giản (1 process)<br/>- Chi phí vận hành thấp<br/>- Tốc độ ra tính năng ban đầu cao"]
    B --> B2["❌ Mất:<br/>- Không scale độc lập từng phần<br/>- Team bị khóa chung release train<br/>- Ranh giới module dễ bị xói mòn<br/>- Rủi ro 'big ball of mud' khi domain lớn dần"]

    C --> C1["✅ Được:<br/>- Scale độc lập theo tải thực tế<br/>- Team tự chủ deploy, tự chọn stack<br/>- Ranh giới domain được network cưỡng chế"]
    C --> C2["❌ Mất:<br/>- Độ phức tạp vận hành tăng mạnh<br/>- Cần hạ tầng: tracing, retry, service mesh<br/>- Eventual consistency giữa các service<br/>- Sai ranh giới context = refactor rất đắt"]

    B1 --> D{"Bối cảnh hiện tại là gì?<br/>- Team size?<br/>- Domain đã chín chưa?<br/>- Tải & tốc độ tăng trưởng?<br/>- Năng lực vận hành của tổ chức?"}
    B2 --> D
    C1 --> D
    C2 --> D

    D --> E["Ghi lại đánh đổi đã chấp nhận<br/>(→ ADR, xem Bài 6)"]

    style A fill:#2b6cb0,color:#fff
    style D fill:#805ad5,color:#fff
    style E fill:#2f855a,color:#fff

Điều đáng chú ý trong sơ đồ: cả hai nhánh đều có ô "✅ Được" và "❌ Mất" — không nhánh nào chỉ toàn dấu cộng. Nếu bạn thấy mình vẽ một cây quyết định kiến trúc thực tế mà một nhánh không có ô "mất" nào, đó là dấu hiệu bạn đang thiên vị (bias) chứ không đang phân tích.

Implementation Guardrails

Từ lý thuyết đến thực hành, có bốn quy tắc cứng nên áp dụng ngay từ dự án đầu tiên:

  1. Mọi quyết định kiến trúc có chi phí thay đổi cao phải được viết ra dưới dạng đánh đổi tường minh, không phải phát biểu một chiều. Cụ thể: khi đề xuất một hướng kiến trúc, bắt buộc phải trả lời câu hỏi "chúng ta chấp nhận mất gì" trước khi trình bày "được gì" — không phải sau. Nếu tài liệu đề xuất chỉ liệt kê lợi ích, nó chưa đủ điều kiện để review.

  2. Xác định rõ chi phí đảo ngược (reversal cost) trước khi quyết định. Với mỗi lựa chọn kiến trúc, hỏi: "nếu 6-12 tháng nữa phát hiện sai, chi phí sửa là gì — giờ, tuần, hay quý?" Quyết định có chi phí đảo ngược tính bằng quý (chọn database chính, ranh giới bounded context, giao thức giao tiếp liên service) cần review kỹ hơn nhiều lần so với quyết định tính bằng giờ (đặt tên module, chọn thư viện logging).

  3. Không import "best practice" mà không đối chiếu bối cảnh gốc của nó. Trước khi áp dụng một pattern vì "công ty X làm vậy", bắt buộc trả lời: quy mô team của họ khi áp dụng pattern đó là bao nhiêu, giai đoạn sản phẩm (MVP hay ở scale) là gì, và điều đó có khớp với bối cảnh hiện tại của bạn không. Nếu không khớp, pattern đó không tự động chuyển giao giá trị.

  4. Mọi đánh đổi đã chấp nhận phải được ghi lại có cấu trúc, gắn với ngày và bối cảnh ra quyết định — không nằm rải rác trong Slack hay trí nhớ cá nhân. Đây chính là tiền đề trực tiếp cho Architecture Decision Record (ADR) sẽ được trình bày chi tiết ở Bài 6: một kiến trúc sư giỏi không chỉ ra quyết định đúng, mà còn để lại dấu vết để người kế nhiệm hiểu tại sao — vì bối cảnh sẽ thay đổi, và quyết định cần được xét lại đúng lúc, không phải bị coi là bất biến mãi mãi.

Architect's Verdict

Nếu bạn rời cuộc họp kiến trúc mà cảm thấy mình vừa tìm ra "giải pháp tốt nhất", hãy dừng lại và hỏi lại: bạn đã liệt kê cái giá phải trả chưa, hay mới chỉ liệt kê lợi ích? Công việc của một kiến trúc sư không phải là tìm kiến trúc tốt nhất trong chân không — kiến trúc "tốt nhất" theo nghĩa tuyệt đối không tồn tại. Công việc thực sự là tìm đánh đổi phù hợp nhất với ràng buộc hiện tại: quy mô team, giai đoạn sản phẩm, năng lực vận hành, và khẩu vị rủi ro của tổ chức — rồi có đủ kỷ luật để ghi lại đánh đổi đó, và đủ khiêm tốn để xét lại nó khi bối cảnh thay đổi. Một kiến trúc sư giỏi không phải người biết nhiều pattern nhất; đó là người từ chối trả lời "cái nào đúng" và luôn hỏi lại "đúng cho ai, trong bối cảnh nào, với cái giá gì."

🔗 Bài viết liên quan


Bài tiếp theo: First Principles Thinking cho Software Architect

Kiến trúc là gì? – Tại sao mọi quyết định kiến trúc đều là một sự đánh đổi (Trade-off)