Software Architecture

Cấu trúc vs. Hành vi: Tại sao kiến trúc sư phải quan tâm đến khả năng bảo trì hơn là tính năng

7/19/2026 · 12p đọc


title: "Cấu trúc vs. Hành vi: Tại sao kiến trúc sư phải quan tâm đến khả năng bảo trì hơn là tính năng"
series: "Software Architecture Masterclass"
part: "Phần 1 — Nguyên lý & Triết lý Kiến trúc"
order: 3
audience: "Solutions Architect & Technical Lead"
reading_time: "13 phút"
tags:

  • software-architecture
  • maintainability
  • structure-vs-behavior
  • technical-debt
  • layered-architecture
  • architecture-review

Cấu trúc vs. Hành vi: Tại sao kiến trúc sư phải quan tâm đến khả năng bảo trì hơn là tính năng

Có một cuộc họp sprint review mà gần như kiến trúc sư nào cũng từng dự ít nhất một lần. Product Owner hài lòng vì demo chạy mượt. Team engineering vỗ ngực vì "lại một sprint năng suất cao". Velocity chart đi lên đều đặn ba quý liền. Không ai nói ra, nhưng bên dưới lớp mã nguồn đang thắng lợi ấy, ranh giới module đã bị xuyên thủng ở gần chục chỗ — service A gọi thẳng vào bảng dữ liệu nội bộ của service B, layer trình bày (presentation layer) chứa logic nghiệp vụ, và không ai còn nhớ nổi luồng dữ liệu thật sự đi qua bao nhiêu tầng trước khi tới database.

Đây không phải là một câu chuyện về năng lực kém. Đây là hệ quả tất yếu của một hệ thống khuyến khích sai thứ. Khi mọi thước đo thành công đều là "tính năng nào vừa xong", không ai được thưởng vì giữ được một ranh giới module sạch sẽ mà không ai nhìn thấy. Kiến trúc mục ruỗng không xảy ra trong một buổi chiều tệ hại — nó xảy ra trong hàng trăm quyết định nhỏ, mỗi quyết định đều "hợp lý" trong bối cảnh cục bộ của nó, để rồi một ngày công ty nhận ra: thêm một trường dữ liệu mới cần sửa 40 file, và không ai còn dám refactor vì sợ vỡ thứ gì đó không biết đang phụ thuộc vào đâu.

Vấn đề gốc rễ nằm ở việc lẫn lộn hai khái niệm tưởng như đồng nhất nhưng bản chất đối lập nhau về tốc độ thay đổi: cấu trúc (structure) và hành vi (behavior). Bài này mổ xẻ sự phân biệt đó, và tại sao vai trò của kiến trúc sư — nói thẳng — là bảo vệ cái ổn định trong khi để cái biến động tự do bung nở.

The Problem

Hãy hình dung một hệ thống CRM điển hình. Sprint 1: thêm tính năng "gắn tag cho khách hàng". Sprint 2: thêm "workflow phê duyệt hợp đồng". Sprint 3: thêm "tích hợp webhook cho đối tác thứ ba". Mỗi tính năng đều có deadline, đều có stakeholder chờ đợi, đều được đo bằng story points đã đóng. Áp lực luôn là: code chạy được, test pass, demo xong, merge.

Nhưng có một câu hỏi không ai hỏi trong ba sprint đó: tính năng mới này có tôn trọng ranh giới đã được thiết lập không? Nó có gọi thẳng vào internal của module khác thay vì qua interface công khai không? Nó có nhét business logic vào controller vì "nhanh hơn" không? Nó có làm cho hai module vốn độc lập giờ phải deploy cùng nhau vì coupling ngầm không?

Không ai hỏi, vì không ai bị phạt khi không hỏi. Ngược lại, người hỏi thường bị xem là "làm chậm tiến độ". Và đó chính xác là cách một kiến trúc sạch trở thành một kiến trúc mục ruỗng: không phải vì một quyết định tồi tệ duy nhất, mà vì hàng trăm quyết định "tạm thời chấp nhận được" cộng dồn lại, không ai chịu trách nhiệm tổng thể, cho tới khi độ phức tạp vượt ngưỡng con người có thể giữ trong đầu.

