Mười bảy bài trước, mỗi bài giải một vấn đề riêng lẻ. Bài này ghép chúng lại: nhận một đề bài, ước lượng, chọn kiến trúc tối giản, chạy thật, đo, tìm nút cổ chai, vá, rồi đo lại. Phần giá trị nhất không phải kiến trúc cuối cùng mà là bảng số liệu trước/sau của từng bước — vì nó cho thấy bước nào đáng công và bước nào chỉ thêm phức tạp.

Kết quả tổng: từ 6.115 lên 25.786 request/giây (4,2 lần) và p99 từ 14,71 xuống 1,28 ms (11,5 lần). Nhưng hai con số đáng nhớ hơn nằm ở giữa. Cache — tối ưu ai cũng nghĩ tới đầu tiên — xoá được 99% truy vấn đọc nhưng chỉ tăng throughput 16%, vì nút cổ chai thật nằm ở đường ghi. Và read replica, một tối ưu hoàn toàn hợp lý trên giấy, cho đúng 0% cải thiện — thậm chí kém đi một chút.

ℹ️ Cấu hình máy đo
Apple M1 Max, 10 lõi, 32 GB RAM, macOS 26.5.2, Docker 29.6.2. Service rút gọn URL (app/shortener.js, Node http thuần, 0 dependency) giới hạn 1 CPU và 256 MB; PostgreSQL 18 primary + read replica, Redis 7; bộ đo tải loadgen.js, 20 kết nối, closed-loop, warm-up 3 giây, đo 12 giây, không gian khoá 1.000 link.

Mỗi phiên bản chỉ đổi đúng một tham số so với phiên bản trước. Đây không phải chi tiết nhỏ mà là điều kiện để kết luận có nghĩa: đổi hai thứ cùng lúc thì bạn chỉ biết "có gì đó tốt hơn", không biết là thứ nào — và lần sau sẽ lặp lại cả hai dù chỉ một cái có tác dụng.

18.1 Nhận đề và làm rõ yêu cầu

Đề bài: một hệ thống rút gọn URL kèm thống kê lượt click. Chọn bài này có chủ đích, vì nó gói ba dạng tải rất khác nhau vào một hệ thống nhỏ đủ để đọc hết trong một buổi:

Đường đi Tính chất tải Yêu cầu nhất quán
Chuyển hướng — người dùng bấm link Đọc cực nặng, tỉ lệ đọc:ghi có thể 1000:1 Mạnh vừa phải — link sai là dẫn nhầm chỗ
Tạo link Ghi, lưu lượng thấp Mạnh — mã không được trùng
Thống kê click Ghi nặng, sinh ra từ mỗi lượt chuyển hướng Yếu — trễ vài giây không ai chết

Cột cuối là cột quyết định toàn bộ thiết kế, và nó chỉ lộ ra khi ta chịu hỏi. Ba đường đi có ba yêu cầu nhất quán khác nhau, nên chúng xứng đáng ba cách xử lý khác nhau — và như phần đo sẽ cho thấy, chính chỗ này là nơi phần lớn hiệu năng nằm.

Trước khi vẽ bất cứ thứ gì, đây là những câu phải hỏi. Không có câu trả lời thì mọi lựa chọn sau đó là phỏng đoán:

  • DAU và RPS đỉnh? Quyết định có cần scale ngang hay không.
  • Tỉ lệ đọc:ghi? Quyết định cache và replica có đáng không.
  • Link sống bao lâu? Quyết định dung lượng và chiến lược dọn dẹp.
  • Thống kê cần chính xác tới đâu? Quyết định được phép bất đồng bộ hay không.
  • SLO là gì? Quyết định khi nào thì gọi là "đủ tốt" và dừng tối ưu.
⚠️ Cạm bẫy: vẽ kiến trúc trước khi biết con số
Một sơ đồ có gateway, cache, hàng đợi, sharding và ba loại database trông rất thuyết phục — và hoàn toàn vô nghĩa nếu chưa ai biết hệ thống phải chịu 100 hay 100.000 request mỗi giây. Hai con số đó dẫn tới hai thiết kế khác hẳn nhau, và chọn sai theo hướng "to hơn cho chắc" cũng tốn kém y như chọn sai theo hướng ngược lại.

Thứ tự đúng là: hỏi số → ước lượng → thiết kế bản tối giản → đo → chỉ thêm thành phần khi số liệu chỉ ra nhu cầu. Phần 18.4 sẽ cho thấy một tối ưu nghe rất hợp lý mà đo ra không cải thiện gì.

