Nghĩ Trước Khi Code: Viết Design Doc Để Ra Quyết Định Tốt Hơn

Bạn nhận một ticket mới. Feature cỡ trung — không đơn giản, không phải rewrite, nhưng đủ nhiều phần chuyển động để cần suy nghĩ. Bạn làm gì?
Nếu bạn giống đa số developer, bạn mở editor và bắt đầu code. Có thể phác thảo vài ý tưởng trong đầu. Có thể chat với đồng nghiệp 5 phút. Rồi code hai ngày, mở PR, và phát hiện trong lúc review rằng:
- Bạn bỏ sót một edge case quan trọng thay đổi toàn bộ approach
- Team khác đã build thứ tương tự với interface khác
- Tech lead có opinion rất mạnh về data model — nhưng chẳng ai hỏi
- Solution của bạn chạy được, nhưng tạo coupling sẽ gây đau đầu trong 3 tháng tới
Hai ngày lãng phí. Không phải vì code dở — mà vì việc suy nghĩ diễn ra quá muộn.
Đây là gap phổ biến nhất giữa developer mid-level và senior. Mid-level nhảy vào code ngay. Senior nghĩ trước, code sau — và họ biến quá trình tư duy đó thành thứ nhìn thấy được thông qua design document.
Bạn Sẽ Học Được Gì
✅ Tại sao design doc quan trọng hơn bạn nghĩ
✅ Khi nào nên viết (và khi nào không)
✅ Template design doc thực tế có thể dùng ngay ngày mai
✅ Cách viết phần quan trọng nhất: trade-offs
✅ Ví dụ thực tế: API versioning, notification system, caching layer
✅ Cách dùng design doc để influence quyết định và build trust
✅ Những sai lầm phổ biến khiến design doc vô dụng
Tại Sao Design Doc Quan Trọng
Design doc không phải bureaucracy. Không phải thủ tục. Nó là công cụ tư duy.
Viết buộc bạn phải rõ ràng. Khi design chỉ nằm trong đầu, nó cảm giác hoàn chỉnh. Nhưng khoảnh khắc bạn cố viết ra — những lỗ hổng xuất hiện. Những edge case bạn lướt qua. Những assumption bạn không đặt câu hỏi. Những alternative bạn chưa xem xét.
Design doc thực sự làm gì
-
Buộc bạn nghĩ trước khi build. Viết là tư duy. Bạn không thể viết design doc rõ ràng mà không hiểu sâu problem.
-
Bộc lộ disagreement sớm. PR review là nơi tệ nhất để phát hiện approach của bạn conflict với hướng đi của team. Design doc bộc lộ những conflict này trước khi viết một dòng code nào.
-
Tạo mental model chung. Khi team đọc doc, ai cũng hiểu không chỉ cái gì bạn đang build, mà tại sao bạn chọn approach này thay vì những cái khác.
-
Ghi lại quyết định cho developer tương lai. Sáu tháng sau, ai đó sẽ hỏi "tại sao mình build kiểu này?" Design doc là câu trả lời — không phải một comment cũ trong code, mà là bản ghi đầy đủ về context, constraint, và lý do.
-
Build reputation bạn là engineer biết suy nghĩ. Developer viết design doc là người được trust với những problem lớn hơn. Không phải vì bản thân tài liệu, mà vì tài liệu chứng minh họ nghĩ trước khi hành động.
Mục tiêu của design doc không phải tạo ra document. Mà là tạo ra sự rõ ràng.
Khi Nào Nên Viết Design Doc
Không phải thứ gì cũng cần design doc. Viết cho bug fix hay CRUD endpoint đơn giản là overkill. Nhưng bỏ qua cho một feature quan trọng là cách bạn kết thúc bằng việc rewrite code hai tuần sau.
Viết design doc khi:
- Có nhiều approach hợp lệ. Nếu rõ ràng chỉ có một cách — cứ làm thôi. Nếu bạn đang phân vân giữa hai ba approach — viết ra.
- Thay đổi cross team boundaries. Nếu công việc ảnh hưởng API, data model, hoặc deployment của team khác — document approach để họ góp ý trước khi bạn bắt đầu.
- Thay đổi khó reverse. Database schema, public API contract, infrastructure decision — đều đắt đỏ để undo. Nghĩ hai lần, viết một lần.
- Bạn không chắc về approach. Nếu có doubt, đó là signal. Viết design doc là cách bạn giải quyết doubt trước khi nó biến thành code lãng phí.
- Feature mất hơn 2-3 ngày. Nếu đầu tư nhiều thời gian, bỏ 1-2 tiếng cho design doc là bảo hiểm rẻ.
Bỏ qua design doc khi:
- Solution rõ ràng và team đều hiểu
- Bug fix với root cause rõ ràng
- Thay đổi nhỏ và dễ dàng reverse
- Bạn đang prototype hoặc explore (viết doc sau khi học được gì đó)
Template Design Doc
Đây là template thực tế. Cố ý ngắn gọn — design doc nên mất 1-2 tiếng để viết, không phải một tuần. Nhắm 1-3 trang, không phải 10.
1. Problem Statement (3-5 câu)
Bạn đang giải quyết problem gì? Tại sao nó quan trọng bây giờ? Chuyện gì xảy ra nếu không làm gì?
Ví dụ tệ:
"Chúng ta cần thêm notification vào hệ thống."
Ví dụ tốt:
"User hiện không có cách nào biết khi order status thay đổi trừ khi họ refresh trang thủ công. Support ticket về 'đơn hàng bị mất' tăng 40% trong quý vừa rồi. Chúng ta cần notification system chủ động thông báo user về status change qua web và email."
Khác biệt? Ví dụ tốt cho bạn biết tại sao quan trọng, mức độ urgent thế nào, và scope bạn đang deal.
2. Goals và Non-Goals
Nêu rõ bạn muốn đạt được gì — và KHÔNG muốn đạt được gì. Non-goals quan trọng không kém goals vì chúng ngăn scope creep.
Ví dụ:
Goals:
- Notify user khi order status thay đổi (placed → confirmed → shipped → delivered)
- Support web push notification và email
- User có thể configure notification nào muốn nhận
Non-goals (cho iteration này):
- SMS notification (phase sau)
- Real-time in-app notification center (project riêng)
- Notification analytics hoặc A/B testing
- Notification cho admin/internal user
3. Proposed Solution
Mô tả approach ở mức detail phù hợp. Đây không phải code review — không cần specify từng function signature. Nhưng cần giải thích:
- Architecture tổng quan
- Data model hoặc schema change chính
- API contract (nếu team khác consume)
- Cách integrate với hệ thống hiện có
Dùng diagram. Một hình về cách data flow qua system đáng giá cả ngàn chữ.
4. Alternatives Considered (Phần Quan Trọng Nhất)
Đây là nơi tư duy senior thể hiện. Ai cũng có thể đề xuất solution. Nước đi senior là cho thấy bạn không chọn gì và tại sao.
Với mỗi alternative:
- Mô tả approach trong 2-3 câu
- Liệt kê pros
- Liệt kê cons
- Giải thích tại sao không chọn
5. Trade-offs và Risks
Mọi design đều có trade-off. Gọi tên chúng rõ ràng. Điều này thể hiện sự trưởng thành và build trust với reviewer.
6. Implementation Plan
Chia công việc thành phase hoặc milestone. Đây không phải sprint plan chi tiết — mà là sequence sơ bộ cho thấy bạn đã nghĩ về thứ tự delivery.
7. Open Questions
Những thứ bạn chưa biết. Những thứ cần input. Phần này mời gọi collaboration và thể hiện sự trung thực về mặt trí tuệ.
Cách Viết Phần Trade-offs (Skill Tạo Khác Biệt Cho Senior)
Phần trade-off là nơi đa số design doc hoặc tỏa sáng hoặc rớt đài. Đây là cách làm tốt.
Bảng so sánh
Luôn dùng table. Nó buộc có cấu trúc và giúp comparison dễ scan.
Ví dụ: Chọn notification delivery strategy
| Tiêu chí | Option A: Synchronous (in-process) | Option B: Async via message queue | Option C: Third-party service (vd: Novu) |
|---|---|---|---|
| Complexity | Thấp — gọi email/push API inline | Trung bình — cần setup message broker | Thấp — SDK integration |
| Reliability | Kém — email fail thì order update fail | Cao — retry với dead-letter queue | Cao — vendor quản lý |
| Latency impact | Tệ — thêm 200-500ms vào order API | Không — fire-and-forget | Không — fire-and-forget |
| Cost | Free (infrastructure có sẵn) | Thấp (managed queue service) | $$ khi scale (tính phí per-notification) |
| Vendor lock-in | Không | Không | Cao |
| Team familiarity | Cao | Trung bình (dùng RabbitMQ chỗ khác) | Thấp (vendor mới) |
| Observability | Khó debug failure | Tốt (queue metric, DLQ) | Tùy vendor dashboard |
Quyết định và lý do
Sau table, viết recommendation với reasoning:
Recommendation: Option B (Async via message queue)
Option A là non-starter — coupling notification delivery với order API tạo system mong manh, nơi email provider outage chặn order processing. Option C tiết kiệm thời gian develop nhưng tạo vendor lock-in và recurring cost không justify ở scale hiện tại (~5K notification/ngày).
Option B cho reliability qua retry và dead-letter queue, zero latency impact lên main API, và dùng infrastructure pattern team đã biết từ payment processing pipeline. Setup cost (1-2 ngày cho queue configuration) là đầu tư một lần.
Điều gì khiến phần này hiệu quả?
- Bạn không chỉ chọn cái ưa thích. Bạn cho thấy ba option thực sự.
- Đánh giá trên nhiều tiêu chí. Không chỉ "cái nào dễ nhất" mà complexity, reliability, cost, team familiarity.
- Giải thích lý do loại bỏ. "Option A là non-starter vì..." cho reader biết bạn đã nghĩ sâu.
- Kết nối với team context. "...dùng infrastructure pattern team đã biết" cho thấy bạn không design trong chân không.
Ví Dụ Thực Tế: API Versioning Design Doc
Cùng đi qua một design doc hoàn chỉnh, cô đọng cho một problem thực tế.
Problem Statement
Public REST API phục vụ 200+ external client. Chúng ta cần breaking change cho /users endpoint (restructure response format để support multi-tenancy). Hiện tại không có versioning strategy — bất kỳ breaking change nào đều break tất cả client cùng lúc. Cần versioning approach cho phép evolve API mà không làm gián đoạn integration hiện có.
Goals
- Đưa API versioning vào tất cả public endpoint
- Cho phép breaking change mà không disrupt client hiện tại
- Cung cấp deprecation và migration path rõ ràng
Non-Goals
- Internal API versioning (microservice-to-microservice)
- Automated client migration tool
- GraphQL migration (initiative riêng)
Alternatives Considered
| Tiêu chí | URL Path (/v1/users) | Header (Accept-Version: 1) | Query Param (?version=1) |
|---|---|---|---|
| Discoverability | Xuất sắc — nhìn thấy trong URL | Kém — ẩn trong header | Trung bình — thấy nhưng rối |
| Caching | Dễ — cache key dựa trên URL | Khó — cần Vary header | Trung bình — query string trong cache key |
| Client effort | Thấp — chỉ đổi base URL | Trung bình — sửa request header | Thấp — thêm parameter |
| Routing | Đơn giản — path-based routing | Phức tạp — header inspection | Trung bình — query parsing |
| Industry adoption | Rất phổ biến (Stripe, GitHub) | Ít phổ biến (REST purist) | Hiếm cho versioning |
| Proxy/CDN friendly | Có | Tùy configuration | Có |
Recommendation: URL Path Versioning
URL path versioning (/v1/users, /v2/users) là lựa chọn thực tế nhất cho tình huống của chúng ta:
- Client chủ yếu là server-side integration configure base URL một lần — đổi
/v1/thành/v2/là trivial. - CDN cache aggressive theo URL — header-based versioning cần Vary header configuration trên tất cả endpoint, thứ chúng ta từng gặp issue trước đây.
- Industry precedent quan trọng: Stripe, GitHub, và Twilio đều dùng URL path versioning. Client expect nó.
Trade-offs chấp nhận
- URL "pollution": Version hiển thị trong mọi URL. Chấp nhận vì discoverability quan trọng hơn thẩm mỹ.
- Route duplication: Cần maintain hai route handler trong transition period (3-6 tháng). Mitigation: shared service layer — chỉ controller/serializer thay đổi giữa các version.
- Không "content negotiation" thuần túy: REST purist thích header-based versioning. Chúng ta ưu tiên pragmatism hơn theoretical correctness.
Implementation Plan
- Tuần 1: Thêm version prefix routing (
/v1/*map tới handler hiện tại) - Tuần 2: Build
/v2/usersendpoint với response format mới - Tuần 3: Update API documentation, thông báo client về
/v2 - Tháng 2-4: Migration period — cả hai version active
- Tháng 5: Deprecation warning trên
/v1response - Tháng 6: Sunset
/v1
Open Questions
- Versioning toàn bộ API cùng lúc, hay cho phép per-endpoint versioning?
- Policy về thời gian deprecated version còn active là bao lâu?
- Có nên thêm
Sunsetheader vào deprecated endpoint (IETF draft standard)?
Ví Dụ Thực Tế: Caching Layer Design Doc (Cô Đọng)
Problem Statement
Product catalog API response time đã degrade từ 50ms lên 350ms p95 trong quý vừa rồi khi catalog tăng từ 10K lên 100K item. Database là bottleneck — mỗi request chạy 3-4 JOIN trên bảng products, categories, và pricing.
Options
| Tiêu chí | Application cache (in-memory) | Redis cache | Database materialized view |
|---|---|---|---|
| Latency reduction | ~1ms (cùng process) | ~5ms (network hop) | ~50ms (vẫn DB, nhưng pre-joined) |
| Cache invalidation | Khó (per-instance, không sharing) | Quản lý được (central, TTL + event-based) | Tự động (refresh theo schedule) |
| Memory cost | Đắt (mỗi instance giữ full cache) | Vừa phải (shared, single copy) | Free (DB xử lý) |
| Horizontal scaling | Tệ (cache per instance = inconsistency) | Tốt (shared cache) | Tốt (DB xử lý) |
| Complexity | Thấp | Trung bình | Thấp |
| Team experience | Cao (dùng trong auth) | Cao (dùng trong session management) | Thấp (chưa dùng trong project này) |
Recommendation: Redis cache với event-based invalidation
In-memory cache không work cho setup của chúng ta — chạy 4 API instance sau load balancer, cache inconsistency giữa các instance khiến user thấy price khác nhau tùy instance nào xử lý request.
Materialized view giải quyết query performance nhưng không address repeated identical query — vẫn hit database cho mọi request. Ở 500 req/s, đó là load không cần thiết.
Redis cho sub-5ms read cho repeated query, shared state trên tất cả instance, và chúng ta đã có Redis infrastructure từ session management. Invalidate khi có product/pricing update event từ admin service, với 5-phút TTL làm safety net.
Trade-offs
- Stale data window: Tối đa 5 phút cho non-event-driven change (chấp nhận được cho catalog data, không cho inventory count — cái đó bypass cache)
- Thêm infrastructure dependency: Redis trở thành critical path component. Mitigation: fallback về database khi Redis fail (circuit breaker pattern)
- Cache warming khi deploy: Request đầu tiên sau deploy sẽ chậm. Mitigation: pre-warm cache trong deployment rollout
Những Sai Lầm Giết Chết Design Doc
1. Viết tiểu thuyết
Design doc không phải specification. Nếu dài hơn 3 trang, bạn đã mất audience. Ngắn gọn. Dùng bullet point. Dùng table. Cắt phần thừa.
Quy tắc: Nếu reader không hiểu proposal trong 10 phút, doc quá dài.
2. Chỉ trình bày một option
Nếu chỉ trình bày một option, trông như bạn đã quyết định rồi và doc chỉ là hình thức. Luôn trình bày ít nhất hai alternative, dù một cái rõ ràng tốt hơn. Hành động so sánh mới là thứ build trust.
3. Giấu trade-off
Mọi design đều có downside. Nếu doc không đề cập, reviewer sẽ assume bạn chưa nghĩ tới. Gọi tên trade-off rõ ràng. Nó thể hiện sự trưởng thành.
4. Viết sau khi đã code xong
Design doc viết sau implementation là lãng phí thời gian của mọi người. Mục đích là nghĩ trước khi code, không phải justify cái đã build.
5. Không có open questions
Giả vờ có tất cả câu trả lời là red flag. Design doc tốt nhất có phần "những thứ tôi chưa biết" — mời gọi collaboration và thể hiện sự trung thực.
6. Bỏ qua "tại sao bây giờ"
Context quan trọng. Tại sao điều này quan trọng bây giờ? Cái gì thay đổi? Chi phí không làm gì là bao nhiêu? Thiếu urgency context, doc của bạn cạnh tranh với mọi initiative khác để giành attention.
Design Doc Như Công Cụ Sự Nghiệp
Đây là điều đa số developer bỏ lỡ: design doc không chỉ là artifact kỹ thuật. Nó là công cụ leadership.
Build influence
Khi viết design doc, bạn không xin phép — bạn đề xuất hướng đi. Bạn đang nói: "Tôi đã nghĩ sâu về problem này, cân nhắc các alternative, và đây là recommendation của tôi." Đó là leadership behavior, bất kể title của bạn.
Developer viết design doc:
- Được mời vào architecture discussion
- Được trust với project ambiguous, high-impact
- Build reputation là engineer biết suy nghĩ
- Có paper trail về judgment tốt — thứ nói hộ họ khi promotion
Từ documentation đến discussion
Design doc là conversation starter, không phải conversation ender. Outcome tốt nhất không phải ai cũng rubber-stamp proposal — mà là doc khơi mào discussion dẫn đến solution tốt hơn.
Share doc trước khi code. Hỏi feedback cụ thể: "Mình đặc biệt muốn nghe ý kiến về caching invalidation strategy — đó là phần mình ít confident nhất." Điều này thể hiện sức mạnh, không phải điểm yếu.
Hiệu ứng compound
Theo thời gian, design doc của bạn trở thành thư viện quyết định. Team member mới đọc chúng để hiểu tại sao system trông như vậy. Trong incident, bạn reference chúng để hiểu constraint ban đầu. Trong planning, bạn revisit chúng để xem assumption nào đã thay đổi.
Bạn không chỉ build software — bạn đang build institutional knowledge.
Quick-Start Checklist
Sẵn sàng viết design doc đầu tiên? Đây là checklist:
- Problem rõ ràng: Người ngoài team có hiểu bạn đang giải quyết gì và tại sao không?
- Goals và non-goals defined: Scope có explicit không?
- Ít nhất 2 alternative: Bạn đã cho thấy nhiều approach chưa?
- Trade-offs table: Các alternative được so sánh trên nhiều tiêu chí chưa?
- Recommendation có reasoning: Bạn giải thích tại sao, không chỉ cái gì?
- Kết nối team context: Recommendation có xét đến team skill, infrastructure hiện có, và timeline không?
- Implementation plan sơ bộ: Có delivery sequence sơ bộ không?
- Open questions liệt kê: Những unknown được gọi tên trung thực chưa?
- Độ dài hợp lý: Ai đó có thể đọc xong trong 10 phút không?
- Share trước khi code: Bạn đã lấy feedback trước khi viết dòng code nào chưa?
Kết Luận
Gap giữa developer mid-level và senior không phải ở viết code tốt hơn. Mà ở tư duy rõ ràng hơn trước khi viết bất kỳ dòng code nào.
Design doc là cách bạn biến tư duy đó thành thứ nhìn thấy được. Chúng buộc bạn articulate problem, xem xét alternative, cân nhắc trade-off, và đưa ra recommendation có lý do. Chúng bộc lộ disagreement trước khi chúng biến thành PR lãng phí. Chúng build reputation bạn là người nghĩ trước khi hành động.
Bạn không cần quy trình formal hay management buy-in. Cứ bắt đầu. Lần tới bạn nhận task có nhiều approach hợp lệ, bỏ một tiếng viết ra suy nghĩ. Share với team. Hỏi feedback.
Cái đầu tiên sẽ gượng. Cái thứ hai sẽ nhanh hơn. Đến cái thứ năm, nó thành second nature — và bạn sẽ tự hỏi làm sao trước đây mình từng code mà không nghĩ trước.
Bài Viết Liên Quan
- Choosing the Right Architecture: Decision Framework & Trade-offs — Ra quyết định architecture với ADR template (bổ sung cho focus feature-level của bài này)
- Domain-Driven Design: Strategic & Tactical Patterns — Cách model domain phức tạp với bounded context và aggregate
- Stop Over-Engineering: You're Not Building Netflix — Mặt kia: khi nào đơn giản tốt hơn phức tạp
- How to Never Be Replaced: A Developer's Growth Mindset Guide — Những habit compound thành tư duy senior
- SOLID Principles Explained — Các design principle hỗ trợ phân tích trade-off
Series: Senior Developer Playbook
Next: Domain Modeling in Practice: Boundaries, Bounded Contexts & Real Examples
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.