Điểm bùng phát luôn đến theo cùng một kịch bản: một tính năng đơn giản trên giấy — "chỉ cần thêm một điều kiện lọc" — hóa ra đụng vào 15 chỗ không liên quan, mất ba tuần thay vì ba ngày, và ai đó trong phòng họp buông câu "hay là mình viết lại từ đầu". Đó là lúc công ty phải trả toàn bộ hóa đơn technical debt tích lũy suốt nhiều năm, cộng thêm phí tổn cơ hội vì đóng băng roadmap trong nhiều tháng để rewrite. Chi phí của rewrite không chỉ là effort kỹ thuật — nó là mất thị phần, mất niềm tin khách hàng, và thường kết thúc bằng một hệ thống mới... sẽ mục ruỗng theo đúng cách cũ nếu kỷ luật không đổi.

The Theory

Để hiểu vì sao chuyện này xảy ra một cách hệ thống chứ không phải ngẫu nhiên, cần tách bạch hai khái niệm mà ngôn ngữ hàng ngày hay gộp chung làm một: cấu trúchành vi.

Hành vi (behavior) là những gì hệ thống làm — tính năng, business rule, luồng nghiệp vụ, output cho một input cụ thể. Hành vi được đặc trưng bởi tốc độ thay đổi cao. Yêu cầu nghiệp vụ hôm nay khác ngày mai; sản phẩm sống được là nhờ liên tục thích nghi hành vi theo thị trường. Một CRM năm nay cần workflow phê duyệt 3 bước, sang năm có thể cần 5 bước với điều kiện rẽ nhánh theo hạn mức — đó là hành vi thay đổi, và nó nên thay đổi liên tục, vì đó là cách sản phẩm tạo giá trị.

Cấu trúc (structure) là mối quan hệ giữa các thành phần của hệ thống — module nào phụ thuộc module nào, dữ liệu chảy qua những tầng nào, ranh giới nào là bất khả xâm phạm, contract nào là hợp đồng công khai giữa các bên. Cấu trúc, khác với hành vi, phải có tốc độ thay đổi thấp và có chủ đích. Nó là bộ xương giữ cho cơ thể đứng vững trong khi các cơ quan bên trong (hành vi) liên tục vận động.

Đây chính là nguyên lý cốt lõi mà kiến trúc sư phải nội tâm hóa: hành vi được phép — và nên — thay đổi nhanh; cấu trúc phải thay đổi chậm, có kiểm soát, và chỉ khi có lý do kiến trúc thực sự (không phải vì tiện cho một tính năng đơn lẻ). Đây gần như là diễn giải trực tiếp của nguyên lý Open/Closed: hệ thống nên mở với việc mở rộng hành vi (extension) nhưng đóng với việc sửa đổi cấu trúc nền tảng (modification) mỗi khi có nhu cầu nghiệp vụ mới.

Vai trò kiến trúc sư, nói một cách trần trụi, không phải là "người viết code khung" hay "người vẽ sơ đồ". Nó là người gác cổng cho tính bất biến của cấu trúc, trong khi để đội ngũ tính năng (feature team) tự do sáng tạo trong không gian mà cấu trúc đó cho phép. Nếu một tính năng mới cần thay đổi cấu trúc — ví dụ thêm một tầng mới, đổi contract giữa hai module — đó không còn là quyết định của feature team nữa, mà phải đi qua review kiến trúc, vì nó ảnh hưởng đến mọi tính năng tương lai, không chỉ tính năng đang làm.

Sự nhầm lẫn tai hại nhất là khi team coi cấu trúc như một chi tiết triển khai của tính năng, thay vì một tài sản chung phải bảo vệ độc lập với bất kỳ tính năng cụ thể nào. Khi controller viết thẳng SQL vì "nhanh hơn gọi qua service layer", đó là hành vi đang ăn mòn cấu trúc. Khi service A import trực tiếp entity của service B thay vì qua API hoặc event, đó là ranh giới bounded context (đã bàn ở bài 10) đang bị xóa nhòa để đổi lấy vài phút tiết kiệm hôm nay.

The Trade-off

Đây là phần không thể mơ hồ. Không có lựa chọn nào miễn phí.