18.2 Ước lượng và thiết kế v1

Giả sử con số nhận được là 10 triệu lượt chuyển hướng mỗi ngày và 100 nghìn link mới mỗi ngày. Back-of-envelope theo đúng cách của Bài 1:

Đại lượng Phép tính Kết quả
RPS trung bình (đọc) 10.000.000 ÷ 86.400 ~116 req/s
RPS đỉnh (hệ số 3) 116 × 3 ~350 req/s
RPS ghi (tạo link) 100.000 ÷ 86.400 ~1,2 req/s
Tỉ lệ đọc:ghi 116 ÷ 1,2 ~100:1
Dung lượng link sau 5 năm 100k/ngày × 365 × 5 × ~200 byte ~36 GB
Dung lượng bản ghi click sau 5 năm 10 triệu/ngày × 365 × 5 × ~50 byte ~912 GB

Hai dòng cuối đã nói cho ta một điều quan trọng trước khi viết dòng code nào: bảng click lớn gấp 25 lần bảng link. Bài toán thật ở đây không phải lưu link mà là xử lý click — và đó là gợi ý đầu tiên rằng đường ghi analytics xứng đáng được đối xử khác.

Còn 350 req/s đỉnh thì sao? Nó rất nhỏ. Một server duy nhất thừa sức. Vì vậy v1 được cố ý thiết kế tối giản: một service, một PostgreSQL, không cache, không hàng đợi. Ghi click thẳng vào bảng clicks ngay trong lượt chuyển hướng.

💡 ADR: ghi lại quyết định, lý do, và những phương án đã loại bỏ
Một ADR (Architecture Decision Record) tốt chỉ cần bốn phần và vừa một trang: bối cảnh (số liệu và ràng buộc lúc đó), quyết định, hệ quả (được gì, mất gì), và phần hay bị bỏ nhất — các phương án đã cân nhắc và lý do loại.

Phần cuối mới là phần đáng giá nhất về sau. Sáu tháng nữa sẽ có người hỏi "sao không dùng X?", và nếu không có ADR thì cả đội sẽ tranh luận lại từ đầu — với ít thông tin hơn lúc quyết định, vì bối cảnh đã phai. ADR đầu tiên của dự án này chỉ có một câu đáng nhớ: "350 req/s đỉnh không cần kiến trúc phân tán; bắt đầu bằng một server và đo."
Kiến trúc tiến hoá — mỗi bước thêm đúng một thứ, sau khi số liệu chỉ ra nhu cầu v1 shortener PostgreSQL đọc VÀ ghi click — mỗi lượt chuyển hướng là 1 đọc + 1 GHI 6.115 rps · p99 14,71 ms v2 shortener Redis PostgreSQL đọc: 90.431 → 1.006 truy vấn (xoá 99%) · nhưng GHI vẫn nguyên 7.122 rps (chỉ +16%) · p99 5,94 ms v3 shortener Redis cache + bộ đệm đếm click 25.853 rps (×3,6) · p99 1,75 ms — đường nóng KHÔNG còn chạm database v4 shortener read replica 24.803 rps — KHÔNG cải thiện gì, thậm chí kém hơn Vì cache đã hấp thụ 100% lượt đọc: không còn truy vấn nào để chuyển sang replica. Tổng v1 → v5: throughput ×4,2 · p99 tốt hơn 11,5 lần. Nhưng phần lớn mức cải thiện đến từ ĐÚNG MỘT bước (v3).
Bốn bước, và chỉ một bước thật sự đáng công — điều mà không sơ đồ nào nói trước được.

18.3 Dựng thật và đo baseline

v1 chạy được. Bây giờ điều quan trọng nhất và cũng hay bị bỏ qua nhất: đo trước khi tối ưu. Không có baseline thì mọi cải thiện sau đó là cảm giác, và bạn không có cách nào biết một thay đổi có đáng giữ hay không.

baseline_v1.txt
$ ./tools/capstone-test.sh v1
  v1  rps=   6115.1  p50=  2.91  p95=  4.93  p99=  14.71
      dbReads=  90431  dbWrites=  90431  redisOps=       0  hitRatio=None

Hai dòng này đã chứa toàn bộ manh mối. Số truy vấn đọc và số lệnh ghi bằng nhau chằn chặn: 90.431 và 90.431. Nghĩa là mỗi lượt chuyển hướng sinh ra một lệnh ghi vào database — và ghi thì đắt hơn đọc rất nhiều, lại còn nằm trên đường đi nóng nhất của hệ thống.

