Back to blog

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

senior-developersoftware-designcareerbest-practicesleadership
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ì

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 queueOption C: Third-party service (vd: Novu)
ComplexityThấp — gọi email/push API inlineTrung bình — cần setup message brokerThấp — SDK integration
ReliabilityKém — email fail thì order update failCao — retry với dead-letter queueCao — vendor quản lý
Latency impactTệ — thêm 200-500ms vào order APIKhông — fire-and-forgetKhông — fire-and-forget
CostFree (infrastructure có sẵn)Thấp (managed queue service)$$ khi scale (tính phí per-notification)
Vendor lock-inKhôngKhôngCao
Team familiarityCaoTrung bình (dùng RabbitMQ chỗ khác)Thấp (vendor mới)
ObservabilityKhó debug failureTố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ả?

  1. Bạn không chỉ chọn cái ưa thích. Bạn cho thấy ba option thực sự.
  2. Đá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.
  3. Giải thích lý do loại bỏ. "Option A là non-starter vì..." cho reader biết bạn đã nghĩ sâu.
  4. 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)
DiscoverabilityXuất sắc — nhìn thấy trong URLKém — ẩn trong headerTrung bình — thấy nhưng rối
CachingDễ — cache key dựa trên URLKhó — cần Vary headerTrung bình — query string trong cache key
Client effortThấp — chỉ đổi base URLTrung bình — sửa request headerThấp — thêm parameter
RoutingĐơn giản — path-based routingPhức tạp — header inspectionTrung bình — query parsing
Industry adoptionRất phổ biến (Stripe, GitHub)Ít phổ biến (REST purist)Hiếm cho versioning
Proxy/CDN friendlyCóTùy configurationCó

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

  1. Tuần 1: Thêm version prefix routing (/v1/* map tới handler hiện tại)
  2. Tuần 2: Build /v2/users endpoint với response format mới
  3. Tuần 3: Update API documentation, thông báo client về /v2
  4. Tháng 2-4: Migration period — cả hai version active
  5. Tháng 5: Deprecation warning trên /v1 response
  6. 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 Sunset header 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 cacheDatabase materialized view
Latency reduction~1ms (cùng process)~5ms (network hop)~50ms (vẫn DB, nhưng pre-joined)
Cache invalidationKhó (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 scalingTệ (cache per instance = inconsistency)Tốt (shared cache)Tốt (DB xử lý)
ComplexityThấpTrung bìnhThấp
Team experienceCao (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

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.