Chọn kỷ luật cấu trúc chặt — nghĩa là: interface rõ ràng giữa các module, layer nghiêm ngặt (presentation không được đụng vào domain logic, domain không được đụng vào infrastructure), contract giữa các bounded context được review kỹ trước khi thay đổi, mọi truy cập chéo module phải qua cổng công khai.

  • Được gì: Tính năng mới có thể được thêm vào mà không cần hiểu toàn bộ hệ thống — chỉ cần hiểu contract của module mình chạm vào. Bug được cô lập trong ranh giới module, không lan tỏa. Refactor một module không kéo theo domino effect sang module khác. Onboarding thành viên mới nhanh hơn vì họ chỉ cần nắm một lát cắt, không phải toàn bộ monolith tư duy. Hệ thống có thể sống 5-10 năm mà không cần rewrite toàn bộ.
  • Mất gì: Tốc độ ra tính năng ban đầu chậm hơn rõ rệt — có thể chậm 20-40% so với việc "viết tắt". Mỗi tính năng cần thời gian thiết kế interface, viết DTO, xử lý mapping giữa các tầng thay vì query thẳng. Đội ngũ phải chịu overhead của việc review kiến trúc cho mọi thay đổi chạm ranh giới — đôi khi cảm giác như quan liêu, đặc biệt với các tính năng nhỏ mà bản chất nghiệp vụ đơn giản nhưng vẫn phải đi đúng quy trình. Có nguy cơ thực sự của over-engineering: thiết kế interface cho những trường hợp chưa từng và có thể không bao giờ xảy ra.

Chọn cấu trúc lỏng lẻo — nghĩa là: cho phép truy cập chéo module khi "cần thiết", bỏ qua layer khi thấy "quá đơn giản để cần abstraction", không có review kiến trúc bắt buộc, để feature team tự quyết định cách implement miễn là chạy được.

  • Được gì: Tốc độ ra tính năng cực nhanh trong 6-12 tháng đầu. Không có overhead review, không có thời gian "lãng phí" thiết kế interface cho use case chưa rõ ràng. Team nhỏ, sản phẩm giai đoạn early-stage cần validate thị trường nhanh — đây có thể là lựa chọn đúng đắn có chủ đích, không phải sai lầm.
  • Mất gì: Nợ kỹ thuật (technical debt) tích lũy âm thầm, không hiển thị trên bất kỳ dashboard nào cho tới khi đạt ngưỡng tới hạn. Chi phí thay đổi (cost of change) tăng theo cấp số nhân theo thời gian — tính năng thứ 100 tốn gấp nhiều lần công sức so với tính năng thứ 10, dù độ phức tạp nghiệp vụ tương đương. Không ai dám refactor vì không biết phạm vi ảnh hưởng thực sự. Cuối cùng, đội ngũ đối mặt với lựa chọn nhị phân tồi tệ: hoặc đóng băng tính năng để "dọn dẹp" trong nhiều tháng, hoặc viết lại từ đầu — cả hai đều là chi phí cơ hội khổng lồ so với việc trả nợ dần dần.

Điểm mấu chốt: đây không phải là "cách nào đúng" mà là "cách nào đúng cho giai đoạn nào, quy mô nào, và ai là người trả giá". Một startup 3 người validate MVP trong 2 tháng thì kỷ luật cấu trúc chặt là lãng phí — có thể sẽ pivot toàn bộ domain trước khi cấu trúc đó kịp trả giá trị. Một hệ thống CRM phục vụ hàng nghìn khách hàng doanh nghiệp, vòng đời dự kiến 5-10 năm, với đội ngũ hàng chục kỹ sư luân chuyển liên tục — kỷ luật lỏng lẻo ở đây không phải rủi ro, nó là chắc chắn sẽ nổ.

The Architecture Diagram

Sơ đồ dưới đây minh họa nguyên lý cốt lõi: các tầng cấu trúc ổn định (structure) nằm bên dưới, thay đổi chậm và có kiểm soát; các tính năng (behavior) liên tục được thêm/sửa/xóa ở tầng trên, nhưng luôn phải đi qua các cổng (interface/contract) đã định nghĩa — không bao giờ được xuyên thẳng xuống các tầng dưới.