Trực giác thông thường sẽ nói: "đọc nặng thì thêm cache". Trực giác đó không sai, nhưng nó trả lời câu hỏi sai. Ta hãy làm theo nó trước — vì đó chính là điều mọi người sẽ làm — rồi xem số liệu nói gì.

⚠️ Cạm bẫy: tối ưu theo cảm giác hoặc theo thói quen
"Chắc là database chậm" là câu mở đầu của rất nhiều buổi tối ưu, và nó dẫn thẳng tới việc thêm cache, thêm index, thêm replica — những thứ có thể chẳng liên quan gì tới nút cổ chai thật.

Cách chữa rẻ đến bất ngờ: đếm số thao tác, không chỉ đo thời gian. Bộ đếm dbReadsdbWrites ở trên chỉ là hai biến số nguyên, nhưng chúng chỉ thẳng vào chỗ cần nhìn theo cách mà biểu đồ latency không làm được. Đây chính là bài học Bài 16 áp dụng vào một tình huống cụ thể: metrics cho biết chuyện, còn bộ đếm đúng chỗ cho biết ở đâu.

18.4 Vòng lặp đo → vá → đo lại

Bốn vòng, mỗi vòng đổi đúng một tham số. Toàn bộ số liệu là số đo thật của cùng một máy, cùng một kịch bản tải:

Phiên bản Thay đổi Throughput p99 Truy vấn đọc DB Lệnh ghi DB
v1 Bản gốc tối giản 6.115 rps 14,71 ms 90.431 90.431
v2 + cache-aside (Bài 5) 7.122 rps (+16%) 5,94 ms 1.006 103.775
v3 + đếm click bất đồng bộ (Bài 12) 25.853 rps (×3,6) 1,75 ms 0 0
v4 + read replica (Bài 7) 24.803 rps (−4%) 1,85 ms 0 0
v5 + rate limit (Bài 13) 25.786 rps 1,28 ms 0 0
Throughput và p99 qua từng vòng vá (số đo thật) Throughput (req/s) — càng cao càng tốt v1 6.115 v2 7.122 — chỉ +16% dù cache đã xoá 99% lượt đọc v3 25.853 v4 24.803 — thêm replica, KÉM đi v5 25.786 p99 (ms) — càng thấp càng tốt v1 14,71 v2 5,94 — cache giúp p99 nhiều hơn giúp throughput v3 1,75 v4 1,85 v5 1,28
Hai cột v2 kể hai câu chuyện khác nhau: cache giúp p99 rất nhiều nhưng gần như không giúp throughput.

v1 → v2: cache xoá 99% truy vấn đọc nhưng chỉ tăng 16% throughput. Nếu chỉ nhìn cột "truy vấn đọc" thì đây là một thành công vang dội — 90.431 xuống 1.006. Nhưng cột throughput nói khác, và lý do nằm ở cột cuối: số lệnh ghi không giảm chút nào, thậm chí tăng theo lưu lượng. Cache đã sửa một thứ không phải nút cổ chai. Điểm sáng duy nhất là p99 giảm 2,5 lần (14,71 → 5,94), vì đường đọc không còn phải xếp hàng chờ connection pool cùng đường ghi.

🔬 Vì sao cache giúp p99 rất nhiều nhưng gần như không giúp throughput
Hai cột của v2 lệch nhau đến mức đáng ngờ: p99 tốt hơn 2,5 lần nhưng throughput chỉ nhích 16%. Giải thích nằm ở chỗ hai chỉ số này đo hai thứ khác nhau.

Throughput bị chặn bởi tài nguyên khan hiếm nhất trên đường đi — ở v2 là đường ghi vào database, và cache không chạm tới nó chút nào. Bỏ lượt đọc đi thì nút cổ chai vẫn nguyên vẹn, nên trần thông lượng gần như không đổi.

p99 thì phản ánh thời gian xếp hàng. Ở v1, lượt đọc và lượt ghi tranh nhau cùng một connection pool, nên một lượt đọc xui xẻo có thể phải đợi sau vài lệnh ghi (Bài 1). Cache rút 99% lượt đọc ra khỏi hàng đợi đó, nên phần đuôi gọn hẳn lại — dù tổng năng lực không tăng.

