Tìm cách giữ mã sinh dễ bảo trì bằng nguyên tắc kiến trúc nhàm chán: ranh giới thư mục rõ, quy ước đặt tên nhất quán và mặc định đơn giản để giảm công việc sửa sau này.

Mã được sinh thay đổi công việc hàng ngày. Bạn không chỉ xây tính năng, bạn còn hướng hệ thống có thể tạo nhiều file nhanh chóng. Tốc độ là thật, nhưng những không nhất quán nhỏ nhân lên rất nhanh.
Kết quả sinh thường trông ổn nếu nhìn riêng lẻ. Chi phí lộ ra ở lần thay đổi thứ hai, thứ ba: bạn không biết phần nào thuộc nơi nào, bạn sửa cùng hành vi ở hai chỗ, hoặc bạn né không động vào file vì không biết ảnh hưởng ra sao.
“Thông minh” quá mức tốn kém vì khó dự đoán. Mẫu tuỳ chỉnh, magic ẩn và abstraction nặng có thể hợp lý ngày đầu. Đến tuần thứ sáu, thay đổi tiếp theo chậm lại vì bạn phải học lại mánh trước khi cập nhật an toàn. Với sinh mã hỗ trợ AI, sự tinh vi đó còn có thể làm rối các lần sinh sau, dẫn tới logic lặp lại hoặc từng lớp mới chồng lên nhau.
Kiến trúc nhàm chán là ngược lại: ranh giới rõ ràng, tên đơn giản và mặc định hiển nhiên. Không phải theo đuổi hoàn hảo. Mà là chọn cách bố trí mà đồng đội mệt mỏi (hoặc chính bạn tương lai) có thể hiểu trong 30 giây.
Mục tiêu đơn giản: làm cho thay đổi tiếp theo dễ, chứ không phải ấn tượng. Thường nghĩa là một chỗ rõ ràng cho mỗi loại mã (UI, API, dữ liệu, tiện ích dùng chung), tên dễ đoán thể hiện chức năng file, và ít “magic” như auto-wiring, global ẩn, hay metaprogramming.
Ví dụ: nếu bạn yêu cầu Koder.ai thêm “mời nhóm”, bạn muốn nó đặt UI trong khu UI, thêm một route API trong khu API và lưu dữ liệu invite ở tầng data, chứ không phát minh một thư mục hay pattern mới chỉ cho tính năng đó. Sự nhất quán nhàm chán đó giữ sửa đổi sau này rẻ.
Mã sinh tốn kém khi nó cho nhiều cách làm cùng một việc. Nguyên tắc kiến trúc nhàm chán đơn giản: làm cho thay đổi tiếp theo dễ dự đoán, dù lần xây đầu tiên ít “thông minh” hơn.
Bạn nên trả lời những câu này nhanh:
Chọn một cấu trúc đơn giản và dính chặt nó ở mọi nơi. Khi công cụ (hoặc đồng đội) gợi ý pattern hoa mỹ, câu trả lời mặc định là “không” trừ khi nó thực sự loại bỏ đau đầu.
Các mặc định thực tế giữ được theo thời gian:
Tưởng tượng một dev mới mở repo và cần thêm nút “Hủy đăng ký”. Họ không nên phải học kiến trúc tuỳ chỉnh trước. Họ nên tìm thấy khu tính năng rõ ràng, component UI rõ ràng, một client API duy nhất và một đường truy cập dữ liệu duy nhất.
Quy tắc này đặc biệt phù hợp với công cụ tạo nhanh như Koder.ai: bạn có thể sinh nhanh, nhưng vẫn hướng kết quả vào cùng ranh giới nhàm chán mỗi lần.
Mã sinh có xu hướng tăng nhanh. Cách an toàn nhất để giữ nó dễ bảo trì là một bản đồ thư mục nhàm chán, nơi ai cũng đoán được sự thay đổi thuộc chỗ nào.
Một layout top-level nhỏ phù hợp nhiều web app:
app/ screens, routing, and page-level statecomponents/ reusable UI piecesfeatures/ one folder per feature (billing, projects, settings)api/ API client code and request helpersserver/ backend handlers, services, and business rulesĐiều này làm ranh giới rõ ràng: UI nằm trong app/ và components/, lời gọi API nằm trong api/, và logic backend nằm trong server/.
Truy cập dữ liệu cũng nên nhàm chán. Giữ SQL và repository gần backend, không rải rác trong file UI. Trong setup Go + PostgreSQL, quy tắc đơn giản: HTTP handlers gọi services, services gọi repositories, repositories nói chuyện với database.
Types và utils chia sẻ xứng đáng có chỗ rõ ràng, nhưng giữ nó nhỏ. Đặt types chéo vào types/ (DTOs, enums, shared interfaces) và helper nhỏ vào utils/ (format ngày, validator đơn giản). Nếu utils/ bắt đầu như một app thứ hai, có lẽ mã nên về thư mục feature.
Đối xử với thư mục sinh như có thể thay thế.
generated/ (hoặc gen/) và tránh sửa trực tiếp.features/ hoặc server/ để tái sinh không ghi đè.Ví dụ: nếu Koder.ai sinh một client API, lưu nó dưới generated/api/, rồi viết các wrapper mỏng trong api/ để thêm retry, logging, hoặc thông báo lỗi rõ ràng mà không chạm vào file sinh.
Mã sinh dễ tạo và dễ chất đống. Đặt tên giữ nó đọc được sau một tháng.
Chọn một style và đừng trộn:
kebab-case (user-profile-card.tsx, billing-settings)PascalCase (UserProfileCard)camelCase (getUserProfile)SCREAMING_SNAKE_CASE (MAX_RETRY_COUNT)Đặt tên theo vai trò, không theo cách nó hoạt động hiện tại. user-repository.ts là một vai trò. postgres-user-repository.ts là chi tiết triển khai có thể thay đổi. Chỉ thêm hậu tố triển khai khi thực sự có nhiều triển khai.
Tránh các "ngăn kéo rác" như misc, helpers, hay một utils khổng lồ. Nếu hàm chỉ dùng bởi một feature, giữ nó gần feature đó. Nếu nó chia sẻ, làm tên mô tả khả năng (date-format.ts, money-format.ts, id-generator.ts) và giữ module nhỏ.
Khi routes, handlers và components theo pattern, bạn tìm được thứ mà không cần search:
routes/users.ts với đường dẫn như /users/:userIdhandlers/users.get.ts, handlers/users.update.tsservices/user-profile-service.tsrepositories/user-repository.tscomponents/user/UserProfileCard.tsxNếu bạn dùng Koder.ai (hoặc bất kỳ generator nào), đặt những quy tắc này trong prompt và giữ chúng nhất quán khi chỉnh sửa. Mục tiêu là tính dự đoán: nếu bạn đoán được tên file, thay đổi sau này sẽ rẻ hơn.
Mã sinh có thể trông ấn tượng ngày đầu và đau ngày thứ ba mươi. Chọn mặc định khiến mã hiển nhiên, ngay cả khi hơi lặp lại.
Bắt đầu bằng cách giảm magic. Bỏ qua dynamic loading, mẹo reflection, và auto-wiring trừ khi có nhu cầu đo lường. Những tính năng này che nơi xuất phát của thứ gì, làm debug và refactor chậm hơn.
Ưu tiên import rõ ràng và phụ thuộc minh bạch. Nếu file cần thứ gì, import trực tiếp. Nếu modules cần wiring, làm ở một chỗ nhìn thấy được (ví dụ, một file composition duy nhất). Người đọc không nên phải đoán thứ gì chạy trước.
Giữ config nhàm chán và tập trung. Đặt biến môi trường, feature flags và cài đặt app-wide trong một module với một cách đặt tên. Đừng rải config khắp file vì cảm thấy tiện.
Nguyên tắc giữ đội nhất quán:
Xử lý lỗi là nơi tinh ranh gây hại nhất. Chọn một pattern và dùng nó khắp nơi: trả về lỗi có cấu trúc từ tầng dữ liệu, map chúng sang HTTP responses ở một chỗ, và chuyển thành thông báo người dùng ở ranh giới UI. Đừng ném ba kiểu lỗi khác nhau tuỳ file.
Nếu bạn sinh app với Koder.ai, yêu cầu mặc định này từ đầu: wiring module rõ ràng, config tập trung, và một pattern lỗi duy nhất.
Đường ranh rõ giữa UI, API và dữ liệu giữ thay đổi trong phạm vi. Hầu hết bug bí ẩn xảy ra khi một tầng bắt đầu làm việc của tầng khác.
Xem UI (thường React) là nơi render màn hình và quản lý state chỉ dành cho UI: tab mở, lỗi form, spinner tải và xử lý input cơ bản.
Giữ server state riêng: danh sách fetch, profile cache và bất cứ thứ gì phải khớp backend. Khi component UI bắt đầu tính tổng, validate luật phức tạp, hoặc quyết định quyền, logic lan rộng khắp màn hình và trở nên đắt để thay đổi.
Giữ tầng API dự đoán được. Nó nên chuyển HTTP request thành lời gọi tới business code, rồi chuyển kết quả về dạng request/response ổn định. Tránh gửi mô hình database trực tiếp lên wire. Response ổn định cho phép refactor nội bộ mà không phá UI.
Một đường dẫn đơn giản hoạt động tốt:
Đặt SQL (hoặc logic ORM) sau ranh repository để phần còn lại của app không “biết” cách lưu trữ. Trong Go + PostgreSQL, thường là các repository như UserRepo hoặc InvoiceRepo với các method nhỏ, rõ ràng (GetByID, ListByAccount, Save).
Ví dụ cụ thể: thêm mã giảm giá. UI hiển thị ô nhập và cập nhật giá hiển thị. API nhận code và trả {total, discount}. Service quyết định code có hợp lệ và cách giảm giá chồng nhau. Repository lấy và lưu các hàng cần thiết.
App sinh có thể trông “xong” nhanh, nhưng cấu trúc mới làm cho thay đổi rẻ sau này.
Bắt đầu với một lượt hoạch định ngắn. Nếu bạn dùng Koder.ai, Planning Mode là nơi tốt để viết bản đồ thư mục và vài quy tắc đặt tên trước khi sinh bất cứ thứ gì.
Rồi theo trình tự này:
ui/, api/, data/, features/) và vài quy tắc đặt tên.CONVENTIONS.md ngắn và coi nó như hợp đồng. Khi codebase lớn, đổi tên và mô hình thư mục sẽ tốn kém.Thực tế: nếu người mới không đoán được nơi để đặt “edit contact” mà không hỏi, kiến trúc vẫn chưa đủ nhàm chán.
Hình dung CRM đơn giản: trang danh sách contacts và form sửa contact. Bạn làm phiên bản đầu nhanh, rồi một tuần sau cần thêm “tags” cho contacts.
Xử lý app như ba hộp nhàm chán: UI, API và dữ liệu. Mỗi hộp có ranh giới rõ và tên đích thực để thay đổi “tags” giữ nhỏ.
Layout sạch có thể trông như:
web/src/pages/ContactsPage.tsx và web/src/components/ContactForm.tsxserver/internal/http/contacts_handlers.goserver/internal/service/contacts_service.goserver/internal/repo/contacts_repo.goserver/migrations/Bây giờ “tags” trở nên dự đoán được. Cập nhật schema (bảng contact_tags mới hoặc cột tags), rồi chạm từng tầng: repo đọc/ghi tags, service validate, handler expose field, UI render và edit. Đừng lén đưa SQL vào handlers hoặc business rule vào component React.
Nếu sau này product yêu cầu “filter theo tag”, bạn hầu hết làm ở ContactsPage.tsx (UI state và query params) và HTTP handler (parsing request), còn repo xử lý query.
Cho tests và fixtures, giữ nhỏ và gần mã:
server/internal/service/contacts_service_test.go cho rule như “tên tag phải duy nhất trên contact”server/internal/repo/testdata/ cho fixtures tối thiểuweb/src/components/__tests__/ContactForm.test.tsx cho hành vi formNếu bạn sinh bằng Koder.ai, quy tắc vẫn áp dụng sau khi export: giữ thư mục nhàm chán, tên literal, và chỉnh sửa không còn như khảo cổ.
Mã sinh có thể trông sạch ngày đầu và vẫn đắt sau này. Thủ phạm thường không phải “mã xấu”, mà là không nhất quán.
Thói quen tốn kém là để generator phát minh cấu trúc mỗi lần. Một feature có thư mục, kiểu đặt tên và helper riêng, và bạn có ba cách làm cùng một việc. Chọn một pattern, viết nó ra, và coi pattern mới là thay đổi có chủ ý, không phải mặc định.
Bẫy khác là trộn các tầng. Khi component UI nói chuyện trực tiếp với database, hoặc handler API tự xây SQL, thay đổi nhỏ biến thành sửa rủi ro khắp app. Giữ ranh giới: UI gọi API, API gọi service, service gọi data access.
Dùng abstraction generic quá sớm cũng tăng chi phí. Một “BaseService” toàn năng trông gọn, nhưng abstraction sớm thường là phỏng đoán. Khi thực tế thay đổi, bạn chống lại framework tự tạo thay vì ra tính năng.
Đổi tên và tái tổ chức liên tục là dạng nợ im lặng. Nếu file di chuyển mỗi tuần, người ta mất lòng tin vào layout và các sửa nhanh rơi vào chỗ ngẫu nhiên. Ổn định bản đồ thư mục trước, rồi refactor theo đợt có kế hoạch.
Cuối cùng, cẩn thận với “platform code” không có người dùng thực. Thư viện chia sẻ và tooling tự làm chỉ có lợi khi bạn có nhu cầu lặp lại, nếu không, giữ mặc định trực tiếp.
Nếu người mới mở repo, họ nên trả lời một câu nhanh: “Tôi thêm cái này ở đâu?”
Giao project cho đồng đội (hoặc bạn tương lai) và bảo họ thêm tính năng nhỏ, như “thêm trường vào form đăng ký”. Nếu họ không tìm thấy chỗ đúng nhanh, cấu trúc chưa hiệu quả.
Kiểm tra ba nhà ở rõ ràng:
Nếu nền tảng của bạn hỗ trợ, giữ đường lui. Snapshot và rollback rất hữu ích khi bạn thử nghiệm cấu trúc và muốn quay về an toàn.
Bảo trì cải thiện nhanh nhất khi bạn ngừng tranh luận style và bắt đầu quyết vài điều dính. Viết ra một bộ quy ước nhỏ loại bỏ do dự hàng ngày: file để đâu, đặt tên thế nào, xử lý lỗi và config ra sao. Giữ nó đủ ngắn để đọc trong một phút.
Rồi làm một lượt dọn dẹp để khớp những quy tắc đó và ngừng xáo trộn hàng tuần. Tái tổ chức thường xuyên làm thay đổi tiếp theo chậm hơn, dù mã trông có vẻ đẹp hơn.
Nếu bạn xây với Koder.ai (koder.ai), lưu những quy ước này làm prompt khởi đầu để mỗi lần sinh mới nằm đúng cấu trúc. Công cụ có thể chạy nhanh, nhưng ranh giới nhàm chán là thứ giữ mã dễ sửa đổi.