flowchart TB
    subgraph BEHAVIOR["🔄 TẦNG HÀNH VI — thay đổi liên tục theo sprint"]
        F1["Tính năng: Gắn tag khách hàng<br/>(sprint 12)"]
        F2["Tính năng: Workflow phê duyệt<br/>(sprint 13)"]
        F3["Tính năng: Webhook đối tác<br/>(sprint 14)"]
        F4["Tính năng: Báo cáo tùy biến<br/>(sprint 15)"]
    end

    subgraph GATE["🚪 CỔNG KIẾN TRÚC — hợp đồng công khai, ổn định"]
        API["Application Service Interface<br/>(use-case boundary)"]
    end

    subgraph STRUCTURE["🏛️ TẦNG CẤU TRÚC — thay đổi chậm, có review"]
        DOMAIN["Domain Layer<br/>entities, business rules"]
        INFRA["Infrastructure Layer<br/>DB, cache, message queue"]
        BOUND["Bounded Context Boundaries<br/>module A ↔ module B"]
    end

    F1 --> API
    F2 --> API
    F3 --> API
    F4 --> API
    API --> DOMAIN
    DOMAIN --> INFRA
    DOMAIN --> BOUND

    F3 -.->|"❌ VI PHẠM: gọi thẳng<br/>xuống Infra để 'nhanh hơn'"| INFRA

    style BEHAVIOR fill:#2d3748,stroke:#4299e1,color:#fff
    style GATE fill:#553c9a,stroke:#9f7aea,color:#fff
    style STRUCTURE fill:#1a202c,stroke:#48bb78,color:#fff
    style F3 stroke:#f56565,stroke-width:2px

Đường nét đứt màu đỏ trong sơ đồ là hiện thân của điểm bùng phát mô tả ở phần Problem: một tính năng "chỉ cần nhanh" bỏ qua cổng kiến trúc, gọi thẳng vào tầng hạ tầng. Một lần thì vô hại. Mười lần thì ranh giới coi như không tồn tại.

Implementation Guardrails

  1. Mọi truy cập chéo module bắt buộc đi qua interface công khai đã định nghĩa (Application Service hoặc event), không bao giờ import trực tiếp entity/repository của module khác. Đây là ranh giới cứng, kiểm tra được bằng lint rule hoặc dependency-cruiser trong CI — không phải quy ước bằng miệng.

  2. Mọi Pull Request thêm tính năng nhưng thay đổi hoặc vi phạm ranh giới lớp kiến trúc (đổi contract, thêm dependency chéo module, đổi luồng dữ liệu qua tầng) bắt buộc phải có review kiến trúc riêng biệt, tách khỏi code review thông thường, trước khi merge. Review kiến trúc trả lời một câu hỏi khác review code: "thay đổi này có ảnh hưởng đến mọi tính năng tương lai không, hay chỉ tính năng hiện tại?"

  3. Presentation layer (controller, resolver) không bao giờ chứa business logic; business logic chỉ sống ở domain/service layer. Nếu một controller có if liên quan đến quy tắc nghiệp vụ (không phải validate input), đó là dấu hiệu cấu trúc đang bị ăn mòn.

  4. Mọi ngoại lệ vi phạm ranh giới vì lý do "tạm thời" phải được ghi lại thành technical debt item có chủ đích (không phải im lặng bỏ qua), kèm deadline trả nợ. Nợ có ý thức khác nợ vô thức — cái đầu tiên là quyết định kiến trúc, cái sau là tai nạn.

Architect's Verdict

Hy sinh tốc độ ngắn hạn để giữ tính toàn vẹn cấu trúc — đó là cái giá bắt buộc phải trả nếu hệ thống còn sống quá một năm. Nhưng kỷ luật không có nghĩa là cầu toàn: biết khi nào "đủ tốt" là thực sự đủ quan trọng không kém biết khi nào phải nói không. Một kiến trúc sư giỏi không phải người từ chối mọi ngoại lệ, mà là người phân biệt được đâu là "vi phạm có ý thức, có ghi nhận, có kế hoạch trả nợ" và đâu là "xói mòn âm thầm không ai chịu trách nhiệm". Bảo vệ cấu trúc không phải để đẹp trên giấy — nó là bảo hiểm cho khả năng công ty còn thay đổi được sản phẩm ba năm nữa, khi mà tính năng hôm nay đã bị quên lãng từ lâu.

🔗 Bài viết liên quan: First Principles Thinking cho Software Architect · Bounded Contexts — Về biên giới


Bài trước: First Principles Thinking cho Software Architect · Bài tiếp theo: Tính chất của một hệ thống tốt

Cấu trúc vs. Hành vi: Tại sao kiến trúc sư phải quan tâm đến khả năng bảo trì hơn là tính năng