<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Architecture · Nhân Tài Đức</title>
    <link>https://jared.lynskey.co.nz/vi/tags/architecture/</link>
    <description>Những câu chuyện ngắn về kỹ thuật phần mềm và lãnh đạo đội ngũ kỹ sư — performance review, tuyển dụng và mở rộng team, CI/CD, giám sát DevOps và AI agent. Viết từ Seoul.</description>
    <generator>Hugo</generator>
    <language>vi</language>
    <copyright>© 2026 Nhân Tài Đức</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/vi/tags/architecture/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Tài liệu ra quyết định (decision document) là gì? Hướng dẫn thực tế kèm mẫu</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2024/2024-12-03-decision-docs/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2024/2024-12-03-decision-docs/</guid>
      <pubDate>Tue, 03 Dec 2024 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>decision-making</category>
      <category>documentation</category>
      <category>ADR</category>
      <category>architecture</category>
      <category>team alignment</category>
      <description>Tài liệu ra quyết định (decision document) ghi lại một lựa chọn ngay lúc đưa ra: bối cảnh, các phương án đã cân nhắc và lý do chọn, để sáu tháng sau không ai phải tranh luận lại. Cách viết ADR và RFC mà mọi người thật sự dùng, kèm ADR mẫu và mẫu tài liệu.</description>
      <content:encoded><![CDATA[<p>Đội nào cũng phải ra quyết định. Nhưng phần lớn các đội lại quên mất vì sao mình đã quyết như vậy. Sáu tháng sau, có người hỏi &ldquo;sao hồi đó mình chọn PostgreSQL mà không chọn MongoDB?&rdquo;, chẳng ai còn nhớ những ràng buộc lúc ấy, thế là cả cuộc tranh luận lại diễn ra từ đầu, mà thường còn ít người tham gia hơn lần trước.</p>
<p>Tài liệu ra quyết định sinh ra để giải quyết đúng chuyện đó. Gọi là ADR, RFC hay decision log cũng được, cái tên không quan trọng. Quan trọng là ghi lại bối cảnh, các phương án và lập luận ngay tại thời điểm lựa chọn. Năm nay, khi quản lý một đội lập trình viên, tôi càng thấm giá trị của loại tài liệu này hơn bao giờ hết: có nó thì tổ chức có trí nhớ thật, không có thì chỉ còn lại những lời truyền miệng.</p>
<hr>

<h3 class="relative group">Vì sao đáng bỏ công
    <div id="vì-sao-đáng-bỏ-công" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%ac-sao-%c4%91%c3%a1ng-b%e1%bb%8f-c%c3%b4ng" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Cái lợi dễ thấy nhất là không phải cãi nhau về cùng một chuyện hai lần. &ldquo;Ơ, chiến lược quản lý phiên bản (versioning) cho API mình chốt rồi mà nhỉ?&rdquo; Khi có một bản ghi rõ ràng về điều đã quyết và lý do, chuyện gì đã xong sẽ nằm yên, và cả đội dồn sức vào làm thay vì quay lại đi con đường cũ.</p>
<p>Cái lợi âm thầm hơn thì phải một thời gian mới lộ ra. Dự án kéo dài hàng tháng, hàng năm, người đến rồi người đi, thứ còn lại chỉ là mã nguồn. Mã nguồn cho bạn biết <em>điều gì</em> đã được quyết, nhưng không bao giờ cho biết <em>vì sao</em>. Tài liệu ra quyết định thì giữ lại được lập luận: lúc đó có những ràng buộc gì, phương án nào đã bị loại, ai là người chốt. Người mới vào có bối cảnh để hiểu, thay vì chỉ nghe kể lại. Những quyết định liên quan tới nhiều đội cũng nhận được sự đồng thuận thật, vì ai cũng thấy các yếu tố đã được cân nhắc thế nào, chứ không chỉ thấy kết luận cuối cùng. Và khi tài liệu ghi rõ ai ra quyết định, ai đóng góp ý kiến, thì phần việc tiếp theo sẽ có người thật sự chịu trách nhiệm.</p>
<hr>

<h3 class="relative group">Khi nào nên viết
    <div id="khi-nào-nên-viết" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#khi-n%c3%a0o-n%c3%aan-vi%e1%ba%bft" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Không phải quyết định nào cũng đáng một tài liệu. Tôi chỉ viết khi quyết định đó:</p>
<ul>
<li><strong>Tốn kém nếu muốn quay đầu</strong>: chọn bộ công nghệ (tech stack) như framework, cơ sở dữ liệu hay nhà cung cấp điện toán đám mây; chọn mẫu kiến trúc (vi dịch vụ (microservices) hay nguyên khối (monolith), hướng sự kiện hay yêu cầu-phản hồi); chọn nhà cung cấp bên thứ ba; hay cách tiếp cận về bảo mật và tuân thủ</li>
<li><strong>Có tác động lâu dài</strong>: hợp đồng API mà các hệ thống bên ngoài phụ thuộc vào, mô hình dữ liệu trải qua nhiều dịch vụ, hoặc những thay đổi phá vỡ các mẫu thiết kế đã thành nếp</li>
<li><strong>Liên quan tới nhiều đội</strong>: bất cứ thứ gì cần nhiều đội hoặc các bên liên quan không làm kỹ thuật cùng đồng ý, các quyết định về phân bổ nguồn lực và thứ tự ưu tiên</li>
<li><strong>Là kết quả của một đợt <a href="https://jared.lynskey.co.nz/vi/posts/2023/2023-10-18-spike/" >spike</a></strong>: khi một đợt tìm hiểu có giới hạn thời gian vừa kết thúc, hãy ghi lại những gì tìm được và quyết định rút ra trước khi mọi người quên sạch</li>
</ul>
<p>Với những quyết định dễ đảo ngược, những chi tiết triển khai nội bộ trong đội, hay những thứ gần như không ảnh hưởng ra ngoài phạm vi trực tiếp của bạn, thì bỏ qua thủ tục. Một tài liệu không ai cần tới thì chỉ là bài tập về nhà.</p>
<hr>

<h3 class="relative group">Vòng đời
    <div id="vòng-đời" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%b2ng-%c4%91%e1%bb%9di" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
</span></span><span class="line"><span class="cl">│   Draft     │ ──► │   Review    │ ──► │  Approved   │ ──► │ Implemented │
</span></span><span class="line"><span class="cl">│  Proposed   │     │  Feedback   │     │  Accepted   │     │  Completed  │
</span></span><span class="line"><span class="cl">└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
</span></span><span class="line"><span class="cl">                           │
</span></span><span class="line"><span class="cl">                           ▼
</span></span><span class="line"><span class="cl">                    ┌─────────────┐
</span></span><span class="line"><span class="cl">                    │  Rejected/  │
</span></span><span class="line"><span class="cl">                    │  Superseded │
</span></span><span class="line"><span class="cl">                    └─────────────┘</span></span></code></pre></div></div>
<p>Người viết phác thảo vấn đề và các giải pháp có thể chọn. Sau đó các bên liên quan đọc và góp ý. Đây là giai đoạn làm nên giá trị của cả quá trình, vì đây là lúc các giả định bị chất vấn và những điểm mù lộ ra. Người ra quyết định (thường là trưởng nhóm kỹ thuật, kiến trúc sư hoặc người phụ trách sản phẩm) chốt lần cuối, đội bắt tay triển khai, và nếu thực tế buộc phải thay đổi giữa chừng thì tài liệu được cập nhật theo. Có tài liệu chết ngay ở vòng góp ý, có tài liệu về sau bị thay thế khi hoàn cảnh đổi khác. Trường hợp nào cũng phải đánh dấu cho rõ, vì một tài liệu &ldquo;accepted&rdquo; đã lỗi thời chính là cái bẫy cho người tiếp theo tìm thấy nó.</p>
<hr>

<h3 class="relative group">Tài liệu thế nào thì đáng đọc
    <div id="tài-liệu-thế-nào-thì-đáng-đọc" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#t%c3%a0i-li%e1%bb%87u-th%e1%ba%bf-n%c3%a0o-th%c3%ac-%c4%91%c3%a1ng-%c4%91%e1%bb%8dc" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Bắt đầu từ vấn đề, đừng bắt đầu từ giải pháp bạn thích nhất. Mình đang giải quyết chuyện gì, vì sao lúc này chuyện đó quan trọng, và nếu không làm gì thì sẽ ra sao? Khi mọi người còn chưa thống nhất vấn đề là gì, thì tranh luận về giải pháp chỉ là diễn kịch.</p>
<p>Tiếp theo, đưa ra các phương án thật sự, ít nhất hai hoặc ba, kể cả khi bạn đã nghiêng hẳn về một cái. Mỗi phương án cần có: mô tả ngắn, ưu và nhược điểm, ước lượng công sức sơ bộ (ước theo cỡ áo S/M/L/XL là đủ), cùng các rủi ro và cách bạn sẽ giảm thiểu chúng. Có ghi lại các phương án thay thế thì người đọc sau này mới hiểu vì sao phương án được chọn lại thắng.</p>
<p>Nói thẳng ra những đánh đổi. &ldquo;Chúng tôi chọn ra thị trường nhanh hơn, chấp nhận kiến trúc không được đẹp.&rdquo; &ldquo;Chúng tôi chấp nhận vận hành phức tạp hơn để đổi lấy khả năng mở rộng quy mô tốt hơn.&rdquo; &ldquo;Chúng tôi ưu tiên trải nghiệm của lập trình viên hơn hiệu năng thuần.&rdquo; Chỉ vài câu như vậy thôi đã giúp người đọc sau này nhiều hơn cả chục trang phân tích.</p>
<p>Neo cuộc thảo luận vào những thứ cụ thể: đoạn mã minh hoạ giải pháp sẽ trông ra sao, sơ đồ tương tác giữa các hệ thống, liên kết tới các bản thử nghiệm khái niệm (proof of concept) từ những đợt <a href="https://jared.lynskey.co.nz/vi/posts/2023/2023-10-18-spike/" >spike</a>. Thảo luận chung chung thì chỉ sinh ra quyết định chung chung.</p>
<p>Cuối cùng, đặt hạn chót: khi nào phải ra quyết định, ai phải góp ý trước ngày nào, và những việc gì đang bị kẹt lại trong lúc chờ. Quyết định mà không có hạn chót thì sẽ bị treo mãi.</p>
<hr>

<h3 class="relative group">Ví dụ: Architecture Decision Record (ADR)
    <div id="ví-dụ-architecture-decision-record-adr" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%ad-d%e1%bb%a5-architecture-decision-record-adr" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Dưới đây là một ví dụ thực tế về tài liệu ra quyết định:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># ADR-001: API Authentication Strategy
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Status:**</span> Accepted
</span></span><span class="line"><span class="cl"><span class="gs">**Date:**</span> 2024-11-15
</span></span><span class="line"><span class="cl"><span class="gs">**Decision Maker:**</span> Sarah Chen (Platform Architect)
</span></span><span class="line"><span class="cl"><span class="gs">**Contributors:**</span> Backend Team, Security Team, Mobile Team
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Context
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Our public API currently uses API keys for authentication. As we expand to
</span></span><span class="line"><span class="cl">support third-party integrations and mobile apps, we need a more robust
</span></span><span class="line"><span class="cl">authentication mechanism that supports:
</span></span><span class="line"><span class="cl"><span class="k">-</span> Token expiration and refresh
</span></span><span class="line"><span class="cl"><span class="k">-</span> Scoped permissions
</span></span><span class="line"><span class="cl"><span class="k">-</span> User-level authentication (not just service-level)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Decision
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">We will implement OAuth 2.0 with JWT tokens for API authentication.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Options Considered
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 1: OAuth 2.0 with JWT (Recommended)
</span></span></span><span class="line"><span class="cl">**Description:** Industry-standard protocol with self-contained tokens
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Industry standard, well-documented | More complex initial implementation |
</span></span><span class="line"><span class="cl">| Self-contained tokens reduce database lookups | Tokens cannot be revoked instantly |
</span></span><span class="line"><span class="cl">| Broad library support | Requires refresh token management |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Medium (2-3 sprints)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 2: Session-based Authentication
</span></span></span><span class="line"><span class="cl">**Description:** Traditional server-side sessions with cookies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Simple to implement | Not suitable for mobile apps |
</span></span><span class="line"><span class="cl">| Easy to revoke sessions | Requires sticky sessions or shared session store |
</span></span><span class="line"><span class="cl">| Familiar to most developers | Doesn&#39;t scale as well |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Small (1 sprint)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 3: Custom Token System
</span></span></span><span class="line"><span class="cl">**Description:** Build our own token-based authentication
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Fully customizable | Reinventing the wheel |
</span></span><span class="line"><span class="cl">| No external dependencies | Security risks from custom implementation |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Large (4+ sprints)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Consequences
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Mobile team can implement standard OAuth flows
</span></span><span class="line"><span class="cl"><span class="k">-</span> We&#39;ll need to set up a token refresh mechanism
</span></span><span class="line"><span class="cl"><span class="k">-</span> API documentation will need updates for OAuth flows
</span></span><span class="line"><span class="cl"><span class="k">-</span> Existing API key users will need a migration path (6-month deprecation)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## References
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> [<span class="nt">OAuth 2.0 RFC 6749</span>](<span class="na">https://tools.ietf.org/html/rfc6749</span>)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Internal spike document: [<span class="nt">Authentication Options Spike</span>](<span class="na">/spikes/auth-spike-2024</span>)
</span></span><span class="line"><span class="cl">- Security team review: SEC-2024-042</span></span></code></pre></div></div>
<hr>

<h3 class="relative group">Mẫu tài liệu, cứ lấy về mà dùng
    <div id="mẫu-tài-liệu-cứ-lấy-về-mà-dùng" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#m%e1%ba%abu-t%c3%a0i-li%e1%bb%87u-c%e1%bb%a9-l%e1%ba%a5y-v%e1%bb%81-m%c3%a0-d%c3%b9ng" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Lấy mẫu (template) này làm điểm xuất phát, rồi cắt gọt cho vừa với đội của bạn:</p>
<table>
	<thead>
			<tr>
					<th><strong>Mục</strong></th>
					<th><strong>Nội dung</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Tên quyết định/vấn đề</strong></td>
					<td>[Tiêu đề ngắn gọn, nói rõ nội dung]</td>
			</tr>
			<tr>
					<td><strong>Trạng thái</strong></td>
					<td>[Draft / In Review / Accepted / Rejected / Superseded]</td>
			</tr>
			<tr>
					<td><strong>Mức độ ảnh hưởng</strong></td>
					<td>[Cao / Trung bình / Thấp]</td>
			</tr>
			<tr>
					<td><strong>Người phụ trách</strong></td>
					<td>[Người dẫn dắt quá trình ra quyết định]</td>
			</tr>
			<tr>
					<td><strong>Người ra quyết định</strong></td>
					<td>[Người có quyền quyết cuối cùng]</td>
			</tr>
			<tr>
					<td><strong>Hạn chót</strong></td>
					<td>[Ngày phải ra quyết định]</td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">Mô tả vấn đề
    <div id="mô-tả-vấn-đề" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#m%c3%b4-t%e1%ba%a3-v%e1%ba%a5n-%c4%91%e1%bb%81" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<p><em>Mình đang giải quyết vấn đề gì? Vì sao nó quan trọng? Không làm gì thì phải trả giá thế nào?</em></p>

<h4 class="relative group">Bối cảnh
    <div id="bối-cảnh" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#b%e1%bb%91i-c%e1%ba%a3nh" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<p><em>Nêu bối cảnh, lịch sử và các chi tiết liên quan. Dẫn liên kết tới những tài liệu liên quan, các quyết định trước đó, hoặc kết quả spike.</em></p>

<h4 class="relative group">Ràng buộc
    <div id="ràng-buộc" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#r%c3%a0ng-bu%e1%bb%99c" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<p><em>Giải pháp nào cũng phải tuân theo những giới hạn hay yêu cầu gì?</em></p>
<ul>
<li>Ràng buộc về ngân sách</li>
<li>Yêu cầu về tiến độ</li>
<li>Ràng buộc kỹ thuật (hệ thống hiện có, kỹ năng của đội)</li>
<li>Yêu cầu về tuân thủ hoặc bảo mật</li>
</ul>

<h4 class="relative group">Các giải pháp đã cân nhắc
    <div id="các-giải-pháp-đã-cân-nhắc" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1c-gi%e1%ba%a3i-ph%c3%a1p-%c4%91%c3%a3-c%c3%a2n-nh%e1%ba%afc" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<table>
	<thead>
			<tr>
					<th><strong>Phương án</strong></th>
					<th><strong>Mô tả</strong></th>
					<th><strong>Ưu điểm</strong></th>
					<th><strong>Nhược điểm</strong></th>
					<th><strong>Công sức</strong></th>
					<th><strong>Rủi ro</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Phương án 1</strong></td>
					<td><em>Mô tả</em></td>
					<td><em>Ưu A, Ưu B</em></td>
					<td><em>Nhược A, Nhược B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>Thấp/TB/Cao</em></td>
			</tr>
			<tr>
					<td><strong>Phương án 2</strong></td>
					<td><em>Mô tả</em></td>
					<td><em>Ưu A, Ưu B</em></td>
					<td><em>Nhược A, Nhược B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>Thấp/TB/Cao</em></td>
			</tr>
			<tr>
					<td><strong>Phương án 3</strong></td>
					<td><em>Mô tả</em></td>
					<td><em>Ưu A, Ưu B</em></td>
					<td><em>Nhược A, Nhược B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>Thấp/TB/Cao</em></td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">Đề xuất
    <div id="đề-xuất" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91%e1%bb%81-xu%e1%ba%a5t" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<p><em>Nêu phương án bạn đề xuất, và tóm tắt vì sao với các ràng buộc và đánh đổi hiện có, đó là lựa chọn tốt nhất.</em></p>

<h4 class="relative group">Quyết định
    <div id="quyết-định" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#quy%e1%ba%bft-%c4%91%e1%bb%8bnh" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<p><em>Chốt xong thì ghi lại quyết định cuối cùng. Nếu khác với đề xuất, giải thích lý do.</em></p>

<h4 class="relative group">Hệ quả
    <div id="hệ-quả" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#h%e1%bb%87-qu%e1%ba%a3" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<p><em>Quyết định này kéo theo những gì? Cần làm tiếp những việc gì?</em></p>

<h4 class="relative group">Việc cần làm
    <div id="việc-cần-làm" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#vi%e1%bb%87c-c%e1%ba%a7n-l%c3%a0m" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<table>
	<thead>
			<tr>
					<th><strong>Việc cần làm</strong></th>
					<th><strong>Người phụ trách</strong></th>
					<th><strong>Hạn chót</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><em>Việc 1</em></td>
					<td><em>Tên</em></td>
					<td><em>Ngày</em></td>
			</tr>
			<tr>
					<td><em>Việc 2</em></td>
					<td><em>Tên</em></td>
					<td><em>Ngày</em></td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">Tài liệu tham khảo
    <div id="tài-liệu-tham-khảo" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#t%c3%a0i-li%e1%bb%87u-tham-kh%e1%ba%a3o" aria-label="Cọc dấu">#</a>
    </span>
    
</h4>
<p><em>Liên kết tới các tài liệu liên quan, nguồn bên ngoài, kết quả spike, hoặc các quyết định trước đó.</em></p>
<hr>

<h3 class="relative group">Làm sao để thói quen này bám rễ
    <div id="làm-sao-để-thói-quen-này-bám-rễ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#l%c3%a0m-sao-%c4%91%e1%bb%83-th%c3%b3i-quen-n%c3%a0y-b%c3%a1m-r%e1%bb%85" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Lưu tài liệu ở một chỗ cố định, chẳng hạn một thư mục riêng trong kho mã nguồn (repo) hay một không gian trên wiki, vì một quyết định không ai tìm thấy thì cũng như chưa từng có. Viết thật gọn nhẹ thôi; tài liệu mà mất cả mấy ngày mới viết xong thì sẽ chẳng bao giờ có ai viết. Nhắc tới chúng trong chú thích trong mã, thông điệp commit và pull request, để phần &ldquo;vì sao&rdquo; luôn đi liền với phần &ldquo;cái gì&rdquo;. Khi một quyết định bị thay thế, đừng xoá tài liệu cũ, mà hãy đánh dấu nó là superseded rồi dẫn liên kết sang tài liệu mới; giữ lại lịch sử mới là mục đích. Và hãy mang các quyết định cũ ra xem lại trong những <a href="https://jared.lynskey.co.nz/vi/posts/2023/2023-09-01-retrospecs/" >buổi họp nhìn lại (retrospective)</a>: cái nào vẫn còn đứng vững, cái nào bây giờ bạn sẽ quyết khác đi.</p>
<p>Những kiểu thất bại thì cũng dễ đoán y như vậy. Phân tích mãi mà không quyết được: hãy đặt hạn chót, quyết dựa trên thông tin đang có, và nhớ rằng phần lớn quyết định đều có thể xem xét lại. Góp ý cho có, chỉ để đóng dấu mà không bao giờ có phản biện thật, thì chỉ thêm quy trình chứ không thêm giá trị. Tài liệu viết xong rồi bỏ đó, không dẫn tới hành động nào, còn tệ hơn là không có tài liệu. Quá nhiều chi tiết cũng là một lỗi: tài liệu nên nói về vì sao và cái gì, không phải làm thế nào; phần triển khai là việc của bản đặc tả kỹ thuật. Và khi các bên liên quan bất đồng, hãy ghi lại ý kiến phản đối. Quyết định có thể vẫn giữ nguyên, nhưng chính lời phản đối đó là bối cảnh mà một năm sau sẽ có người cần tới.</p>
<p>Hãy bắt đầu từ quyết định quan trọng tiếp theo của bạn. Cứ viết ra, dù còn thô, rồi vừa làm vừa chỉnh lại quy trình. Mỗi cuộc họp &ldquo;sao hồi đó mình chọn X?&rdquo; không cần diễn ra nữa vì ai đó chỉ việc mở ADR ra đọc, là thêm một tiếng đồng hồ đội bạn lấy lại được.</p>
<hr>

<h3 class="relative group">Đọc thêm
    <div id="đọc-thêm" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c4%91%e1%bb%8dc-th%c3%aam" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p><strong>Bài viết liên quan trên blog này:</strong></p>
<ul>
<li><a href="https://jared.lynskey.co.nz/vi/posts/2023/2023-10-18-spike/" >Tìm hiểu về spike trong phát triển phần mềm Agile</a> - làm một đợt spike để thu thập thông tin trước khi chốt quyết định lớn</li>
<li><a href="https://jared.lynskey.co.nz/vi/posts/2023/2023-09-01-retrospecs/" >Họp nhìn lại: sức mạnh của vòng lặp phản hồi</a> - xem lại các quyết định cũ như một phần của quá trình cải tiến liên tục</li>
<li><a href="https://jared.lynskey.co.nz/vi/posts/2023/2023-10-08-planning-an-api/" >Lên kế hoạch cho một API</a> - áp dụng tư duy tài liệu ra quyết định vào thiết kế API</li>
<li><a href="https://jared.lynskey.co.nz/vi/posts/2024/2024-01-04-githubforprojectmanagement/" >Dùng GitHub để quản lý dự án</a> - để tài liệu ra quyết định nằm ngay cạnh mã nguồn</li>
</ul>
<p><strong>Tài liệu bên ngoài:</strong></p>
<ul>
<li><a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions"  target="_blank" rel="noreferrer">Documenting Architecture Decisions - Michael Nygard</a> - bài viết gốc đã giúp ADR trở nên phổ biến</li>
<li><a href="https://adr.github.io/"  target="_blank" rel="noreferrer">ADR GitHub Organization</a> - công cụ, mẫu tài liệu và ví dụ cho Architecture Decision Record</li>
<li><a href="https://www.industrialempathy.com/posts/design-docs-at-google/"  target="_blank" rel="noreferrer">Design Docs at Google</a> - cách Google viết tài liệu thiết kế</li>
<li><a href="https://rust-lang.github.io/rfcs/"  target="_blank" rel="noreferrer">Quy trình RFC của Rust</a> - quy trình RFC của ngôn ngữ Rust, được ghi chép rất kỹ</li>
<li><a href="https://engineering.atspotify.com/2020/04/when-should-i-write-an-architecture-decision-record/"  target="_blank" rel="noreferrer">Mẫu Decision Record của Spotify</a> - góc nhìn của Spotify về chuyện khi nào nên viết ADR</li>
</ul>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-12-03-decision-docs/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>SaaS single-tenant hay multi-tenant: khi nào nên chọn cái nào</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2024/2024-04-07-singletenantsingleinstance/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2024/2024-04-07-singletenantsingleinstance/</guid>
      <pubDate>Sat, 06 Apr 2024 00:30:03 +0000</pubDate>
      <dc:creator>Me</dc:creator>
      <category>SaaS</category>
      <category>architecture</category>
      <category>single-tenant</category>
      <category>multi-tenant</category>
      <description>SaaS single-tenant (multi-instance) và multi-tenant khác nhau thế nào: so sánh về cô lập dữ liệu, chi phí và vận hành, các phương án ở giữa, khi nào nên chọn cái nào, cách tách tenant trong một cơ sở dữ liệu dùng chung, và những công cụ giúp bạn trụ được nếu đi theo hướng mỗi khách hàng một instance.</description>
      <content:encoded><![CDATA[<p>SaaS single-tenant cấp cho mỗi khách hàng một bản ứng dụng và cơ sở dữ liệu riêng. SaaS multi-tenant cho tất cả khách hàng dùng chung một ứng dụng và một cơ sở dữ liệu, phân biệt nhau bằng mã tenant (tenant ID), và mã này phải được kiểm tra trong mọi truy vấn. Single-tenant mang lại sự cô lập, quyền kiểm soát riêng cho từng khách hàng và giúp việc đáp ứng các yêu cầu tuân thủ (compliance) dễ hơn, đổi lại bạn phải vận hành cả một loạt bản sao. Multi-tenant rẻ hơn và vận hành đơn giản hơn nhiều, nhưng việc giữ dữ liệu của các khách hàng tách biệt nhau trở thành trách nhiệm của code. Phần lớn sản phẩm bắt đầu với multi-tenant, rồi chỉ cung cấp instance riêng cho số ít khách hàng thật sự cần.</p>
<p>Sản phẩm SaaS B2B nào sớm muộn cũng phải đối mặt với câu hỏi về tenancy, tức các khách hàng (tenant) dùng chung hay tách riêng hạ tầng, thường là vào cái ngày có một khách hàng lớn tiềm năng hỏi xem dữ liệu của họ có được lưu tách riêng khỏi các khách hàng khác không. Tôi đã chứng kiến cuộc thảo luận này ở hơn một công ty, và lần nào câu trả lời cũng quy về hai lựa chọn: cho mỗi khách hàng một instance riêng (single-tenant, multi-instance), hoặc cho tất cả chạy chung trên một instance (multi-tenant, single-instance). Không có lựa chọn nào là &ldquo;đúng&rdquo;. Mỗi cái chỉ gặp sự cố theo một kiểu khác nhau.</p>

<h2 class="relative group">Thiết kế hạ tầng
    <div id="thiết-kế-hạ-tầng" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thi%e1%ba%bft-k%e1%ba%bf-h%e1%ba%a1-t%e1%ba%a7ng" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<figure class="diagram">
  <div role="img" aria-label="Khác biệt giữa Single-Tenant và Multi-Tenant"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 790" aria-hidden="true" focusable="false">
<defs><marker id="tn-ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="d-head" d="M0,0 L10,5 L0,10 z"/></marker></defs>
<text class="d-title" x="320" y="164" text-anchor="middle" font-size="42">Multi-instance</text>
<text class="d-sub" x="320" y="204" text-anchor="middle" font-size="24">single-tenant: mỗi khách hàng một bản riêng</text>
<rect class="d-panel" x="12" y="234" width="616" height="304" rx="36"/>
<rect class="d-group" x="25" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="33" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="118.0" y="336.92" text-anchor="middle" font-size="22">Cơ sở dữ liệu</text>
<polygon class="d-badge" points="118.0,256.0 143.7,274.7 133.9,304.8 102.1,304.8 92.3,274.7"/>
<g class="d-icon">
<ellipse cx="118" cy="273" rx="10" ry="4"/>
<path d="M108,273 v15 a10,4 0 0 0 20,0 v-15 M108,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="33" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="118.0" y="475.64" text-anchor="middle" font-size="24">Ứng dụng</text>
<polygon class="d-badge" points="118.0,394.0 143.7,412.7 133.9,442.8 102.1,442.8 92.3,412.7"/>
<g class="d-icon">
<rect x="110" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="114.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M114,411 v-4 M114,427 v4 M110,415 h-4 M126,415 h4 M118,411 v-4 M118,427 v4 M110,419 h-4 M126,419 h4 M122,411 v-4 M122,427 v4 M110,423 h-4 M126,423 h4"/>
</g>
<line class="d-line" x1="118" y1="508" x2="118" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="84" cy="710.88" r="14.44"/><path d="M58.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="152" cy="710.88" r="14.44"/><path d="M126.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="118" cy="704.0" r="19.0"/><path d="M84.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<rect class="d-group" x="227" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="235" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="320.0" y="336.92" text-anchor="middle" font-size="22">Cơ sở dữ liệu</text>
<polygon class="d-badge" points="320.0,256.0 345.7,274.7 335.9,304.8 304.1,304.8 294.3,274.7"/>
<g class="d-icon">
<ellipse cx="320" cy="273" rx="10" ry="4"/>
<path d="M310,273 v15 a10,4 0 0 0 20,0 v-15 M310,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="235" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="320.0" y="475.64" text-anchor="middle" font-size="24">Ứng dụng</text>
<polygon class="d-badge" points="320.0,394.0 345.7,412.7 335.9,442.8 304.1,442.8 294.3,412.7"/>
<g class="d-icon">
<rect x="312" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="316.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M316,411 v-4 M316,427 v4 M312,415 h-4 M328,415 h4 M320,411 v-4 M320,427 v4 M312,419 h-4 M328,419 h4 M324,411 v-4 M324,427 v4 M312,423 h-4 M328,423 h4"/>
</g>
<line class="d-line" x1="320" y1="508" x2="320" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="286" cy="710.88" r="14.44"/><path d="M260.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="354" cy="710.88" r="14.44"/><path d="M328.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="320" cy="704.0" r="19.0"/><path d="M286.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<rect class="d-group" x="429" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="437" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="522.0" y="336.92" text-anchor="middle" font-size="22">Cơ sở dữ liệu</text>
<polygon class="d-badge" points="522.0,256.0 547.7,274.7 537.9,304.8 506.1,304.8 496.3,274.7"/>
<g class="d-icon">
<ellipse cx="522" cy="273" rx="10" ry="4"/>
<path d="M512,273 v15 a10,4 0 0 0 20,0 v-15 M512,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="437" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="522.0" y="475.64" text-anchor="middle" font-size="24">Ứng dụng</text>
<polygon class="d-badge" points="522.0,394.0 547.7,412.7 537.9,442.8 506.1,442.8 496.3,412.7"/>
<g class="d-icon">
<rect x="514" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="518.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M518,411 v-4 M518,427 v4 M514,415 h-4 M530,415 h4 M522,411 v-4 M522,427 v4 M514,419 h-4 M530,419 h4 M526,411 v-4 M526,427 v4 M514,423 h-4 M530,423 h4"/>
</g>
<line class="d-line" x1="522" y1="508" x2="522" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="488" cy="710.88" r="14.44"/><path d="M462.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="556" cy="710.88" r="14.44"/><path d="M530.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="522" cy="704.0" r="19.0"/><path d="M488.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<text class="d-title" x="982" y="50" text-anchor="middle" font-size="42">Multi-tenant</text>
<text class="d-sub" x="982" y="88" text-anchor="middle" font-size="24">mọi khách hàng dùng chung một instance</text>
<rect class="d-panel" x="698" y="104" width="568" height="434" rx="44"/>
<rect class="d-box" x="887" y="126" width="190" height="86" rx="14"/>
<text class="d-label" x="982.0" y="183.64" text-anchor="middle" font-size="24">Cơ sở dữ liệu</text>
<polygon class="d-badge" points="982.0,102.0 1007.7,120.7 997.9,150.8 966.1,150.8 956.3,120.7"/>
<g class="d-icon">
<ellipse cx="982" cy="119" rx="10" ry="4"/>
<path d="M972,119 v15 a10,4 0 0 0 20,0 v-15 M972,126 a10,4 0 0 0 20,0"/>
</g>
<line class="d-line" x1="982" y1="216" x2="982" y2="246" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<rect class="d-group" x="704" y="240" width="556" height="156" rx="30"/>
<rect class="d-box" x="714" y="282" width="536" height="90" rx="16"/>
<polygon class="d-badge" points="982.0,248.0 1007.7,266.7 997.9,296.8 966.1,296.8 956.3,266.7"/>
<g class="d-icon">
<rect x="974" y="265" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="978.5" y="269.5" width="7" height="7"/>
<path stroke-width="1.4" d="M978,265 v-4 M978,281 v4 M974,269 h-4 M990,269 h4 M982,265 v-4 M982,281 v4 M974,273 h-4 M990,273 h4 M986,265 v-4 M986,281 v4 M974,277 h-4 M990,277 h4"/>
</g>
<rect class="d-box" x="722" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="806.0" y="347.92" text-anchor="middle" font-size="22">Ứng dụng</text>
<rect class="d-box" x="898" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="982.0" y="347.92" text-anchor="middle" font-size="22">Ứng dụng</text>
<rect class="d-box" x="1074" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="1158.0" y="347.92" text-anchor="middle" font-size="22">Ứng dụng</text>
<rect class="d-box" x="887" y="440" width="190" height="86" rx="14"/>
<text class="d-label" x="982.0" y="497.28" text-anchor="middle" font-size="23">Load balancer</text>
<polygon class="d-badge" points="982.0,404.0 1007.7,422.7 997.9,452.8 966.1,452.8 956.3,422.7"/>
<g class="d-icon">
<path d="M982,416 l6,9 h-12 z M982,425 v5 M972,433 h20 M972,433 v3 M982,433 v3 M992,433 v3"/>
<rect class="d-icon-fill" x="969" y="436" width="6" height="5"/>
<rect class="d-icon-fill" x="979" y="436" width="6" height="5"/>
<rect class="d-icon-fill" x="989" y="436" width="6" height="5"/>
</g>
<line class="d-line" x1="806" y1="364" x2="922" y2="438" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="982" y1="364" x2="982" y2="398" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="1158" y1="364" x2="1042" y2="438" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="932" y1="528" x2="780" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="716" cy="710.88" r="14.44"/><path d="M690.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="784" cy="710.88" r="14.44"/><path d="M758.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="750" cy="704.0" r="19.0"/><path d="M716.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<line class="d-line" x1="982" y1="528" x2="982" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="948" cy="710.88" r="14.44"/><path d="M922.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="1016" cy="710.88" r="14.44"/><path d="M990.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="982" cy="704.0" r="19.0"/><path d="M948.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<line class="d-line" x1="1032" y1="528" x2="1184" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="1180" cy="710.88" r="14.44"/><path d="M1154.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="1248" cy="710.88" r="14.44"/><path d="M1222.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="1214" cy="704.0" r="19.0"/><path d="M1180.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
</svg>
</div>
</figure>


<h3 class="relative group">Single tenant, multi-instance
    <div id="single-tenant-multi-instance" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#single-tenant-multi-instance" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>

  



<div
  
    class="flex px-4 py-3 rounded-md shadow bg-primary-100 dark:bg-primary-900"
  
  >
  <span
    
      class="text-primary-400 pe-3 flex items-center"
    
    >
    <span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>
</span>
  </span>

  <span
    
      class="dark:text-neutral-300"
    
    ><strong>Single Tenant, Multi-Instance</strong>: Với mô hình này, hạ tầng được nhân thành nhiều instance, mỗi tenant chạy trên một instance riêng.</span>
</div>

<p>Điểm hấp dẫn nhất ở đây là tính cô lập, và gần như mọi lợi ích khác đều xuất phát từ đó. Dữ liệu của mỗi khách hàng nằm trong một môi trường riêng, nên các đợt đánh giá bảo mật hay những buổi làm việc về tuân thủ quy định (compliance) dễ thở hơn hẳn. Một lần triển khai lỗi hay một tải xử lý (workload) quá nặng chỉ làm sập một khách hàng, chứ không kéo sập tất cả. Bạn có thể mở rộng quy mô, tinh chỉnh và tùy biến từng instance độc lập với nhau, và vì tài nguyên được cấp riêng nên hiệu năng của khách này không phụ thuộc vào việc tenant bên cạnh đang làm gì. Lịch bảo trì cũng có thể xếp theo từng khách hàng, nên thời gian gián đoạn dịch vụ (downtime) vừa ngắn vừa đúng chỗ. Và nếu định giá hợp lý, bạn có thể tính chi phí hạ tầng sát với mức sử dụng thực tế của từng tenant.</p>
<p>Cái giá phải trả là giờ bạn đang vận hành N bản sao của mọi thứ. Chi phí tài nguyên cộng dồn lại, cấu hình giữa các instance dần dần lệch nhau, và riêng việc giữ cho phiên bản phần mềm lẫn dữ liệu đồng bộ trên cả dàn instance đã thành một dự án riêng. Chuyện mở rộng quy mô, cô lập và quản lý chi phí vẫn cần được lên kế hoạch nghiêm túc. Các vấn đề không hề biến mất, chúng chỉ nhân lên.</p>

<h3 class="relative group">Multi tenant, single instance
    <div id="multi-tenant-single-instance" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#multi-tenant-single-instance" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>

  



<div
  
    class="flex px-4 py-3 rounded-md shadow bg-primary-100 dark:bg-primary-900"
  
  >
  <span
    
      class="text-primary-400 pe-3 flex items-center"
    
    >
    <span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>
</span>
  </span>

  <span
    
      class="dark:text-neutral-300"
    
    ><strong>Multi Tenant, Single Instance</strong>: Chỉ một ứng dụng duy nhất phục vụ tất cả các tenant.</span>
</div>

<p>Đây là lựa chọn mặc định cũng có lý do của nó: một lần triển khai, một cơ sở dữ liệu cần sao lưu, một phiên bản phần mềm duy nhất trên môi trường production. Mô hình này tiết kiệm tài nguyên, vận hành đơn giản, và mọi khách hàng đều nhận bản cập nhật cũng như bản vá bảo mật cùng một lúc.</p>
<p>Những phần khó lại chính là mặt trái của việc dùng chung. Chỉ một tenant dùng nặng cũng có thể kéo hiệu năng của tất cả mọi người đi xuống. Khả năng tùy biến cho từng khách hàng thì bị hạn chế. Việc cô lập dữ liệu giờ nằm trong mã ứng dụng chứ không còn nằm ở tầng hạ tầng, nên chỉ một lỗi nhỏ cũng có thể dẫn tới rò rỉ dữ liệu giữa các tenant. Khi gặp điểm nghẽn (bottleneck) lúc mở rộng quy mô thì mọi khách hàng cùng chịu ảnh hưởng, và việc bảo trì phải cẩn thận hơn, vì không còn kiểu sự cố &ldquo;chỉ ảnh hưởng mỗi khách này&rdquo; nữa.</p>
<p><a href="https://www.scaleway.com/en/blog/saas-multi-tenant-vs-multi-instance-architectures/"  target="_blank" rel="noreferrer">Tìm hiểu thêm về giải pháp SaaS - Kiến trúc Multi-instance và Multi-tenant</a></p>

<h2 class="relative group">Multi-tenant hay multi-instance
    <div id="multi-tenant-hay-multi-instance" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#multi-tenant-hay-multi-instance" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Bạn sẽ thường gặp lựa chọn này dưới cái tên multi-tenant và multi-instance, nhưng thực ra vẫn là cùng một lựa chọn, chỉ nhìn từ phía bên kia. &ldquo;Tenant&rdquo; nói về khách hàng: bao nhiêu khách hàng dùng chung một bản phần mềm. &ldquo;Instance&rdquo; nói về việc triển khai: bạn chạy bao nhiêu bản phần mềm. Một sản phẩm single-tenant chính là một hệ thống multi-instance, mỗi khách hàng một bản, còn sản phẩm multi-tenant thường là một instance duy nhất mà mọi người dùng chung. Vậy nên khi ai đó đòi multi-instance, thực chất họ đang đòi single-tenant.</p>
<p>Trên thực tế, đây giống một dải lựa chọn hơn là hai ô tách biệt, và phần lớn quyết định quan trọng nằm ở cơ sở dữ liệu. Ở đầu dùng chung nhiều nhất, dữ liệu của mọi khách hàng nằm trong cùng các bảng, mỗi dòng có một cột <code>tenant_id</code>, và mọi truy vấn đều phải lọc theo cột đó. Cách này rẻ nhất để vận hành và cũng dễ sai nhất. Tiến thêm một bước, mỗi khách hàng có một schema riêng bên trong một cơ sở dữ liệu dùng chung: tách biệt gọn gàng hơn, sao lưu được theo từng khách hàng, nhưng mỗi lần chuyển đổi cấu trúc dữ liệu (migration) phải chạy lại cho từng schema. Xa hơn nữa, ứng dụng vẫn dùng chung nhưng mỗi khách hàng có cơ sở dữ liệu riêng: đây là điểm cân bằng được nhiều đội chọn, vì dữ liệu được cô lập chắc chắn mà không phải chạy cả một bộ hạ tầng cho từng khách hàng. Ở đầu bên kia là single-tenant hoàn toàn, cả ứng dụng lẫn cơ sở dữ liệu đều dành riêng. Tài liệu hướng dẫn SaaS của AWS gọi hai đầu này là silo và pool, còn kiểu kết hợp cả hai là bridge. Đây là bộ từ vựng khá tiện khi bạn cần giải thích kiến trúc của mình cho đội bảo mật của khách hàng.</p>

<h2 class="relative group">So sánh nhanh single-tenant và multi-tenant
    <div id="so-sánh-nhanh-single-tenant-và-multi-tenant" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#so-s%c3%a1nh-nhanh-single-tenant-v%c3%a0-multi-tenant" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Single-tenant (multi-instance)</th>
					<th>Multi-tenant (single instance)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Cô lập dữ liệu</td>
					<td>Hạ tầng riêng cho từng khách hàng</td>
					<td>Do code hoặc chính sách của cơ sở dữ liệu đảm bảo</td>
			</tr>
			<tr>
					<td>Chi phí cho mỗi khách hàng</td>
					<td>Cao: mỗi khách hàng một bộ hạ tầng đầy đủ</td>
					<td>Thấp: mọi người dùng chung một bộ</td>
			</tr>
			<tr>
					<td>Vận hành</td>
					<td>Triển khai, nâng cấp, bảng giám sát nhân theo số khách hàng</td>
					<td>Mỗi thứ chỉ một</td>
			</tr>
			<tr>
					<td>Hàng xóm ồn ào (noisy neighbour)</td>
					<td>Không có</td>
					<td>Một tenant dùng nặng có thể làm chậm tất cả</td>
			</tr>
			<tr>
					<td>Tùy biến</td>
					<td>Theo từng khách hàng, thậm chí khác phiên bản</td>
					<td>Chủ yếu dừng ở cấu hình và feature flag</td>
			</tr>
			<tr>
					<td>Phát hành</td>
					<td>Lần lượt từng khách hàng</td>
					<td>Tất cả cùng lúc</td>
			</tr>
			<tr>
					<td>Một lần triển khai lỗi ảnh hưởng tới</td>
					<td>Một khách hàng</td>
					<td>Mọi khách hàng</td>
			</tr>
			<tr>
					<td>Đánh giá bảo mật và nơi lưu trữ dữ liệu</td>
					<td>Dễ trả lời</td>
					<td>Phải giải thích nhiều hơn</td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">Khi nào nên chọn cái nào
    <div id="khi-nào-nên-chọn-cái-nào" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#khi-n%c3%a0o-n%c3%aan-ch%e1%bb%8dn-c%c3%a1i-n%c3%a0o" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Hãy chọn multi-tenant, trừ khi có lý do cụ thể khiến bạn phải làm khác. Đây là lựa chọn mặc định đúng khi bạn có nhiều khách hàng, mỗi khách trả không nhiều, khi người dùng tự đăng ký, và khi đội của bạn đủ nhỏ để ba mươi môi trường là quá sức.</p>
<p>Single-tenant chỉ đáng với chi phí vận hành của nó trong vài trường hợp. Khách hàng trong ngành y tế, tài chính và cơ quan nhà nước thường yêu cầu dữ liệu phải được tách biệt về mặt vật lý, hoặc phải lưu ở một quốc gia nhất định, và trong buổi đánh giá bảo mật, một instance riêng là câu trả lời đơn giản và trung thực nhất. Nếu chỉ có vài khách hàng lớn ký hợp đồng doanh nghiệp, họ đủ sức gánh chi phí cho một bộ hạ tầng riêng, và nhiều khi họ còn mặc định là phải có. Còn khi khách hàng cần lịch phát hành riêng, cần giữ cố định một phiên bản, hay cần tùy biến sâu, thì việc tách instance không còn là gánh nặng nữa mà trở thành một phần của thứ bạn đang bán.</p>
<p>Cuối cùng thì phần lớn các đội dùng cả hai: một sản phẩm multi-tenant cho gần như tất cả khách hàng, và instance riêng như một gói doanh nghiệp cho số ít khách hàng cần và sẵn sàng trả tiền. Nếu ngay từ ngày đầu bản multi-tenant đã gắn tenant ID ở mọi nơi, thì sau này tách một khách hàng sang cơ sở dữ liệu riêng chỉ là một lần migration, không phải viết lại từ đầu.</p>

<h2 class="relative group">Làm cho việc cô lập trong multi-tenant khó bị phá vỡ hơn
    <div id="làm-cho-việc-cô-lập-trong-multi-tenant-khó-bị-phá-vỡ-hơn" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#l%c3%a0m-cho-vi%e1%bb%87c-c%c3%b4-l%e1%ba%adp-trong-multi-tenant-kh%c3%b3-b%e1%bb%8b-ph%c3%a1-v%e1%bb%a1-h%c6%a1n" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Lỗi đáng sợ nhất trong multi-tenant là một truy vấn quên lọc theo tenant. Nó không báo lỗi. Nó chỉ lặng lẽ trả về dữ liệu của khách hàng khác, và code review không phải lúc nào cũng bắt được. Nếu bạn dùng Postgres, tính năng bảo mật mức dòng (row-level security, RLS) sẽ chuyển việc kiểm tra này xuống cơ sở dữ liệu, nên kể cả truy vấn quên lọc thì cũng chỉ thấy được các dòng của khách hàng hiện tại:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">invoices</span><span class="w"> </span><span class="n">ENABLE</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">SECURITY</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">POLICY</span><span class="w"> </span><span class="n">tenant_isolation</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">invoices</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">USING</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;app.tenant_id&#39;</span><span class="p">)::</span><span class="n">uuid</span><span class="p">);</span></span></span></code></pre></div></div>
<p>Sau đó, mỗi request đặt tenant của mình bên trong một transaction bằng <code>SET LOCAL app.tenant_id = '…'</code>. Nhờ vậy, một kết nối trong connection pool sẽ không mang ID của khách hàng trước sang request tiếp theo, và request nào quên đặt tenant sẽ nhận về lỗi thay vì dữ liệu. Có hai điểm cần lưu ý: ứng dụng phải kết nối bằng một role không sở hữu bảng, vì chủ sở hữu bảng sẽ bỏ qua các chính sách này trừ khi bạn thêm <code>FORCE ROW LEVEL SECURITY</code>; và tuyệt đối không kết nối bằng superuser, vì superuser luôn bỏ qua chúng.</p>

<h2 class="relative group">Nếu chọn single-tenant thì bắt đầu từ đâu?
    <div id="nếu-chọn-single-tenant-thì-bắt-đầu-từ-đâu" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#n%e1%ba%bfu-ch%e1%bb%8dn-single-tenant-th%c3%ac-b%e1%ba%aft-%c4%91%e1%ba%a7u-t%e1%bb%ab-%c4%91%c3%a2u" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Nếu bạn quyết định đi theo single-tenant, công cụ sẽ quyết định mọi thứ còn nằm trong tầm kiểm soát, hay trở thành một thảm họa diễn ra từ từ. Không ai quản lý thủ công nổi ba mươi môi trường cả.</p>
<p>Phần cốt lõi là container kết hợp với hạ tầng dưới dạng mã (infrastructure as code, IaC). Docker đóng gói ứng dụng giống hệt nhau cho mọi tenant, Kubernetes lo việc triển khai và co giãn cả đống instance đó, còn Terraform, Pulumi hay AWS CloudFormation cho phép bạn định nghĩa toàn bộ hạ tầng của một tenant bằng mã. Nhờ vậy, dựng thêm khách hàng thứ 31 chỉ tốn một pull request, chứ không phải mất trắng một cuối tuần. Nếu hệ thống của bạn chủ yếu chạy trên máy ảo (VM), các công cụ quản lý cấu hình như Ansible, Chef hay Puppet cũng đóng vai trò tương tự. Ở mô hình này, một quy trình CI/CD vững chắc (Jenkins, GitLab CI/CD) còn quan trọng hơn bình thường, vì mỗi bản phát hành phải được triển khai tới mọi instance mà không cần ai ngồi canh.</p>
<p>Ngoài những thứ đó ra thì tùy vào đặc thù sản phẩm của bạn. Kiến trúc phi máy chủ (serverless) như AWS Lambda hay Azure Functions có thể loại bỏ hẳn tầng máy chủ cho một số tải xử lý. Các dịch vụ cơ sở dữ liệu được quản lý sẵn (managed) như Amazon RDS hay Azure SQL Database giúp bạn khỏi phải tự tay quản trị từng cơ sở dữ liệu cho từng tenant. Giám sát bằng Prometheus, Grafana hay New Relic thì không còn là tùy chọn khi bạn đã có cả một dàn instance, vì bạn muốn biết instance nào đang trục trặc trước khi khách hàng gọi tới báo. Về định danh, một dịch vụ như Auth0 hay Okta sẽ lo phần quản lý người dùng và phân quyền truy cập, để bạn khỏi phải làm lại cho từng tenant. Dù bạn dùng ngôn ngữ nào thì chắc cũng có sẵn framework hay thư viện hướng SaaS giúp bạn bớt được phần nào đống việc &ldquo;đi dây&rdquo; này, và nhà cung cấp đám mây lớn nào (AWS, Azure, Google Cloud Platform) cũng sẵn lòng vận hành hết cho bạn.</p>
<p>Đừng dùng hết cả danh sách này ngay từ ngày đầu. Hãy bắt đầu với container và IaC, bổ sung giám sát và quản lý định danh, truy cập (IAM) khi số tenant tăng dần, rồi để những khó khăn thực tế quyết định phần còn lại.</p>

<h2 class="relative group">Nói ngắn gọn
    <div id="nói-ngắn-gọn" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#n%c3%b3i-ng%e1%ba%afn-g%e1%bb%8dn" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Single-tenant, hay multi-instance, nghĩa là mỗi khách hàng có một bản ứng dụng và cơ sở dữ liệu riêng: cô lập tốt, kiểm soát được theo từng khách hàng, đổi lại bạn phải vận hành cả một loạt instance. Multi-tenant nghĩa là mọi người dùng chung một bản: rẻ và đơn giản, còn việc cô lập do code đảm bảo. Phần lớn sản phẩm nên bắt đầu với multi-tenant, gắn tenant ID ở mọi nơi và để cơ sở dữ liệu thực thi việc lọc, rồi cung cấp instance riêng cho số ít khách hàng cần và sẵn sàng trả tiền. Nếu bạn thật sự chạy single-tenant, thứ giúp mọi việc còn trong tầm kiểm soát là container và hạ tầng dưới dạng code (IaC).</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-04-07-singletenantsingleinstance/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
