Phần 1: Kiến thức về dữ liệu — nền móng của mọi hệ thống backend
Backend, suy cho cùng, là nghệ thuật quản lý dữ liệu: nhận dữ liệu vào, biến đổi nó, lưu trữ nó và trả nó ra một cách nhanh chóng, chính xác, an toàn. Vì vậy, trước khi nói về framework hay kiến trúc, hãy chắc chắn bạn nắm vững những điều sau.
1. Cơ sở dữ liệu quan hệ và SQL — kỹ năng bị đánh giá thấp nhất
Nhiều bạn trẻ học ORM trước khi học SQL, và đó là một sai lầm. ORM chỉ là lớp vỏ — khi hệ thống chậm, bạn sẽ phải đọc câu SQL mà ORM sinh ra để hiểu chuyện gì đang xảy ra.
Những thứ cần thành thạo:
- Mô hình quan hệ: bảng, khóa chính, khóa ngoại, và các dạng chuẩn hóa (1NF → 3NF). Quan trọng không kém: biết khi nào nên phi chuẩn hóa có chủ đích để tăng tốc độ đọc.
- SQL thực chiến: các loại JOIN, subquery, CTE, window functions, GROUP BY/HAVING. Một câu SQL tốt có thể thay thế hàng trăm dòng code xử lý ở tầng ứng dụng.
- Index: hiểu cách B-tree index hoạt động, composite index và tầm quan trọng của thứ tự cột, covering index. Và đặc biệt: biết khi nào index bị “vô hiệu hóa” — dùng hàm trên cột được index,
LIKE '%abc', hoặc so sánh sai kiểu dữ liệu.
2. Transaction và ACID — bảo hiểm cho dữ liệu của bạn
Transaction đảm bảo một nhóm thao tác hoặc thành công toàn bộ, hoặc thất bại toàn bộ. Bốn tính chất ACID (Atomicity, Consistency, Isolation, Durability) là kiến thức phỏng vấn kinh điển, nhưng thứ thực sự phân biệt junior và senior là hiểu về isolation level:
- Read Uncommitted → Read Committed → Repeatable Read → Serializable
- Các hiện tượng đi kèm: dirty read, non-repeatable read, phantom read
Cùng với đó là locking: phân biệt pessimistic lock (khóa trước, làm sau) và optimistic lock (làm trước, kiểm tra version khi ghi), hiểu deadlock xảy ra như thế nào và cách phòng tránh.
3. NoSQL — công cụ, không phải trào lưu
Mỗi loại NoSQL sinh ra để giải một bài toán cụ thể:
- Document store (MongoDB): dữ liệu có cấu trúc linh hoạt, lồng nhau
- Key-value (Redis): cache, session, dữ liệu cần truy xuất cực nhanh
- Wide-column (Cassandra): ghi nhiều, dữ liệu khổng lồ, phân tán
- Search engine (Elasticsearch): tìm kiếm toàn văn
Đi kèm là định lý CAP: trong hệ phân tán, khi xảy ra phân mảnh mạng, bạn buộc phải đánh đổi giữa tính nhất quán (Consistency) và tính sẵn sàng (Availability). Khái niệm eventual consistency — dữ liệu sẽ nhất quán “sau một lúc” — nghe đơn giản nhưng kéo theo rất nhiều hệ quả trong thiết kế.
Lời khuyên: đừng chọn database theo trend. Hầu hết ứng dụng khởi đầu tốt nhất với PostgreSQL hoặc MySQL, thêm Redis để cache.
4. Mô hình hóa dữ liệu — thiết kế theo cách truy vấn
Sai lầm phổ biến là thiết kế schema chỉ dựa trên cấu trúc thực thể mà quên mất câu hỏi quan trọng nhất: dữ liệu này sẽ được truy vấn như thế nào?
Vài nguyên tắc thực tế:
- Dùng DECIMAL cho tiền, tuyệt đối không dùng FLOAT (sai số dấu phẩy động sẽ khiến bạn mất ngủ).
- Lưu thời gian theo UTC, chỉ chuyển sang giờ địa phương khi hiển thị.
- Cân nhắc UUID vs auto-increment ID: UUID tiện cho hệ phân tán và không lộ số lượng bản ghi, nhưng tốn dung lượng và làm index phân mảnh hơn.
- Quyết định sớm về soft delete vs hard delete, audit log và versioning dữ liệu.
5. Vận hành dữ liệu — nơi lý thuyết gặp thực tế
- Migration an toàn: thay đổi schema trên hệ thống đang chạy cần làm theo nhiều bước — thêm cột nullable trước, backfill dữ liệu, rồi mới thêm ràng buộc. Đừng bao giờ
ALTER TABLEkiểu “tất tay” trên bảng triệu dòng giờ cao điểm. - Backup: có backup chưa đủ — phải thực sự test việc restore. Backup chưa từng được restore thử cũng giống như không có backup.
- Replication và sharding: master-replica để scale đọc, sharding để scale ghi — và sharding luôn phức tạp hơn bạn nghĩ.
- Connection pooling: mở kết nối database rất tốn kém, pool là bắt buộc.
- Caching: nắm các pattern cache-aside, write-through, TTL. Và hãy nhớ câu nói nổi tiếng: trong khoa học máy tính chỉ có hai bài toán khó — đặt tên biến và cache invalidation.
Phần 2: 20 lỗi backend developer thường mắc phải
Nhóm 1: Lỗi về dữ liệu và database
1. N+1 query — lỗi quốc dân của ORM
Bạn lấy 100 đơn hàng, rồi với mỗi đơn lại query riêng để lấy thông tin khách hàng → 101 câu query thay vì 1-2 câu với JOIN hoặc eager loading. Ứng dụng chạy mượt lúc dev, sập khi production có dữ liệu thật. Hãy bật log SQL của ORM lên và nhìn vào số lượng query mỗi request.
2. SQL Injection
Nối chuỗi trực tiếp vào câu query thay vì dùng parameterized query. Lỗi cũ kỹ nhưng năm nào cũng có hệ thống bị hack vì nó.
-- Đừng bao giờ:
"SELECT * FROM users WHERE email = '" + email + "'"
-- Luôn luôn:
"SELECT * FROM users WHERE email = ?"
3. Quên đánh index (hoặc đánh index tràn lan)
Cột hay được lọc/sắp xếp mà không có index → full table scan. Ngược lại, index quá nhiều làm chậm thao tác ghi và tốn dung lượng. Hãy dùng EXPLAIN để xem query plan thay vì đoán.
4. Quên transaction cho các thao tác liên quan
Trừ tiền tài khoản và tạo đơn hàng là hai thao tác phải đi cùng nhau. Không bọc trong transaction, lỗi xảy ra giữa chừng → tiền bị trừ nhưng đơn hàng không tồn tại.
5. Race condition
Ví dụ kinh điển: hai request cùng lúc đọc số dư 100k, cùng kiểm tra “đủ tiền”, cùng trừ 80k — và cả hai đều thành công. Giải pháp: dùng lock hoặc atomic update:
UPDATE accounts
SET balance = balance - 80000
WHERE id = ? AND balance >= 80000
6. SELECT * và không phân trang ở tầng query
Lấy toàn bộ cột khi chỉ cần vài cột, load cả triệu dòng vào memory thay vì phân trang. Server hết RAM là chuyện sớm muộn.
Nhóm 2: Lỗi về API và xử lý request
7. Tin tưởng dữ liệu từ client
“Frontend đã validate rồi” là câu nói nguy hiểm nhất trong nghề. Mọi dữ liệu từ client — body, query param, header — đều phải được coi là không đáng tin và validate lại ở server.
8. Trả lỗi không nhất quán hoặc lộ thông tin nội bộ
Stack trace, câu SQL lỗi bị trả thẳng về client là món quà cho hacker. Mỗi endpoint trả format lỗi một kiểu là cơn ác mộng cho frontend. Hãy chuẩn hóa cấu trúc lỗi cho toàn bộ API.
9. Endpoint không phân trang
API trả về toàn bộ records — chạy ổn lúc dev với 50 dòng, timeout khi production có 5 triệu dòng. Phân trang (offset hoặc cursor-based) phải là mặc định, không phải tính năng “làm sau”.
10. API không idempotent cho thao tác nhạy cảm
Người dùng bấm “Thanh toán”, mạng lag, bấm lại lần nữa → bị trừ tiền hai lần. Các thao tác quan trọng cần idempotency key: cùng một key, dù gọi bao nhiêu lần cũng chỉ thực thi một lần.
Nhóm 3: Lỗi về bảo mật
11. Lưu mật khẩu sai cách
Plain text là tội ác. MD5/SHA1 cũng không khá hơn vì quá nhanh, dễ bị brute force. Hãy dùng bcrypt, scrypt hoặc argon2 — các thuật toán được thiết kế để chậm một cách có chủ đích.
12. Hardcode secret vào code
API key, mật khẩu database nằm trong code và bị đẩy lên Git. Kể cả repo private cũng không an toàn. Dùng biến môi trường hoặc secret manager, và một khi secret đã lên Git thì coi như đã lộ — phải rotate ngay.
13. Lỗi phân quyền (IDOR)
Lỗi tinh vi và phổ biến hơn bạn tưởng: hệ thống check user đã đăng nhập nhưng quên check user đó có quyền với resource cụ thể hay không. Kết quả: GET /orders/123 cho phép xem đơn hàng của người khác chỉ bằng cách đổi số ID trên URL.
14. Không có rate limiting
Endpoint đăng nhập không giới hạn số lần thử → mời gọi brute force. API công khai không rate limit → một script vô tình (hoặc cố ý) có thể kéo sập hệ thống.
Nhóm 4: Lỗi về kiến trúc và vận hành
15. Logging và monitoring sơ sài
Đến khi production lỗi mới phát hiện không có log để điều tra. Log tốt cần có context (request ID, user ID, timestamp) nhưng tuyệt đối không log dữ liệu nhạy cảm như mật khẩu, token, số thẻ.
16. Xử lý tác vụ nặng một cách đồng bộ
Gửi email, xử lý ảnh, gọi API bên thứ ba ngay trong request chính → request chậm, dễ timeout, trải nghiệm người dùng tệ. Những việc này nên đẩy vào queue và xử lý bằng background job.
17. Không set timeout khi gọi service ngoài
Một service bên thứ ba phản hồi chậm có thể giữ toàn bộ connection của bạn, kéo sập cả hệ thống. Mọi lời gọi ra ngoài đều cần timeout hợp lý, kèm theo circuit breaker nếu có thể.
18. Giả định external service luôn thành công
Gọi API thanh toán mà không xử lý trường hợp thất bại, không retry, không fallback. Mạng sẽ lỗi, service sẽ sập — câu hỏi chỉ là khi nào, và hệ thống của bạn phản ứng ra sao.
19. Over-engineering
Dựng microservices, Kafka, Kubernetes cho ứng dụng 100 người dùng. Độ phức tạp là chi phí — chỉ trả khi thực sự cần. Một monolith được tổ chức tốt đi xa hơn bạn nghĩ rất nhiều.
20. Môi trường dev và production khác nhau quá xa
Config hardcode, dev dùng SQLite còn production dùng PostgreSQL, dữ liệu test không phản ánh thực tế. “Trên máy em chạy được mà” — câu nói huyền thoại sinh ra từ chính lỗi này. Hãy dùng biến môi trường, container hóa và giữ các môi trường càng giống nhau càng tốt.
Lời kết
Điểm chung của hầu hết các lỗi trên: chúng không lộ diện lúc dev, chỉ bùng phát khi hệ thống có dữ liệu thật, người dùng thật và tải thật. Vì vậy, kinh nghiệm backend không nằm ở việc viết code chạy được, mà ở việc lường trước những cách mà hệ thống có thể hỏng.
Nếu bạn mới bắt đầu, đừng cố nhớ hết 20 lỗi cùng lúc. Hãy bắt đầu từ ba thứ có tác động lớn nhất: học SQL tử tế, luôn validate input, và bọc các thao tác liên quan trong transaction. Phần còn lại sẽ đến cùng với những lần “sập production” — bài học mà không cuốn sách nào dạy thấm bằng.
Chúc bạn code vui, và mong rằng production của bạn luôn bình yên!