Bài học tổng quát: một tối ưu có thể cải thiện độ trễ mà không cải thiện thông lượng, và ngược lại. Nếu chỉ theo dõi một trong hai, bạn sẽ hoặc bỏ lỡ giá trị thật của một thay đổi, hoặc gán cho nó công lao nó không có.

v2 → v3: đổi cách đếm click, throughput nhân 3,6 lần. Đây là bước đáng công nhất, và nó không phải một tối ưu kỹ thuật mà là một quyết định về yêu cầu: chấp nhận rằng số liệu thống kê được phép trễ vài giây. Nhận ra điều đó ở mục 18.1 là lý do bước này khả thi. Sau v3, đường nóng của hệ thống không chạm database một lần nào — 0 đọc, 0 ghi.

🚨 v4: tối ưu hợp lý trên giấy, cho đúng 0% cải thiện
Read replica là bước tiếp theo hiển nhiên trong mọi tài liệu thiết kế hệ thống: tải đọc nặng thì tách đọc sang replica. Đo thật thì nó cho 24.803 rps so với 25.853 — kém hơn 4%, tức là trong khoảng nhiễu, và chắc chắn không phải cải thiện.

Lý do hoàn toàn hiển nhiên sau khi nhìn số liệu: cột "truy vấn đọc DB" ở v3 đã là 0. Cache hấp thụ 100% lượt đọc, nên không còn truy vấn nào để chuyển sang replica. Chúng ta vừa thêm một container, một luồng replication, một nguồn replication lag và một lớp phức tạp vận hành — để đổi lấy con số không.

Giới hạn của kết luận này: tập dữ liệu nóng ở đây chỉ có 1.000 link nên nằm gọn trong cache. Với không gian khoá lớn hơn nhiều lần bộ nhớ cache, hit ratio sẽ tụt và replica sẽ có việc để làm. Bài học không phải "read replica vô dụng" mà là thứ tự áp dụng tối ưu quyết định giá trị của từng cái — và chỉ phép đo trên chính hệ thống của bạn mới nói được thứ tự đó.

v4 → v5: rate limit gần như miễn phí. p50 tăng 0,06 ms — đúng một vòng mạng tới Redis, khớp với con số của Bài 13. Và cũng như ở Bài 13, p95/p99 lại đo được thấp hơn (1,01 và 1,28 so với 1,49 và 1,85). Hiện tượng này lặp lại ổn định qua hai bài khác nhau nhưng mình vẫn chưa chứng minh được cơ chế, nên vẫn ghi lại như một quan sát chứ không phải kết luận.

Còn cái giá của v3 thì sao? Nó có thật, chỉ là không nằm trong cột throughput. Điểm cuối /stats phơi bày nó ra:

cai_gia_cua_bat_dong_bo.json
$ curl -s localhost:3010/stats/k7
{
  "code": "k7",
  "clickTrongDb": 191,          // already flushed to the database
  "clickConTrongBoDem": 1028,   // still sitting in Redis, not flushed yet
  "tong": 1219                  // the CORRECT number only exists when you add both
}

Khoảng cách giữa 191 và 1.219 chính là nhất quán cuối ở dạng cụ thể nhất (Bài 9). Ai đọc thẳng bảng clicks trong database sẽ thấy một con số sai. Đó là cái giá phải trả cho 3,6 lần throughput — và với thống kê click thì đó là cái giá đúng, còn với số dư tài khoản thì không.

18.5 Tổng kết đánh đổi, và những gì lab này không nói được

Mười bảy bài, và mỗi bài rút gọn lại được thành một câu đánh đổi:

Chủ đề Được Mất
Cache (Bài 5) Giảm tải database, giảm p99 Dữ liệu cũ, thundering herd, một tầng nữa phải vận hành
CDN / edge (Bài 6) Vượt qua giới hạn khoảng cách vật lý Vô hiệu hoá cache trở nên khó, phân mảnh theo cache key
Replication (Bài 7) Scale tầng đọc, có bản dự phòng Replication lag, bug read-your-writes
Sharding (Bài 8) Vượt giới hạn một máy Truy vấn xuyên shard, hotspot, rebalance
Bất đồng bộ (Bài 12) Tách độ trễ người dùng khỏi việc nặng Nhất quán cuối, tồn đọng, bắt buộc idempotent
Rate limit (Bài 13) Hệ thống nói "không" có kiểm soát Một vòng mạng thêm vào mọi request
Event sourcing (Bài 14) Lịch sử đầy đủ, dựng lại read model bằng replay Chi phí nhận thức, upcasting, mất truy vấn tuỳ ý
Microservices (Bài 15) Deploy độc lập, scale riêng phần nóng p99 xấu 4 lần, độ khả dụng nhân dồn, saga
Observability (Bài 16) Trả lời được câu hỏi chưa lường trước Chi phí lưu trữ, cardinality, phải bảo trì
Circuit breaker (Bài 17) Chặn cascade, thất bại nhanh Thêm trạng thái phải chỉnh, và chính nó cũng có bug

