Tôi đã dừng lại để hỏi "Tại sao?" trước khi gõ dòng code đầu tiên — và mọi thứ thay đổi từ đó
Tôi đã dừng lại để hỏi "Tại sao?" trước khi gõ dòng code đầu tiên — và mọi thứ thay đổi từ đó
Hành trình từ một developer "gõ phím giỏi" đến người thực sự tư duy để giải quyết vấn đề — cùng những bài học cụ thể từ thực chiến với JavaScript & TypeScript
1. Sự kiện đã thay đổi cách tôi nghĩ về code
Hồi mới ra trường, tôi tự hào lắm vì gõ code nhanh. Một ticket được assign vào sáng, chiều đã có PR. Teammates khen, lead gật đầu. Tôi nghĩ mình đang làm tốt.
Rồi một buổi chiều thứ Sáu, khách hàng gọi điện cho PM. Feature tôi vừa ship tuần trước — đúng theo spec, pass hết test — nhưng sau 5 ngày lên production, không có một user nào dùng. Không phải vì bug. Không phải UX xấu. Mà vì nó giải quyết sai vấn đề.
Câu chuyện cụ thể thế này: task lúc đó là "Thêm tính năng lọc danh sách đơn hàng theo ngày". Tôi làm date range picker đẹp, responsive, có preset "Hôm nay / 7 ngày / 30 ngày". Nhưng điều mà user thực sự cần — mà không ai hỏi — là xuất báo cáo tháng để gửi kế toán. Họ không cần lọc. Họ cần một nút "Export tháng này ra Excel".
Tôi đã dành 3 ngày để làm thứ không ai cần. Và tôi không nhận ra điều đó vì tôi không hỏi.
⚠️ Bài học đầu tiên: Viết code giỏi và giải quyết vấn đề tốt là hai chuyện hoàn toàn khác nhau. Bạn có thể xuất sắc ở cái đầu tiên mà vẫn thất bại hoàn toàn ở cái thứ hai.
2. Execution mindset — cái bẫy vô hình
Sau sự kiện đó, tôi bắt đầu quan sát lại quy trình làm việc của mình — và của những người xung quanh. Tôi nhận ra một pattern rất phổ biến mà tôi gọi là execution mindset. Nó trông như thế này:
- Nhận ticket từ Jira / Trello
- Đọc mô tả, hiểu yêu cầu kỹ thuật
- Estimate, tạo branch, bắt đầu code
- Commit, push, tạo PR
- Done. Lấy ticket tiếp.
Nhìn vào thì không có gì sai. Đây là cách làm việc của hầu hết developer ở hầu hết các công ty. Nhưng vấn đề nằm ở chỗ: không có bước nào để hỏi "Vấn đề thực sự là gì?".
Execution mindset biến developer thành một cái máy thực thi rất hiệu quả — nhưng thực thi thứ gì thì do người khác quyết định hoàn toàn. Và khi người khác hiểu sai vấn đề, hoặc mô tả không đủ, developer cũng chỉ biết làm theo.
"The most dangerous kind of waste is the waste of doing the wrong thing well." — Shigeo Shingo
Tại sao chúng ta rơi vào cái bẫy này?
Có vài lý do phổ biến mà tôi đã tự phân tích ở bản thân:
- Áp lực velocity: Sprint deadline, KPI số ticket hoàn thành, lead muốn thấy progress — tất cả đẩy chúng ta về phía "làm nhanh" thay vì "làm đúng". Dừng lại để hỏi đôi khi cảm giác như đang lãng phí thời gian.
- Sợ bị nghĩ là không hiểu: Hỏi "Tại sao chúng ta cần tính năng này?" đôi khi bị nhìn nhận là đang thách thức quyết định của người khác, hoặc đơn giản là không đủ năng lực để tự hiểu.
- Thói quen được tạo ra từ trường: Trong đại học, bài tập luôn có đề rõ ràng — input, output, constraints. Không ai dạy chúng ta cách tự đặt đề bài.
- Niềm vui của việc code: Thành thật mà nói — code thú vị hơn việc ngồi hỏi stakeholder. Mở editor lên, gõ phím, thấy output — đó là dopamine loop rất dễ bị nghiện.
3. Bước "uncode" — giải mã trước khi code
Từ sau sự cố đó, tôi thêm một bước vào quy trình của mình: bước uncode. Trước khi mở editor, tôi dành 10–15 phút để trả lời 5 câu hỏi sau, viết ra file note:
- Vấn đề gốc rễ là gì? — Không phải vấn đề được mô tả trong ticket, mà là vấn đề của người dùng cuối.
- Ai sẽ bị ảnh hưởng nếu không làm tính năng này? — Nếu câu trả lời là "không ai", có lẽ cần xem lại mức độ ưu tiên.
- User sẽ làm gì với kết quả sau khi dùng feature này? — Bước tiếp theo trong workflow của họ thường tiết lộ rất nhiều thứ.
- Có edge case nào mà spec chưa đề cập? — Đây là nơi bug thường ẩn náu.
- Có cách nào đơn giản hơn mà vẫn giải quyết được vấn đề không? — Giải pháp tốt nhất đôi khi là ít code nhất.
Câu hỏi cuối cùng là câu tôi hay ngại hỏi nhất — vì sợ bị nghĩ là lười hoặc đang né tránh công việc. Nhưng dần dần tôi nhận ra: đề xuất một giải pháp đơn giản hơn, ít tốn resource hơn, là dấu hiệu của một engineer trưởng thành, không phải engineer lười biếng.
💡 Thực hành ngay: Lần tới khi nhận một ticket, trước khi mở VS Code, hãy mở một file text trống và trả lời 5 câu hỏi trên. Chỉ 10–15 phút — nhưng nó sẽ thay đổi cách bạn tiếp cận task đó hoàn toàn.
4. Khi TypeScript dạy tôi tư duy domain-first
Một bước ngoặt thú vị đến từ... TypeScript. Nghe có vẻ không liên quan đến mindset, nhưng hãy để tôi giải thích.
Khi còn dùng JavaScript thuần, tôi có xu hướng "nghĩ bằng code" — tức là tôi bắt đầu code trước khi thực sự hiểu rõ domain.
// JavaScript — "nghĩ bằng code"
// Nhận task: "Tính tổng tiền đơn hàng" → mở editor, gõ ngay
function processOrder(order) {
return order.items.reduce((sum, item) => {
return sum + item.price * item.qty;
}, 0);
}
// Chạy tốt! Nhưng...
// - price là trước hay sau thuế?
// - qty có thể là số thập phân không (ví dụ: 0.5 kg)?
// - Có discount không? Discount áp vào item hay vào order?
// - Currency là gì? Có float precision issue không?
// Những câu hỏi này chỉ nảy ra khi bug xảy ra.
Khi chuyển sang TypeScript, mọi thứ thay đổi. Vì để viết được type, tôi buộc phải suy nghĩ về domain trước.
// TypeScript — buộc phải "giải mã domain" trước khi code
type Currency = 'VND' | 'USD';
type Money = {
amount: number; // Luôn là số nguyên (tính bằng đồng / cent)
currency: Currency;
};
type OrderItem = {
productId: string;
name: string;
unitPrice: Money; // Giá trước thuế
quantity: number; // Số nguyên — đơn vị là cái/hộp/gói
discountPercent?: number; // 0–100, optional
};
type OrderStatus =
| 'draft' // Đang tạo
| 'confirmed' // Đã xác nhận
| 'processing' // Đang xử lý
| 'shipped' // Đã giao carrier
| 'delivered' // Giao thành công
| 'cancelled'; // Đã hủy
type Order = {
id: string;
customerId: string;
items: OrderItem[];
status: OrderStatus;
taxRate: number; // Ví dụ: 0.1 = 10% VAT
createdAt: Date;
};
// Bây giờ mới viết business logic — rõ ràng hơn nhiều
function calculateItemTotal(item: OrderItem): Money {
const { amount, currency } = item.unitPrice;
const discounted = item.discountPercent
? amount * (1 - item.discountPercent / 100)
: amount;
return {
amount: Math.round(discounted * item.quantity), // Tránh float issue
currency,
};
}
function calculateOrderTotal(order: Order): Money {
const subtotal = order.items.reduce(
(sum, item) => sum + calculateItemTotal(item).amount,
0
);
const tax = Math.round(subtotal * order.taxRate);
return {
amount: subtotal + tax,
currency: order.items[0].unitPrice.currency,
};
}
Nhìn vào đoạn TypeScript trên, bạn thấy gì? Không chỉ là "code có type". Đó là cả một cuộc trò chuyện với domain: Money là gì? Tại sao dùng số nguyên? OrderStatus có những giá trị nào? Discount áp vào item hay order? VAT tính như thế nào?
Discriminated union — phát hiện bug trước khi code
Một kỹ thuật TypeScript nữa đã thay đổi cách tôi mô hình hoá data:
// Ban đầu tôi định nghĩa User như này
type User = {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
isActive: boolean;
};
// Nhưng khi ngồi nhìn vào, câu hỏi nảy ra:
// "role" và "isActive" có thực sự độc lập không?
// Một admin bị deactivate vẫn là admin — có nghĩa gì về mặt business?
// → Phát hiện ra business rule bị thiếu trong spec!
// Sau khi clarify với PM:
type UserStatus =
| { kind: 'active'; role: 'admin' | 'user' }
| { kind: 'suspended'; suspendedAt: Date; reason: string }
| { kind: 'deleted'; deletedAt: Date };
type User = {
id: string;
name: string;
email: string;
status: UserStatus;
};
"Make illegal states unrepresentable." — Richard Feldman
Đây là một trong những nguyên tắc lập trình yêu thích nhất của tôi, và TypeScript giúp thực hiện nó rất hiệu quả.
5. Từ feature factory đến problem solver
Sau vài năm chú tâm thay đổi mindset, tôi nhận ra sự khác biệt rõ ràng nhất không nằm ở kỹ năng kỹ thuật. Nó nằm ở cách tôi tham gia vào cuộc trò chuyện về sản phẩm.
Trước đây: Sprint planning nghĩa là ngồi nghe PM mô tả feature, estimate, gật đầu. Câu hỏi nếu có thường là "Feature này cần làm gì?" hoặc "Deadline là khi nào?" Rất ít khi hỏi "Tại sao chúng ta cần cái này?"
Bây giờ: Khi có một feature request, tôi hay hỏi thêm vài câu trước khi estimate:
- "User journey hiện tại của họ là gì? Họ đang làm gì trước và sau bước này?"
- "Chúng ta có data nào về việc user đang gặp vấn đề ở đây không — support ticket, session recording, user feedback?"
- "Nếu chúng ta không làm feature này, user sẽ làm gì thay thế?"
- "Success metric của feature này là gì? Sau 1 tháng, chúng ta nhìn vào số nào để biết nó thành công?"
Những câu hỏi này đôi khi dẫn đến việc feature bị deprioritize, scope bị thu hẹp, hoặc approach được thay đổi hoàn toàn. Và đó là kết quả tốt — tốt hơn nhiều so với ship một feature mà không ai dùng.
✅ Câu chuyện thực tế: Một lần team đang plan feature "Thêm notification real-time khi có đơn hàng mới". Tôi hỏi "User hiện tại đang biết về đơn hàng mới bằng cách nào?" — Họ đang nhận email. "Email đó đủ nhanh không?" — Đủ, chỉ delay 30 giây. "Vậy pain point thực sự là gì?" — Hoá ra user không cần real-time. Họ cần xem được nhiều thông tin hơn ngay trong email đó. Scope thay đổi hoàn toàn, implementation đơn giản hơn 10 lần.
6. Những thói quen nhỏ tạo ra sự khác biệt lớn
Thay đổi mindset không xảy ra qua một đêm. Nó đến từ những thói quen nhỏ, được thực hành đều đặn.
6.1. Rubber duck debugging ở mức cao hơn
Mọi người biết rubber duck debugging cho bugs. Tôi áp dụng tương tự cho feature design: trước khi code, tôi "giải thích" feature đó bằng tiếng Việt thông thường, viết ra file note.
Nếu tôi không thể giải thích được feature một cách mạch lạc bằng ngôn ngữ tự nhiên, tức là tôi chưa hiểu đủ để code nó.
6.2. Viết test case trước khi viết code
Không nhất thiết phải TDD theo nghĩa strict. Nhưng việc ngồi liệt kê các test case trước — kể cả chỉ viết ra dạng comment — buộc bạn phải nghĩ về tất cả các scenario và edge case.
// Trước khi code function validatePhoneNumber():
// ✅ Số điện thoại VN hợp lệ: 0912345678 (10 số, bắt đầu 0)
// ✅ Format quốc tế: +84912345678
// ✅ Có space hoặc dash: 0912 345 678, 0912-345-678
// ❌ Quá ngắn: 091234567 (9 số)
// ❌ Có chữ cái: 091234567a
// ❓ Số nước ngoài: +1 800 555 0199 — có support không?
// → Câu hỏi này cần hỏi PM trước khi code!
6.3. Code review với câu hỏi "tại sao" thay vì chỉ "như thế nào"
Khi review PR của người khác, tôi cố gắng không chỉ hỏi về implementation details. Tôi cũng hỏi về lý do:
- "Cách tiếp cận này giải quyết vấn đề X thế nào so với cách Y?"
- "Đây là business requirement hay technical decision?"
- "Nếu requirement thay đổi sau này, phần nào cần sửa nhiều nhất?"
Những câu hỏi này không chỉ giúp tôi hiểu code tốt hơn — nó còn giúp người được review tự phát hiện ra vấn đề mà họ chưa thấy.
6.4. Đọc code cũ như đọc một bản ghi lịch sử
Khi tiếp cận một codebase mới, thay vì chỉ đọc để hiểu "nó làm gì", tôi cố đọc để hiểu "nó đang giải quyết vấn đề gì, và tại sao lại giải quyết theo cách này".
// Code này trông weird:
const RETRY_DELAY = [1000, 3000, 10000]; // Tại sao lại là 3 số này?
async function fetchWithRetry(url: string) {
for (let i = 0; i < RETRY_DELAY.length; i++) {
try {
return await fetch(url);
} catch (e) {
if (i === RETRY_DELAY.length - 1) throw e;
await sleep(RETRY_DELAY[i]);
}
}
}
// git log --follow -p reveals:
// commit a3f9c12 — "fix: tăng retry delay vì upstream API rate limit 3 req/10s"
// → Không phải magic number. Là business knowledge được encode vào code.
git blame và git log là bạn tốt nhất trong những lúc đó.
7. Lời kết: Mindset bền vững hơn mọi framework
Framework rồi sẽ thay đổi. React có thể nhường chỗ cho thứ khác. TypeScript sẽ có phiên bản mới. AI tools sẽ ngày càng viết code giỏi hơn.
Nhưng khả năng hiểu đúng vấn đề, đặt câu hỏi đúng, và xây dựng giải pháp thực sự giải quyết nhu cầu của người dùng — đó là thứ không AI nào có thể thay thế hoàn toàn trong tương lai gần. Và đó cũng là thứ phân biệt một developer bình thường với một engineer thực sự tạo ra impact.
Tôi không nghĩ mình đã đến đích của hành trình này. Mỗi ngày tôi vẫn mắc sai lầm, vẫn có lúc code xong mới nhận ra mình hiểu sai requirement. Nhưng khoảng cách giữa "làm xong" và "làm đúng" ngày càng thu hẹp lại — và điều đó đến từ thói quen dừng lại và hỏi trước khi gõ phím.
Chúc mừng Viblo Mayfest 2026! Hãy cùng nhau uncode the mindset — không chỉ giải mã tư duy của bản thân, mà còn lan tỏa cách nghĩ đó đến cả team và cộng đồng xung quanh mình. 🚀
All Rights Reserved