HARRY-32 — GĐ26 — Frontend Architecture: ứng dụng mở rộng, Design System, Micro Frontends & Module Federation
GĐ26 — Frontend Architecture: ứng dụng mở rộng, Design System, Micro Frontends & Module Federation
Tài liệu dành cho frontend engineer đã làm được một SPA/React app và muốn giữ codebase, UI consistency cùng nhịp giao hàng khi sản phẩm và số team lớn lên. Giai đoạn này bắt đầu từ kiến trúc modular trong một ứng dụng, rồi mới xem xét chia ứng dụng thành nhiều phần deploy độc lập.
1. Kết quả cần đạt
Sau giai đoạn này, bạn có thể:
- Chia frontend theo năng lực nghiệp vụ và luồng người dùng, thay vì chỉ chia theo loại file.
- Thiết lập ranh giới module có public API, dependency rule, route ownership và test tương ứng.
- Thiết kế Design System có token, component API, trạng thái, accessibility, tài liệu, release/versioning và quy trình nhận đóng góp.
- Giải thích Micro Frontend giải quyết bài toán tổ chức đội ngũ và triển khai độc lập như thế nào, cùng chi phí về tải, vận hành và governance.
- Phân biệt package/component library, route-level app, server-side composition, iframe/Web Component và runtime Module Federation.
- Dựng một host + remote bằng Module Federation ở mức minh họa, hiểu
remotes,exposes,shared, version negotiation, remote failure và rollback. - Đưa ra quyết định có bằng chứng: modular monolith, monorepo package hay Micro Frontend phù hợp với quy mô nào.
Điều kiện vào
Bạn nên đọc JavaScript/TypeScript, viết được HTML/CSS, dùng một UI framework và hiểu route, component state, npm package cùng quá trình build/deploy frontend. Kiến thức backend/CI ở các giai đoạn trước giúp hiểu API, auth và pipeline, nhưng GĐ26 là nhánh chuyên sâu frontend; không cần làm xong GĐ25 Capstone mới được học.
2. Kiến trúc frontend là gì?
Kiến trúc không phải cây thư mục trông “enterprise”. Nó là các quyết định có chủ đích về:
- Ranh giới: tính năng nào sở hữu state, API, route và UI nào?
- Hướng phụ thuộc: phần nào được phép import phần nào?
- Giao tiếp: module trao đổi dữ liệu qua function, event, route, API hay message contract?
- Thay đổi: một yêu cầu nghiệp vụ bình thường sửa bao nhiêu module và có cần phối hợp bao nhiêu team?
- Chất lượng: lỗi permission, render lại, bundle tăng và visual regression bị phát hiện ở đâu?
Kiến trúc tốt giúp một thay đổi có phạm vi đoán trước. Ví dụ: thay cách tải danh sách sản phẩm nên chủ yếu ở feature catalog; không làm luồng checkout, navigation và mọi package UI cùng thay đổi.
Chia theo feature / vertical slice
Một tổ chức đơn giản có thể bắt đầu như sau:
src/
app/ # bootstrap, router, providers, layout toàn ứng dụng
features/
catalog/ # route, UI, state, API adapter, types của catalog
cart/ # giỏ hàng
checkout/ # checkout
account/ # hồ sơ và cài đặt tài khoản
shared/
ui/ # primitive UI trung tính: Button, Dialog, TextField
lib/ # helper dùng chung, không chứa nghiệp vụ
api/ # client nền, error mapping, request IDfeatures/checkout có thể dùng primitive shared/ui/Button, nhưng shared/ui không import features/checkout. Feature không nên đọc thẳng internal file của feature khác; trao đổi qua public API hẹp hoặc tầng ứng dụng điều phối.
Ví dụ public API:
// features/catalog/index.ts
export { CatalogRoutes } from './routes/CatalogRoutes'
export { useProductSearch } from './api/useProductSearch'
export type { Product, ProductFilter } from './model/types'Consumer import từ package boundary:
import { CatalogRoutes } from '@/features/catalog'Tránh:
// Coupling vào cấu trúc nội bộ, đổi tên file có thể phá consumer.
import { ProductCard } from '@/features/catalog/components/internal/ProductCard'Public barrel chỉ có ích nếu nó thật sự là contract. Đừng export mọi file vì “mai mốt tiện”; mỗi export làm tăng bề mặt API cần giữ tương thích.
Phân ranh component
- Primitive: button, text field, dialog, menu. Không biết order/catalog/user cụ thể.
- Domain component:
ProductPrice,OrderStatus,AddressForm. Hiểu domain nhưng không sở hữu cả page. - Feature/page: điều phối query, mutation, loading/empty/error state và các component domain.
- Application shell: routing, session, navigation toàn app, global providers, error boundaries.
Không có một luật “component phải dưới 200 dòng” đủ tốt cho mọi dự án. Hãy tách khi một module có hơn một lý do thay đổi, public API khó hiểu, test setup nặng hoặc một thay đổi kéo theo sửa nhiều consumer.
3. Ranh giới dữ liệu, route và state
Chọn đúng nơi giữ state
| Loại state | Ví dụ | Nơi phù hợp |
|---|---|---|
| Tạm thời của một control | dialog đang mở, input chưa submit | local component state |
| Theo URL, có thể bookmark/share | filter, sort, page, tab | route/query string |
| Dữ liệu từ server | product list, order detail | query/cache layer có invalidation rõ |
| State cross-cutting của app | session hiện tại, locale | application provider/store với owner rõ |
| State bền giữa lần mở | draft, offline queue | IndexedDB/server; có schema version và cleanup |
| State giữa các micro frontend | current route, identity | contract tối thiểu từ shell; tránh global mutable store dùng chung |
Không đưa mọi state vào một global store. Global hóa làm thay đổi nhỏ ảnh hưởng khó theo dõi, mở rộng quyền ghi và khiến feature không còn tự kiểm soát lifecycle của state.
API adapter theo feature
// features/catalog/api/products.ts
export type ProductFilter = { query?: string; categoryId?: string }
export async function searchProducts(filter: ProductFilter, signal?: AbortSignal) {
const url = new URL('/api/products', window.location.origin)
if (filter.query) url.searchParams.set('q', filter.query)
if (filter.categoryId) url.searchParams.set('categoryId', filter.categoryId)
const response = await fetch(url, { signal })
if (!response.ok) throw new Error(`Product search failed: ${response.status}`)
return response.json() as Promise<{ items: Product[]; nextCursor?: string }>
}Feature giữ mapping từ giao thức API sang kiểu UI của nó. Đừng để component rải URL, header, retry policy và cách parse lỗi khắp nơi.
Kiểm tra dependency rule
Đặt lint rule hoặc dependency graph check để chặn:
sharedimportfeatures;- feature A import internal path của feature B;
- module UI gọi trực tiếp module route cấp cao;
- code trình duyệt import build config hoặc secret tooling;
- vòng phụ thuộc giữa các feature.
Khi codebase nhỏ, import discipline và vài module boundary đủ dùng. Thêm Nx/Turborepo/dep graph khi nhiều package hoặc team thật sự cần cache, affected builds hay enforce ownership — không thêm công cụ chỉ vì repo có vẻ lớn.
4. Mở rộng ứng dụng: monolith, package hay Micro Frontend?
Micro Frontend là một cách phân phối và sở hữu UI, không phải cấp độ trưởng thành bắt buộc sau React. Bắt đầu bằng modular frontend trong một SPA thường có ít chi phí nhất.
| Lựa chọn | Khi hợp | Trade-off chính |
|---|---|---|
| Modular monolith | Một team hoặc vài team phối hợp được; deploy chung vẫn đáp ứng nhu cầu | Build/release chung, cần giữ module boundary bằng code review/lint |
| Shared package trong monorepo | Muốn chia component/util, version code cùng release của app | Consumer thường vẫn build/deploy lại; chưa phải runtime independent deployment |
| Route-level applications + reverse proxy/server composition | Domain/route có owner/deploy riêng, composition ở navigation/server | Cross-app navigation, shared shell, cache/headers và observability cần vận hành |
| Iframe | Cần cách ly mạnh hoặc nhúng sản phẩm bên thứ ba | UX, sizing, focus, auth, communication và accessibility phức tạp |
| Web Components | Muốn trao đổi qua HTML/custom element contract hoặc phục vụ nhiều framework | Styling, event API, lifecycle và SSR/hydration phải thiết kế rõ |
| Runtime Module Federation | Muốn host nạp code từ build khác khi chạy, remote release độc lập | Runtime dependency graph, version drift, tải lỗi, security và debugging phức tạp |
Dấu hiệu đáng xem xét Micro Frontend
- Nhiều team sở hữu domain/luồng người dùng riêng và thường bị nghẽn do phải release chung.
- Các ranh giới nghiệp vụ và UX đủ rõ để một team có thể chịu trách nhiệm từ UI đến API contract.
- Có nhu cầu nâng cấp dần hệ thống cũ hoặc công nghệ theo từng domain, không thể rewrite toàn bộ.
- Đã có pipeline, monitoring, rollback, versioned contract và người chịu trách nhiệm vận hành remote.
Dấu hiệu chưa nên chia
- “Code nhiều file” nhưng một team vẫn làm việc hiệu quả trong repo hiện tại.
- Các module cùng sửa navigation/global state/design system mỗi sprint.
- Các team chỉ tách theo loại kỹ thuật (team CSS, team forms, team routing) thay vì domain.
- Chưa có quyền owner, compatibility test, observability hoặc rollback.
- Tách từng component nhỏ thành remote nhưng host phải chờ hàng chục request/remote mới render được trang.
MFE có thể làm team độc lập hơn nhưng không làm nghiệp vụ tự nhiên ít phụ thuộc hơn. Nếu hai remote cần đồng bộ deploy mỗi lần đổi contract, bạn có thể đã tạo distributed monolith cùng thêm network latency.
5. Design System — nền tảng, component, governance
Design System là hệ thống giúp đội ngũ tạo giao diện thống nhất và tiếp tục thay đổi an toàn. Nó thường gồm design tokens, component primitives, patterns, accessibility rules, tài liệu, ownership và cách phát hành. Một thư viện button đơn lẻ chưa phải Design System.
Lớp cấu thành
- Design foundations: màu, typography, spacing, radius, elevation, motion, breakpoints, focus ring.
- Semantic tokens: ý nghĩa dùng ở UI (
text.primary,surface.canvas,action.primary) độc lập khỏi palette thô. - Primitives: Button, TextField, Checkbox, Dialog, Tooltip, Select, Table.
- Patterns: form validation, empty state, pagination, confirm destructive action, filter bar.
- Guidance: dùng ở đâu, accessibility behavior, responsive states, do/don't, migration.
- Governance: owner, contribution proposal, review, deprecation window, release notes và support.
Primitive tokens và semantic tokens
Primitive token mô tả giá trị palette/scale; semantic token diễn tả vai trò. Component nên dùng semantic token để đổi theme/brand mà không thay từng component.
Ví dụ định dạng JSON của Design Tokens Community Group (đây là format của một W3C Community Group, không phải tiêu chuẩn W3C Recommendation):
{
"color": {
"blue": {
"600": { "$type": "color", "$value": "#174ea6" }
},
"action": {
"primary": {
"background": { "$type": "color", "$value": "{color.blue.600}" },
"foreground": { "$type": "color", "$value": "#ffffff" }
}
}
},
"space": {
"4": { "$type": "dimension", "$value": "1rem" }
}
}Có thể build ra CSS custom properties:
:root {
--color-action-primary-background: #174ea6;
--color-action-primary-foreground: #fff;
--space-4: 1rem;
}
[data-theme="dark"] {
--color-action-primary-background: #a8c7fa;
--color-action-primary-foreground: #10213b;
}Token names nên biểu đạt ý nghĩa, không khóa vào giá trị hiện tại. text-danger là semantic; red-600 là palette. Đừng tạo hàng trăm token trước khi có ví dụ UI và cách duy trì chúng.
Component API là contract
Một component hệ thống cần có:
- API hẹp, có kiểu và không nhận mọi prop implementation của DOM một cách vô kiểm soát;
- trạng thái mặc định, loading, disabled, error, focus, hover, pressed, empty rõ;
- keyboard behavior và semantic HTML đúng;
- nội dung tùy chỉnh qua slot/children thay vì yêu cầu fork component;
- ví dụ và story cho các trạng thái quan trọng;
- policy về breaking changes, deprecation và phiên bản.
Ví dụ API:
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'aria-busy'>
export function Button({
variant = 'primary',
size = 'md',
loading = false,
disabled,
children,
...buttonProps
}: ButtonProps) {
return (
<button
{...buttonProps}
className={`button button--${variant} button--${size}`}
disabled={disabled || loading}
aria-busy={loading || undefined}
>
{children}
</button>
)
}loading không thay cho accessible label. Với action async, cần quyết định focus, thông báo kết quả qua live region và chống submit lặp. Với destructive action, “danger” chỉ là visual variant; nghiệp vụ xác nhận vẫn nằm ở consumer/pattern.
Publish và vận hành Design System
- Có package source of truth, changelog, owner và release cadence.
- Dùng semver nếu consumer dựa vào version; breaking prop/behavior cần migration guide.
- Chạy typecheck, component test, visual review và accessibility audit trên các trạng thái đại diện.
- Tạo story/docs cho component mới; tài liệu cho người dùng component, không chỉ nội bộ tác giả.
- Có quy trình contribution: request → RFC ngắn → prototype/story → review design + engineering + accessibility → release.
- Theo dõi adoption và deprecated API; không để một component cũ vĩnh viễn vì không có chủ dọn.
- Đừng buộc mọi sản phẩm dùng chung mọi pattern nếu khác nhau thật; standardize primitive/contract, cho phép domain composition.
Storybook có thể giúp xem, tài liệu hóa và kiểm tra các trạng thái component; chọn công cụ tương ứng với stack hiện tại, không để docs chạy lệch khỏi production component.
6. Micro Frontend: ownership, composition, contract
Micro Frontend là cách ghép nhiều frontend app có thể phát triển/phát hành độc lập thành một trải nghiệm thống nhất. Ranh giới tốt thường là vertical slice của domain như Catalog, Billing, Account; tránh chia ngang theo Button, CSS hay form.
Application shell sở hữu phần giao nhau
Shell thường giữ:
- URL/navigation và route table toàn ứng dụng;
- global chrome như header/footer và layout;
- đăng nhập/đăng xuất, lấy identity và cách truyền session an toàn;
- design-system entrypoint và theme;
- error/loading boundary, telemetry, feature flag và fallback khi remote hỏng.
Remote sở hữu:
- route/feature/domain UI của mình;
- query, local state và adapter tới API domain;
- tests, pipeline, artifact/version và on-call/owner;
- semantic events cần thông báo cho shell hoặc feature khác.
Shell không nên đọc internal store của remote; remote không nên sửa shell navigation bằng global variable. Trao đổi qua contract có version, ví dụ route, typed props ở ranh giới, custom event hoặc API server.
Ví dụ event contract:
type CartUpdatedV1 = {
type: 'cart.updated.v1'
detail: { itemCount: number }
}
window.dispatchEvent(new CustomEvent<CartUpdatedV1['detail']>(
'cart.updated.v1',
{ detail: { itemCount: 3 } },
))Trong app thật, bọc event bằng adapter có schema/runtime validation và contract test. Tên event tự do không thay thế versioning hay documentation.
Authentication và dữ liệu
- Shell xác thực user; remote chỉ nhận thông tin cần thiết hoặc gọi API theo cơ chế được thống nhất.
- Không phát tán access token qua
window.__GLOBAL_AUTH__nếu có lựa chọn an toàn hơn như cùng-origin secure session/API gateway. - Backend vẫn phải authorize mọi request; frontend remote không phải security boundary.
- Xác định CORS, cookie
SameSite, credential behavior, CSP, logout/revocation và API error contract. - Không giữ hai bản global session state có thể lệch nhau; chọn owner và cách refresh.
Navigation và CSS isolation
Chốt một bên sở hữu history/router, deep link, 404, browser back/forward và route collision. CSS nên có scope/prefix hoặc Shadow DOM nơi phù hợp; tránh global button, h1 selector đè app khác. Reset, font loading, z-index/modal layer và focus management cần contract chung.
Failure containment
- Nếu remote load lỗi: fallback có thông báo và nút retry; shell/navigation phần khác vẫn dùng được.
- Timeout remote API/load không được treo toàn app; lazy-load route khi người dùng đến đó.
- Theo dõi remote version, host version, browser, route, request ID và load error.
- Có kill switch/roll back URL về artifact trước; deploy remote độc lập không có nghĩa là không cần tương thích ngược.
7. Module Federation — dynamic module sharing
Module Federation là một cách để nhiều build độc lập expose và consume module ở runtime. Trong Webpack, mỗi build vừa có thể đóng vai host/container vừa đóng vai remote/container. Đây là một kỹ thuật triển khai Micro Frontend, không phải định nghĩa của Micro Frontend; MFE cũng có thể dùng server-side composition hoặc route-level deploy mà không dùng Federation.
Thuật ngữ
- Host: app khởi tạo trang, router/shell và nạp remote.
- Remote: build expose module (route/page/widget) cho host.
- Remote entry: metadata/runtime entry dùng để tải exposed module và chunk liên quan.
exposes: module remote cho consumer dùng.remotes: container mà host biết cách tải.shared: dependency có thể dùng chung trong share scope thay vì mỗi build đóng gói riêng.
Ví dụ Webpack Module Federation, rút gọn. Cấu hình thực tế khác nhau giữa Webpack, Rspack, Rsbuild, Vite plugin và framework; dùng integration đúng bundler/version của repo.
Remote catalog:
new ModuleFederationPlugin({
name: 'catalog',
filename: 'remoteEntry.js',
exposes: {
'./Routes': './src/routes/CatalogRoutes',
},
shared: {
react: { singleton: true, requiredVersion: dependencies.react },
'react-dom': { singleton: true, requiredVersion: dependencies['react-dom'] },
},
})Host shell:
new ModuleFederationPlugin({
name: 'shell',
remotes: {
catalog: 'catalog@https://cdn.example.com/catalog/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: dependencies.react },
'react-dom': { singleton: true, requiredVersion: dependencies['react-dom'] },
},
})Host dùng route-level lazy load:
const CatalogRoutes = React.lazy(() =>
import('catalog/Routes').then(({ CatalogRoutes }) => ({ default: CatalogRoutes })),
)
function CatalogRoute() {
return (
<RemoteErrorBoundary fallback={<CatalogUnavailable />}>
<React.Suspense fallback={<PageSkeleton />}>
<CatalogRoutes />
</React.Suspense>
</RemoteErrorBoundary>
)
}Webpack TypeScript consumer thường cần declaration cho module remote hoặc types plugin/manifest:
declare module 'catalog/Routes' {
import type { ComponentType } from 'react'
export const CatalogRoutes: ComponentType
}Shared dependency là một policy, không phải “cài một lần miễn phí”
singleton: true hợp với thư viện cần một instance/runtime như React context; nhưng khi host và remote mong major version khác nhau, có thể gây runtime error hoặc fallback ngoài dự kiến. Shared dependency giảm duplication khi cùng version tương thích, nhưng thêm version negotiation và coupling release. Chia sẻ càng nhiều, ranh giới dependency càng chặt.
Chỉ share thứ đã được đo và cần singleton/giảm transfer. Để dependency khác bundled nếu rủi ro sharing lớn hơn bytes tiết kiệm. Ghi rõ:
- ai cung cấp version chuẩn;
requiredVersion/ fallback behavior;- singleton có bắt buộc không;
- quy trình nâng React/design-system;
- compatibility matrix host × remote;
- remote có được deploy mà không release host không.
Artifact, cache và rollback
- Tên runtime build phải duy nhất (
output.uniqueNametrong Webpack); tránh collision giữa host/remotes. - Phát hành manifest/remote entry theo version bất biến hoặc có strategy cập nhật minh bạch.
- Upload chunk assets trước khi trỏ remote entry tới version mới để tránh manifest trỏ file chưa tồn tại.
- Giữ artifact trước đó và rollback bằng config/manifest thay vì rebuild lại “phiên bản gần giống”.
- Host phải xử lý timeout/404/CSP/CORS/runtime mismatch và remote load ở browser cũ.
- Không để remote URL từ user-controlled input; chỉ nạp artifact từ origin được allowlist, áp CSP phù hợp và review supply chain.
- Đưa việc tải remote vào performance budget: remote entry, chunk count, shared fallback, duplicate library, cache hit và route first render.
Rủi ro dễ gặp
| Triệu chứng | Nguyên nhân hay gặp | Hướng điều tra |
|---|---|---|
remoteEntry.js 404 | URL/environment/publicPath sai hoặc deploy chưa upload entry | network tab, remote URL và CDN cache |
Shared module is not available for eager consumption | Entry thực thi trước async sharing boundary | federation bootstrap theo bundler/version; không bật eager đại trà |
| React invalid hook call / context không thấy | Có hai React instance hoặc version scope mismatch | bundle/share scope, dependency tree, singleton + requiredVersion |
| Host chạy local, production lỗi chunk | public path/CDN prefix/runtime artifact thiếu | kiểm tra chunk URL, deploy ordering, cache headers |
| Deploy remote làm shell vỡ | Contract không tương thích hoặc không có fallback | host-remote contract matrix, backward compatibility, rollback |
Tên lỗi và chi tiết config phụ thuộc phiên bản plugin. Tra troubleshooting của bundler/plugin đang dùng, không copy config từ một tutorial cũ vào mọi stack.
8. Lộ trình chuyển đổi thực tế
Đừng bắt đầu bằng việc tách một app đang chạy thành năm repository. Chuyển từng bước để luôn có đường quay lại.
Giai đoạn A — làm monolith modular
- Gắn ownership rõ cho route/features.
- Di chuyển logic vào feature modules với public API và dependency check.
- Đo build time, conflict rate, cycle count, release cadence, change failure và phần trùng code.
- Tách Design System package trước nếu nhiều app/team thực sự dùng chung UI.
Giai đoạn B — xác nhận nguyên nhân cần tách
Viết decision record trả lời: vấn đề hiện tại là build chậm, release coordination, ownership, legacy stack hay runtime UX? Micro Frontend chỉ giải quyết một số nguyên nhân. Nếu vấn đề là import boundary thì ESLint/monorepo modules rẻ hơn runtime federation.
Giai đoạn C — tách một domain ít blast radius
- Chọn một route/vertical slice có contract backend độc lập và metrics rõ.
- Định nghĩa route, props/events, auth, design tokens, API/error behavior và loading/error fallback.
- Dựng remote song song nhưng route mặc định còn phục vụ implementation cũ.
- Chạy compatibility tests trên host hiện tại, remote mới và tổ hợp version cần hỗ trợ.
- Canary một phần traffic, theo dõi page error, route completion, bundle/latency và rollback về app cũ nếu vượt ngưỡng.
- Sau vài lần release độc lập an toàn mới đánh giá có tách remote kế tiếp không.
9. Lab — storefront có shell, design system và một remote
Phạm vi sản phẩm
Một storefront có ba route: /catalog, /cart, /account. Bắt đầu trong một SPA modular. Sau khi boundary và owner rõ, chỉ tách catalog thành remote. Shell tiếp tục quản lý navigation/session; cart/account ở local để có comparison và fallback thật.
Việc cần làm
- Architecture baseline: vẽ dependency graph; ghi route owner, data owner và public API cho ba feature. Thêm lint test chặn import cross-feature nội bộ.
- Design System: tạo token primitive + semantic; Button, TextField, Dialog, PageSkeleton, EmptyState. Ghi variant, keyboard behavior, disabled/loading/error và story cho ít nhất ba trạng thái mỗi component.
- Accessibility: tab/shift-tab, focus visible, Dialog Escape/restore focus, accessible name, color contrast, zoom 200%. Không coi axe check là thay thế manual keyboard/screen-reader pass.
- Federation: deploy
catalogremote, expose route module, host nạp lazy. GắnRemoteErrorBoundary, skeleton và retry. Không expose internal API rộng hơn cần thiết. - Contract: viết typed contract cho route/events; test trường hợp remote mới với host trước đó và host mới với remote đang production.
- Release: artifact immutable, health/availability check, staged rollout, rollback về remote cũ; mô phỏng remote URL 404 và React version conflict.
- Measure: trước/sau đo app shell bytes, catalog route bytes, remote entry/chunk requests, route load/error và time-to-interactive.
Definition of done
- Có sơ đồ module/dependency và decision record giải thích vì sao boundary theo domain này hợp lý.
- Feature public API không lộ internal components/state; dependency rule được kiểm tra tự động.
- Design tokens có semantic aliases; component docs mô tả states, a11y và ví dụ.
- Host vẫn điều hướng được nếu remote lỗi; remote có owner, version, pipeline và rollback artifact.
- Nêu rõ chi phí federation: thêm network hop/chunks/runtime contracts, shared dependency risk và ops.
- Có bằng chứng remote tải độc lập thật; nếu phải deploy host mỗi khi đổi remote thì chưa đạt mục tiêu independent deployment.