Nhìn cả bảng cùng lúc mới thấy điều đáng nói nhất: không dòng nào có cột "mất" trống. Không có kỹ thuật nào trong series này là miễn phí. Câu hỏi đúng chưa bao giờ là "nên dùng X không" mà luôn là "vấn đề mình đang có xứng với cái giá của X không" — và câu đó chỉ trả lời được bằng số liệu của chính hệ thống mình.

⚠️ Cạm bẫy lớn nhất: tin rằng chạy tốt trên laptop nghĩa là sẵn sàng
Toàn bộ số liệu trong 18 bài đến từ Docker trên một máy tính cá nhân. Có những thứ lab này không mô phỏng được, và chúng thường là thứ khiến hệ thống thật sập:

Mạng thật. Mọi container ở đây nói chuyện qua loopback, một vòng mạng tốn ~0,07 ms. Thật là 0,5–2 ms trong cùng trung tâm dữ liệu, hàng chục tới hàng trăm ms liên vùng — và quan trọng hơn, mạng thật mất gói, phân mảnh, và chậm bất thường chứ không sạch như ở đây.

Sự cố hạ tầng. Đĩa hỏng, node bị thu hồi, cả một vùng sẵn sàng mất điện, DNS trục trặc. Lab chỉ tiêm được những lỗi ta nghĩ ra trước.

Chi phí tiền bạc. Ở đây thêm một container là miễn phí. Ngoài kia mỗi thành phần là một hoá đơn hàng tháng, và ràng buộc ngân sách thường định hình kiến trúc nhiều hơn ràng buộc kỹ thuật.

Yếu tố con người. Không lab nào mô phỏng được việc phải chẩn đoán một hệ thống lạ lúc 3 giờ sáng, với tài liệu đã cũ và người viết nó đã nghỉ việc. Đây là lý do thật sự khiến sự đơn giản có giá trị — không phải vì đơn giản thì đẹp, mà vì hệ thống đơn giản là hệ thống bạn còn hiểu nổi khi đang mệt và đang sợ.
💡 Khi nào thì dừng tối ưu
Câu trả lời không phải "khi hết ý tưởng" mà là khi đã đạt SLO và còn dư error budget (Bài 16). Ở capstone này, ước lượng ban đầu là 350 req/s đỉnh — nghĩa là ngay cả v1, với 6.115 req/s, đã dư sức gấp 17 lần.

Nói cách khác: nếu đây là hệ thống thật với con số thật đó, thì v2, v3, v4, v5 đều không cần thiết. Chúng tồn tại trong bài này để dạy quy trình đo, không phải vì bài toán đòi hỏi. Đây có lẽ là điều khó chấp nhận nhất với người kỹ thuật: kiến trúc đúng thường là kiến trúc nhàm chán, và phần lớn tối ưu chúng ta thích làm là tối ưu cho một quy mô chưa bao giờ tới.
Kiến trúc cuối — và bài nào đóng góp phần nào ĐƯỜNG NÓNG — chuyển hướng · 25.786 req/s · p99 1,28 ms · KHÔNG chạm database người dùng rate limiter Bài 13 shortener Bài 2 · 3 Redis: cache Bài 5 (TTL có jitter) 302 Found 99% dừng ở đây Mỗi thành phần trên đường này đều nằm trên p99 của MỌI request — nên mỗi thứ thêm vào phải trả giá bằng độ trễ. ĐƯỜNG NGUỘI — analytics Redis INCR (bộ đệm) gộp về PostgreSQL Bài 12 · nhất quán cuối (Bài 9) Bước đáng công nhất: ×3,6 throughput Cái giá: /stats phải cộng cả hai nguồn, và ai đọc thẳng bảng DB sẽ thấy số sai. PHẦN THÊM VÀO MÀ KHÔNG DÙNG TỚI read replica (Bài 7) 0 truy vấn đọc đi qua nó, vì cache đã hấp thụ 100%. Thêm 1 container, 1 luồng replication, 1 nguồn lag — đổi lấy 0% cải thiện. Giữ lại trong hình vì đây là bài học, không phải lỗi vẽ.
Kiến trúc cuối cùng của một hệ thống thật luôn có cả phần thừa — điều quan trọng là biết phần nào thừa.

