Node.js Production Engineering 10 — Kiến trúc có Khả năng Tiến hóa
Thiết kế modular monolith theo bounded context, giữ dependency rule bằng fitness function, ghi ADR và chỉ dùng outbox, saga, CQRS hay microservice khi constraint đòi hỏi.
- #nodejs
- #architecture
- #modular-monolith
- #domain-driven-design
- #distributed-systems
- #production-engineering
Kiến trúc tốt không được đo bằng số box trong diagram. Nó được đo bằng khả năng trả lời những câu hỏi rất thực tế:
- Thêm một phương thức thanh toán có cần sửa năm module không?
- Hai team có thể phát triển hai capability mà không giẫm cùng bảng và cùng file không?
- Một dependency lỗi có làm toàn hệ thống ngừng phục vụ không?
- Ta có thể đổi database, tách service hoặc rollback release mà vẫn giữ contract không?
- Sáu tháng sau, người mới có biết vì sao quyết định này tồn tại không?
Nói ngắn gọn, kiến trúc quản lý chi phí thay đổi dưới constraint. Nó không cố đoán mọi nhu cầu tương lai; nó giữ những lựa chọn quan trọng có thể đảo ngược và biến các ranh giới thành thứ CI quan sát được.
Sau bài này, bạn sẽ làm được gì?
- Chuyển quality attribute thành quyết định và guardrail đo được.
- Xây modular monolith theo bounded context thay vì folder kỹ thuật toàn cục.
- Áp dụng dependency rule để domain không phụ thuộc bcrypt, Express, ORM hay broker.
- Ghi Architectural Decision Record với context, option, consequence và revisit trigger.
- Tạo architecture fitness function để phát hiện coupling, cycle và contract drift trong CI.
- Dùng transactional outbox cho dual-write và saga cho workflow phân tán ở đúng mức.
- Đánh giá CQRS/microservice bằng criteria về ownership, scale, failure và operability.
Bắt đầu từ quality attributes, không từ technology
Functional requirement nói hệ thống làm gì. Quality attribute nói hệ thống phải làm việc đó dưới điều kiện nào.
Ví dụ cho nền tảng commerce:
| Attribute | Scenario có thể kiểm chứng |
|---|---|
| Availability | Payment provider lỗi vẫn cho xem catalog; checkout trả kết quả rõ trong 2 giây |
| Modifiability | Thêm payment adapter không sửa domain hoặc controller |
| Deployability | Ordering và Notifications có thể release với blast radius đã biết |
| Consistency | Một idempotency key không tạo hơn một order/payment intent |
| Performance | List order p95 dưới 180 ms ở 200 req/s |
| Recoverability | Rollback application dưới 10 phút; outbox không mất event đã commit |
| Security | Credential hash adapter thay được; secret không đi vào domain/log |
Không thể tối đa mọi attribute. Strong consistency, availability, latency và chi phí thường kéo theo trade-off. Quyết định kiến trúc phải nói rõ attribute nào được ưu tiên trong scenario nào.
Architecture decision statement
Mỗi quyết định nên hoàn thành câu:
Vì [constraint / quality scenario],
ta chọn [option],
đổi lại chấp nhận [cost / consequence],
và sẽ xem xét lại khi [measurable trigger].
Ví dụ: vì một team 8 người cần giao dịch ACID giữa Ordering và Inventory, ta chọn modular monolith dùng một PostgreSQL nhưng ownership schema rõ; chấp nhận deploy chung; xem xét tách khi hai module cần release độc lập thường xuyên và boundary dữ liệu đã ổn định.
Modular monolith là mặc định có chủ đích
Monolith nghĩa là một deployment unit, không có nghĩa toàn bộ code là một khối không ranh giới. Modular monolith giữ network, deployment và transaction đơn giản, nhưng chia capability bằng API và dependency rule rõ.
src/
├── modules/
│ ├── identity/
│ │ ├── domain/
│ │ ├── application/
│ │ ├── infrastructure/
│ │ └── index.ts
│ ├── catalog/
│ ├── ordering/
│ ├── inventory/
│ └── notifications/
├── platform/
│ ├── database/
│ ├── messaging/
│ ├── observability/
│ └── config/
└── main.ts
Mỗi module có:
- từ vựng và model riêng;
- public API qua
index.ts; - dữ liệu nó sở hữu;
- use case và adapter riêng;
- test contract cho API/event xuất ra;
- owner và telemetry theo module.
Module khác không import repository/table nội bộ. Nếu Ordering cần dữ liệu Catalog, nó gọi public query/port hoặc nhận snapshot cần thiết, không join tùy ý vào bảng Catalog từ mọi nơi.
Vì sao chưa tách microservice ngay?
Trong một process, refactor boundary còn rẻ: rename type, chuyển capability, thay transaction. Qua network, mọi thay đổi cần version contract, rollout nhiều service, timeout, retry, tracing và consistency model. Boundary chưa ổn định bị phân tán sẽ đóng băng sai lầm bằng chi phí vận hành.
Modular monolith cho phép học domain trước. Microservice là một deployment và ownership decision, không phải cách chữa code coupling.
Bounded context: ranh giới của model và ngôn ngữ
Một thuật ngữ có thể mang nghĩa khác nhau:
Customertrong Identity là tài khoản, credential và trạng thái xác minh.Customertrong Ordering là buyer id, địa chỉ giao hàng snapshot và điều kiện mua.Producttrong Catalog là nội dung/giá hiển thị; trong Inventory là SKU và số lượng có thể giữ chỗ.
Ép tất cả vào một model “universal” tạo entity lớn, nhiều field optional và coupling giữa team. Bounded context cho mỗi vùng một model phù hợp với quyết định của nó.
Context map mẫu
Identity ── customer-id ───────▶ Ordering
Catalog ── product snapshot ──▶ Ordering
Ordering ── reservation cmd ───▶ Inventory
Ordering ── order event ───────▶ Notifications
| Upstream | Downstream | Contract | Consistency |
|---|---|---|---|
| Identity | Ordering | CustomerVerified v1 | Eventual; checkout kiểm trạng thái cần thiết |
| Catalog | Ordering | ProductSnapshot query | Sync trong monolith hoặc cached snapshot |
| Ordering | Inventory | reserve(sku, qty, orderId) | Atomic khi chung DB; saga khi tách DB |
| Ordering | Notifications | OrderConfirmed v1 | Async, at-least-once, consumer idempotent |
Không phải mỗi folder là bounded context. Boundary cần khác biệt về model, ownership và vòng đời thay đổi. Dùng workshop/domain event, change history và team ownership để tìm ranh giới; diagram chỉ là kết quả tạm thời.
Dependency rule: domain không biết mechanism
Domain chỉ chứa invariant và state transition. Nó không import bcrypt, ORM, HTTP hoặc logger.
// modules/identity/domain/User.ts
export class User {
private constructor(
readonly id: string,
readonly email: string,
private passwordDigest: string,
private emailVerifiedAt: Date | null
) {}
static register(input: {
id: string;
email: string;
passwordDigest: string;
}) {
const normalizedEmail = input.email.trim().toLowerCase();
if (!normalizedEmail.includes('@')) throw new InvalidEmail();
return new User(input.id, normalizedEmail, input.passwordDigest, null);
}
verifyEmail(at: Date) {
if (this.emailVerifiedAt) return;
this.emailVerifiedAt = at;
}
}
Hashing là mechanism và có port ở application layer:
// modules/identity/application/ports.ts
export interface PasswordHasher {
hash(plainText: string): Promise<string>;
verify(plainText: string, digest: string): Promise<boolean>;
}
export interface UserRepository {
existsByEmail(email: string, tx?: Transaction): Promise<boolean>;
save(user: User, tx?: Transaction): Promise<void>;
}
export interface IdGenerator {
next(): string;
}
Use case điều phối domain và port:
export class RegisterUser {
constructor(
private readonly users: UserRepository,
private readonly hasher: PasswordHasher,
private readonly ids: IdGenerator
) {}
async execute(command: { email: string; password: string }) {
const email = command.email.trim().toLowerCase();
if (await this.users.existsByEmail(email)) throw new EmailAlreadyUsed();
const digest = await this.hasher.hash(command.password);
const user = User.register({
id: this.ids.next(),
email,
passwordDigest: digest,
});
await this.users.save(user);
return { id: user.id, email: user.email };
}
}
Adapter mới được import bcrypt:
import bcrypt from 'bcrypt';
export class BcryptPasswordHasher implements PasswordHasher {
constructor(private readonly cost = 12) {}
hash(plainText: string) {
return bcrypt.hash(plainText, this.cost);
}
verify(plainText: string, digest: string) {
return bcrypt.compare(plainText, digest);
}
}
Khi chuyển sang Argon2 hoặc dịch vụ identity khác, domain không đổi. Nhưng đừng diễn giải dependency inversion thành “mọi function đều cần interface”. Tạo port ở boundary có khả năng thay đổi, cần test control hoặc thuộc ownership khác.
Composition root
const userRepository = new PostgresUserRepository(pool);
const passwordHasher = new BcryptPasswordHasher(config.BCRYPT_COST);
const registerUser = new RegisterUser(
userRepository,
passwordHasher,
uuidGenerator
);
const identityRouter = createIdentityRouter({ registerUser });
app.use('/identity', identityRouter);
Object graph được lắp một lần ở entry point. Domain/application không gọi container; dependency vẫn hiện rõ và static analysis vẫn theo được.
Aggregate và transaction boundary
Aggregate là cụm object có một root giữ invariant nhất quán trong một transaction. Nó không phải cách gom mọi entity “liên quan”.
Ví dụ Order giữ invariant:
- order đã confirmed không được đổi item;
- total bằng tổng line item theo snapshot giá;
- state transition chỉ đi theo đường cho phép;
- version tăng sau mỗi thay đổi để optimistic concurrency.
Ordering có thể tham chiếu customerId, sku thay vì giữ toàn bộ Customer/Product aggregate. Transaction nhỏ hơn, lock ít hơn và boundary rõ hơn.
await unitOfWork.run(async (tx) => {
const order = await orders.findForUpdate(command.orderId, tx);
order.confirm(command.paymentId);
await orders.save(order, tx);
});
Không kéo network call vào transaction. Nếu payment và inventory nằm ngoài database transaction, state machine/workflow phải biểu diễn trạng thái trung gian và recovery.
Public API và anti-corruption layer
Mỗi module chỉ export use case/query/type contract cần thiết:
// modules/catalog/index.ts
export type ProductSnapshot = {
sku: string;
displayName: string;
unitPrice: number;
priceVersion: number;
};
export interface CatalogQueries {
getProductSnapshot(sku: string): Promise<ProductSnapshot | null>;
}
Nếu tích hợp một vendor có model khác domain, đặt anti-corruption layer để dịch:
vendor SDK response ─▶ VendorPaymentAdapter ─▶ PaymentAuthorization
(mapping, error taxonomy, currency rules)
Đừng để enum/error/vendor object lan vào application. Nếu vendor đổi, blast radius dừng ở adapter và contract test.
ADR: lưu lý do, không chỉ kết quả
Diagram cho thấy hệ thống đang thế nào; ADR cho biết vì sao và khi nào cần xem lại.
# ADR-007: Ordering và Inventory dùng chung deployment
Status: Accepted
Date: 2026-07-11
Owners: Commerce Platform
## Context
- Một team sở hữu cả hai capability.
- Reserve inventory phải atomic với order transition hiện tại.
- Traffic profile giống nhau; chưa có nhu cầu scale độc lập.
## Decision drivers
- Consistency và feedback time ưu tiên hơn deploy độc lập.
- Platform chưa có mature tracing/broker operations.
## Options
1. Modular monolith + schema ownership.
2. Hai service + synchronous API.
3. Hai service + saga.
## Decision
Chọn option 1; public module API, cấm cross-module table access.
## Consequences
- Transaction cục bộ và debug đơn giản.
* Release chung; cần test boundary để tránh coupling.
## Fitness functions
- Dependency rule trong CI.
- Không query chéo schema ngoài approved adapter.
- Module ownership và change-coupling metric hàng tháng.
## Revisit when
- Hai team/roadmap độc lập trong ít nhất hai quý.
- > 30% release phải phối hợp chỉ vì deployment chung.
- Inventory có scale/failure profile khác biệt đã đo được.
ADR nên ngắn, một quyết định, được review cùng code. Khi thay quyết định, supersede ADR cũ thay vì sửa lịch sử cho giống hiện tại.
Architecture fitness function: biến nguyên tắc thành test
Quy tắc chỉ viết trong wiki sẽ trôi. Fitness function là check tự động liên tục cho quality/structure quan trọng.
Dependency rule bằng dependency-cruiser
// .dependency-cruiser.cjs
module.exports = {
forbidden: [
{
name: 'domain-cannot-import-infrastructure',
from: { path: '/domain/' },
to: { path: '/infrastructure/|node_modules/(express|pg|redis|bcrypt)' },
},
{
name: 'application-cannot-import-http',
from: { path: '/application/' },
to: { path: 'express|/presentation/' },
},
{
name: 'no-circular-dependencies',
from: {},
to: { circular: true },
},
],
};
npx depcruise --config .dependency-cruiser.cjs src
Thêm ESLint no-restricted-imports để module khác chỉ import modules/<name> public entry, không chui vào internal path.
Các fitness function khác
| Quality | Check liên tục |
|---|---|
| API compatibility | OpenAPI/event schema breaking-change check |
| Database ownership | SQL lint/review không truy cập schema module khác |
| Performance | p95/p99 guardrail trên workload chuẩn |
| Reliability | chaos/recovery smoke, timeout budget test |
| Security | dependency/SBOM policy, secret scan, authz contract |
| Deployability | migration compatibility old/new code |
Fitness function cũng có chi phí và false positive. Chỉ tự động hóa rule liên quan attribute quan trọng; owner phải xử lý được khi gate fail.
Transactional outbox: giải bài toán dual-write
Giả sử use case commit order vào PostgreSQL rồi publish OrderConfirmed sang broker:
DB commit thành công ─▶ process crash ─▶ broker chưa nhận event
Đảo thứ tự cũng sai: event có thể publish trong khi DB rollback. Không có transaction ACID chung giữa database và broker thông thường.
Outbox ghi state và intent phát event trong cùng database transaction:
CREATE TABLE outbox_events (
id uuid PRIMARY KEY,
aggregate_type text NOT NULL,
aggregate_id uuid NOT NULL,
event_type text NOT NULL,
event_version integer NOT NULL,
payload jsonb NOT NULL,
trace_id text,
occurred_at timestamptz NOT NULL,
published_at timestamptz
);
CREATE INDEX outbox_unpublished_idx
ON outbox_events (occurred_at)
WHERE published_at IS NULL;
await unitOfWork.run(async (tx) => {
const order = await orders.findForUpdate(command.orderId, tx);
order.confirm(command.paymentId);
await orders.save(order, tx);
await outbox.append(
{
id: ids.next(),
aggregateType: 'Order',
aggregateId: order.id,
eventType: 'OrderConfirmed',
eventVersion: 1,
payload: { orderId: order.id, customerId: order.customerId },
traceId: context.traceId,
occurredAt: clock.now(),
},
tx
);
});
Relay đọc row chưa publish, gửi broker rồi đánh dấu. Nếu publish thành công nhưng process crash trước published_at, event sẽ được gửi lại. Vì vậy outbox đảm bảo không mất intent nhưng không tạo exactly-once end-to-end.
Consumer cần idempotent:
CREATE TABLE processed_messages (
consumer text NOT NULL,
event_id uuid NOT NULL,
processed_at timestamptz NOT NULL,
PRIMARY KEY (consumer, event_id)
);
Trong cùng transaction consumer, insert processed_messages; conflict nghĩa event đã xử lý. Side effect ngoài database như email vẫn cần idempotency key phía provider hoặc state machine riêng.
Outbox observability
- số row chưa publish;
- tuổi row cũ nhất;
- publish latency/error/retry;
- duplicate consumed;
- poison event theo type/version;
- chênh lệch sequence nếu ordering quan trọng.
Đặt retention/partition cho outbox; một bảng chỉ tăng mãi sẽ thành bottleneck. Với volume lớn, CDC có thể thay polling nhưng vẫn giữ semantics dual-write.
Saga: workflow phân tán, không phải rollback từ xa
Saga chỉ cần khi một business operation đi qua nhiều transaction cục bộ mà không có transaction chung. Nếu Ordering và Inventory còn chung PostgreSQL và cùng ownership, một transaction cục bộ thường đơn giản và đúng hơn.
Khi chúng đã tách dữ liệu:
CreateOrder
↓
AuthorizePayment
↓
ReserveInventory
↓
ConfirmOrder
Nếu reserve thất bại:
VoidPaymentAuthorization → CancelOrder
Compensation là hành động nghiệp vụ mới, không phải tua ngược thời gian. Email đã gửi không “unsend”; shipment đã giao không thể rollback bằng update row. Mỗi bước phải định nghĩa:
- command idempotency key;
- success/failure/timeout event;
- retry budget;
- compensation và trường hợp compensation cũng lỗi;
- deadline toàn workflow;
- trạng thái cần operator can thiệp.
Choreography hay orchestration?
| Kiểu | Lợi ích | Rủi ro |
|---|---|---|
| Choreography | Module phát/nhận event, coupling trực tiếp thấp | Flow ẩn, khó biết ai tiếp theo, dễ cycle |
| Orchestration | State machine và owner workflow rõ, debug/retry tập trung | Orchestrator có thể thành god-service nếu chứa domain của mọi context |
Với workflow tiền/đơn hàng nhiều bước, orchestration thường dễ quan sát hơn. Orchestrator giữ state transition và timeout, nhưng rule nội bộ từng context vẫn ở context đó.
Metric cần có: saga theo state, age, retry, compensation rate, stuck count và completion latency. Cần UI/runbook để inspect và resume/cancel; chỉ có message log là chưa đủ vận hành.
CQRS: tách trách nhiệm trước khi tách hạ tầng
Command và query có lý do thay đổi khác nhau. Ta có thể tách interface trong cùng module mà chưa cần broker hoặc database thứ hai:
interface OrderCommands {
confirm(input: ConfirmOrderCommand): Promise<void>;
}
interface OrderQueries {
getSummary(id: string): Promise<OrderSummary | null>;
listForCustomer(input: ListOrdersQuery): Promise<OrderPage>;
}
Mức tăng dần:
- Handler command/query riêng nhưng cùng model/database.
- Query dùng projection SQL tối ưu, domain write model giữ invariant.
- Read model riêng cập nhật bất đồng bộ khi scale/shape thật sự khác.
- Event sourcing chỉ khi lịch sử sự kiện là source of truth có giá trị cốt lõi.
Read database riêng thêm eventual consistency, replay, schema version, lag và vận hành. CRUD thông thường không cần trả chi phí đó.
Khi nào tách microservice?
Microservice hợp khi boundary module đã rõ và có nhu cầu deployment/runtime độc lập.
Tín hiệu ủng hộ tách
- Capability có owner và roadmap độc lập ổn định.
- Release coordination tạo bottleneck được đo, không chỉ khó chịu cảm tính.
- Scale profile khác biệt đáng kể; ví dụ image processing CPU-heavy so với API I/O.
- Failure isolation có giá trị: Notifications backlog không được ảnh hưởng checkout.
- Data ownership và contract đã rõ, ít query chéo.
- Đội ngũ có platform: CI/CD, discovery, secret, telemetry, on-call, broker/DB operations.
Tín hiệu nên giữ module trong monolith
- Boundary thay đổi hàng tuần.
- Workflow cần nhiều transaction chéo capability.
- Một team nhỏ sở hữu tất cả và luôn release cùng nhau.
- Không có distributed tracing, contract governance hoặc on-call.
- Lý do chính là “repo lớn” hoặc muốn dùng công nghệ mới.
Extraction sequence giảm rủi ro
1. Củng cố public module API + test + ownership
2. Đo call graph, change coupling, latency và data access
3. Cấm caller mới chạm internal table
4. Đặt facade/port để caller không biết local hay remote
5. Tách data ownership theo expand–migrate–contract
6. Route một cohort traffic; so telemetry
7. Xóa đường cũ sau rollback window
Đừng tạo “distributed monolith”: service deploy riêng nhưng phải release theo thứ tự, cùng sửa schema hoặc gọi sync thành chuỗi dài. Khi tách, cần timeout, idempotency, version contract, capacity và failure mode cho network mới.
Phần 18 sẽ đào sâu gRPC, API gateway và lỗi phân tán; ở đây điều quan trọng là criteria tách, không phải framework transport.
Evolutionary architecture và governance
Kiến trúc tiến hóa qua vòng lặp nhỏ:
quality scenario → ADR → change nhỏ → fitness function → telemetry → review
Ownership không đồng nghĩa silo
Mỗi bounded context cần owner quyết định model, contract và SLO. Platform cung cấp paved road cho logging, auth, deployment và policy. Owner không được âm thầm phá global constraint; platform cũng không ép mọi context vào một model chung.
Review theo evidence
Architecture review nên yêu cầu:
- context và quality scenario;
- option cùng consequence;
- impact lên data/contract/security/operation;
- migration và rollback;
- metric xác nhận;
- fitness function và revisit trigger.
Không cần hội đồng phê duyệt mọi class. Tập trung vào quyết định khó đảo ngược hoặc ảnh hưởng nhiều owner.
Đo kiến trúc
- change lead time và change failure rate theo module;
- số PR chạm nhiều bounded context;
- dependency cycle/violation;
- contract breaking change;
- deploy coordination count;
- incident blast radius và time to isolate;
- outbox/saga lag khi có workflow phân tán.
Metric không tự nói nguyên nhân, nhưng cho biết assumption trong ADR có còn đúng không.
Những failure mode kiến trúc thường gặp
Layered monolith giả modular
Folder có module nhưng mọi module dùng chung entity/repository/table và import internal path. Sửa bằng public API, schema ownership và fitness function, không bằng đổi tên folder.
Abstraction không có pressure
Interface cho mọi class, factory cho object chỉ có một cách tạo, event cho lời gọi cần response ngay. Chi phí đọc/debug tăng mà option không mở. Yêu cầu constraint và decision criteria cho abstraction mới.
Domain phụ thuộc framework
Decorator ORM, bcrypt, request object hoặc logger đi vào entity. Domain test phải boot framework và thay adapter khó. Đưa mechanism ra adapter/port như ví dụ PasswordHasher.
Event-driven nhưng không có delivery semantics
Publish sau DB commit làm mất event; consumer không idempotent tạo side effect lặp; event không version làm deploy phá nhau. Dùng outbox, idempotency, schema compatibility và lag metric.
Saga không có operator path
Workflow stuck chỉ tồn tại trong broker log; compensation fail không ai biết. Cần persisted state, deadline, dashboard, alert và runbook resume/cancel.
Microservice theo entity CRUD
user-service, address-service, order-line-service gọi nhau liên tục và không sở hữu capability hoàn chỉnh. Tìm bounded context theo business decision, không theo table.
ADR thành nghĩa địa tài liệu
ADR không có status/owner/revisit trigger và không liên kết code/fitness function. Review ADR khi assumption hoặc metric vượt trigger; supersede thay vì bỏ quên.
Checklist architecture review
- Quality attribute được viết thành scenario và metric.
- Bounded context có model, public API, data owner và team owner rõ.
- Modular monolith là baseline; lý do tách process/deployment được định lượng.
- Domain/application không import framework, ORM, crypto SDK hoặc broker.
- Transaction boundary khớp invariant/aggregate và không giữ lock qua network.
- ADR ghi option, consequence, guardrail và revisit trigger.
- Fitness function chặn cycle, import sai tầng và contract breaking change.
- Dual-write dùng outbox/CDC; consumer idempotent và event có version.
- Saga chỉ dùng cho transaction phân tán; có compensation, deadline và operator path.
- CQRS/read model tăng mức theo nhu cầu, không mặc định tách database.
- Extraction có facade, migration, canary, telemetry và rollback window.
- Architecture metric được review cùng delivery/incident data.
Lab: modular commerce có đường tiến hóa
Xây một modular monolith gồm identity, catalog, ordering, inventory và notifications.
Yêu cầu
- Viết context map, public API và ownership cho từng module.
- Cài
RegisterUservớiPasswordHasherport; domain không import bcrypt. - Cài
Orderaggregate với optimistic version và state transition. - Thêm dependency-cruiser/ESLint rule cho dependency direction và public imports.
- Ghi ít nhất hai ADR: modular monolith và consistency giữa Ordering–Inventory.
- Ghi
OrderConfirmedqua transactional outbox; consumer Notifications idempotent. - Mô hình hóa một saga payment–inventory trên giấy/state machine, nhưng chỉ bật khi hai data store thực sự tách.
- Tạo dashboard theo module, outbox và workflow; định nghĩa extraction trigger.
Acceptance criteria
- CI fail khi domain import
bcrypt,pg,expresshoặc infrastructure. - Module khác không import
ordering/infrastructure/*hay query table nội bộ trực tiếp. - Đổi Bcrypt adapter sang Argon2/fake không sửa entity, use case hoặc HTTP contract.
- User/order write và outbox event commit hoặc rollback cùng nhau.
- Crash relay sau publish tạo duplicate nhưng consumer không gửi hai notification.
- Event v2 thêm field vẫn tương thích consumer v1; breaking change bị gate chặn.
- ADR nêu rõ consequence và metric/revisit trigger, không chỉ ghi lựa chọn.
- Báo cáo chỉ đề xuất tách service khi ít nhất một extraction criterion có evidence.
- Canary extraction có cùng contract, trace liên tục và rollback không mất dữ liệu.
Tài liệu nền tảng
- Microsoft Learn — Tactical DDD và bounded context
- Martin Fowler — Monolith First
- Architectural Decision Records
- dependency-cruiser — Validate dependency rules
- AWS Prescriptive Guidance — Transactional outbox
- AWS Prescriptive Guidance — Saga pattern
- Martin Fowler — Breaking a monolith into microservices
Phần tiếp theo
Kiến trúc chỉ đứng vững khi storage semantics được hiểu đến mức transaction, lock, index và query plan. Phần 11 đi sâu PostgreSQL để các ranh giới transaction, outbox và concurrency trong bài này có nền tảng dữ liệu chính xác và quan sát được.