Checklist tự đánh giá một bản thiết kế

  • Đã có số chưa — RPS đỉnh, tỉ lệ đọc:ghi, dung lượng, SLO?
  • Bản tối giản đã đo baseline chưa, hay đang tối ưu trong bóng tối?
  • Mỗi thành phần thêm vào có số liệu trước/sau chứng minh nó đáng không?
  • Mỗi lời gọi mạng có timeout, và timeout có giảm dần theo độ sâu không?
  • Thao tác nào có thể được thử lại — chúng đã idempotent chưa?
  • Retry ở mấy tầng? (Đáp án đúng gần như luôn là: một.)
  • Khi quá tải, bạn bỏ cái gì trước, và điều đó đã được quyết định chưa?
  • correlation ID xuyên suốt, kể cả qua hàng đợi không?
  • Đường fallback có phụ thuộc ít thứ hơn đường chính không?
  • Cơ chế failover gần nhất được diễn tập khi nào?

Tự làm lại các phép đo

reproduce_measurements.sh
cd blog/sysdesign/sysdesign-lab

./tools/capstone-test.sh seed   # seed 1,000 links (run once)
./tools/capstone-test.sh all    # run v1 -> v5 in sequence

#   v1 baseline             rps  6,115  p99 14.71ms   dbReads 90,431  dbWrites 90,431
#   v2 + cache              rps  7,122  p99  5.94ms   dbReads  1,006  dbWrites 103,775
#   v3 + async click counts rps 25,853  p99  1.75ms   dbReads      0  dbWrites      0
#   v4 + read replica       rps 24,803  p99  1.85ms   (!) NO improvement at all
#   v5 + rate limit         rps 25,786  p99  1.28ms

# The visible cost of v3:
curl -s localhost:3010/stats/k7   # inDb 191 · stillBuffered 1028 · total 1219

Tóm lại

Quy trình quan trọng hơn kiến trúc. Hỏi số trước khi vẽ; thiết kế bản tối giản; đo baseline; rồi mỗi vòng chỉ đổi một thứ và ghi lại số trước/sau. Nghe rất chậm, nhưng nó là cách duy nhất để biết mình đang cải thiện hay chỉ đang thêm phức tạp.

Ba con số của capstone này đáng mang theo. Cache — tối ưu ai cũng nghĩ tới đầu tiên — xoá được 99% truy vấn đọc mà chỉ tăng throughput 16%, vì nút cổ chai nằm ở chỗ khác. Thứ thật sự đáng công là một quyết định về yêu cầu, không phải một thủ thuật kỹ thuật: chấp nhận cho thống kê click trễ vài giây, và throughput nhân 3,6 lần. Còn read replica, hợp lý trên mọi sơ đồ, cho đúng 0% — vì cache đã hấp thụ hết phần đọc mà nó định gánh.

Và điều cuối cùng, cũng là điều đáng nhớ nhất của cả 18 bài: bảng đánh đổi ở mục 18.5 không có dòng nào cột "mất" để trống. Mọi kỹ thuật trong series này đều có giá. Việc của người thiết kế không phải là biết nhiều kỹ thuật, mà là biết khi nào cái giá đó xứng đáng — và câu trả lời luôn nằm trong số liệu của chính hệ thống mình, không nằm trong bài viết nào, kể cả bài này.

Đây là bài cuối của lộ trình. Nếu bạn đã chạy hết các lab và tự đo lại bằng máy của mình, bạn không chỉ biết những khái niệm này — bạn đã thấy chúng xảy ra. Đó là khác biệt mà cả series được viết ra để tạo nên.

📖 Tài liệu tham khảo

Tải mã nguồn thực hành

Toàn bộ hệ rút gọn URL dùng trong capstone: cả năm phiên bản v1–v5 (baseline, cache, đếm click bất đồng bộ, read replica, rate limit) nằm trong cùng một file, chọn qua biến môi trường — mọi con số trong bài đến từ file này:

Tải về shortener.js (toàn bộ hệ rút gọn URL — 0 dependency)

Bài viết liên quan trong series

Bài 17: Chế Độ Lỗi & Khả Năng Chống Chịu 🎉 Bạn đã hoàn thành lộ trình Thiết Kế Hệ Thống Quay lại Lộ trình Thiết Kế Hệ Thống

Bình luận