Chọn pattern từ pressure thật
Trả lời theo problem → forces → pattern/option → consequence → test/evidence. Không nói “dùng pattern X vì code clean” mà thiếu context.
details/summary gốc của trình duyệt, không cần JavaScript.Smell → options
| Smell/pressure | Pattern có thể xét | Câu hỏi trước khi dùng |
|---|---|---|
| Switch theo loại tăng liên tục | Strategy, Factory, polymorphism | Variation có cùng contract và cần runtime selection? |
| Constructor nhiều optional arguments | Builder, parameter object, static factory | Object có invariant hay model đang sai? |
| Vendor API rò vào domain | Adapter / anti-corruption layer | Cần translate semantics/error/timeout nào? |
| Logging/retry/cache lặp quanh client | Decorator, Proxy, AOP | Ordering và business visibility cần explicit? |
| Lifecycle switch khổng lồ | State, state machine | Transition có concurrency/durability/audit? |
| Nhiều handlers tuần tự | Chain of Responsibility | Short-circuit, order và aggregation? |
| Workflow nhiều services | Saga + Outbox/Inbox | Local boundary, compensation và unknown outcome? |
Câu hỏi GoF và Java
1. Design Pattern là gì?
Góc nhìn áp dụng: Nêu một pressure cụ thể trước khi nêu pattern: thay thuật toán phí, thay vendor hay bảo vệ transaction boundary. Nếu chưa có variation, một hàm hoặc conditional tập trung có thể đủ.
Evidence gợi ý: So một thay đổi trước/sau refactor: số nơi phải sửa, mức coupling và các regression tests. Không dùng số class hay độ dài tên pattern làm thước đo.
2. Singleton khác Spring singleton bean?
Làm rõ scope: Tài liệu Spring diễn đạt chính xác là per-container, per-bean, không phải “một object cho cả class trên mọi nơi”. [7]
Evidence gợi ý: Tạo hai bean definitions cùng class và hai contexts; so identity theo từng phạm vi. Sau đó kiểm tra mutable shared state riêng, không suy ra thread-safety từ singleton.
3. Factory và Dependency Injection liên quan thế nào?
Trade-off: Container wiring có thể lo dependency tĩnh, còn runtime input có thể cần factory/registry riêng. Không để domain phải tra bean name hay tự tìm dependency trong ApplicationContext.
Evidence gợi ý: Unit test consumer bằng dependency được truyền vào và test registry cho supported, missing, duplicate keys.
4. Adapter, Decorator và Proxy đều wrap object; phân biệt?
Góc nhìn áp dụng: Xem contract nhìn từ caller: đổi ngôn ngữ vendor sang domain là Adapter; thêm metrics quanh cùng port là Decorator; kiểm soát quyền hoặc defer access là Proxy. Cùng cú pháp wrap chưa đủ kết luận.
Evidence gợi ý: Viết một contract test nêu input/output/error trước và sau wrapper, rồi chỉ ra behavior nào được thêm hay chuyển đổi.
5. Strategy tốt hơn switch lúc nào?
Trade-off: Giữ switch nếu tập trường hợp nhỏ, tập trung và có thể đọc hết. Chọn Strategy khi các biến thể thật sự độc lập và cùng contract. Với Java 21, pattern switch có thể kiểm tra exhaustiveness trên sealed hierarchy.
Evidence gợi ý: Thử thêm một variant/algorithm và kiểm tra compile errors cùng contract tests; đừng giả định exhaustiveness thay validation cho null hoặc input nghiệp vụ. [11]
6. Observer có gây memory leak?
Failure window: Subscriber hết vòng đời nhưng publisher vẫn giữ registration là tình huống cần kiểm tra. Test thêm listener ném exception hoặc publish lồng nhau để phát hiện order/reentrancy không được thiết kế.
Evidence gợi ý: Lặp register/unregister nhiều lần rồi kiểm tra số listener còn đăng ký; kiểm tra một event không bị xử lý nhiều lần do đăng ký lặp. Không kết luận leak chỉ từ một lần GC.
7. Template Method có nhược điểm gì?
Trade-off: Một skeleton ổn định giúp framework kiểm soát lifecycle, nhưng thêm protected hook cho từng ngoại lệ thường làm subclass phụ thuộc nội bộ nhiều hơn.
Evidence gợi ý: Test thứ tự validate/perform/audit và thử thêm một bước chỉ một use case cần. So với inject collaborator; xem chú thích type trong sketch nguồn.
8. Visitor hay pattern matching?
Trade-off: Lập ma trận variants × operations: thêm cột thường xuyên gợi ý Visitor; thêm hàng thường xuyên khiến visitor implementations phải đổi. Pattern matching cũng cần cập nhật các switch bị ảnh hưởng.
Evidence gợi ý: Thực hiện một thay đổi thuộc mỗi chiều và ghi lại số nơi sửa, compiler feedback và test readability, thay vì chọn chỉ vì cú pháp ngắn.
Câu hỏi system design và Spring
9. Thiết kế nhiều payment providers?
Failure window: Provider đã charge nhưng caller timeout: không tự động chuyển sang provider khác để thử charge lần nữa. Cần biết kết quả cũ qua idempotency contract hoặc reconciliation trước khi tạo effect mới.
Evidence gợi ý: Fake provider lưu ledger rồi làm mất response; chạy lại command và đối chiếu một logical payment với ledger. Đây là mục tiêu bài lab, không phải guarantee do Adapter tự tạo.
10. Có nên dùng AOP cho business audit?
Trade-off: Tách telemetry “best effort” khỏi audit phải tồn tại cùng business transition. Thiết kế ai ghi audit, lúc nào ghi và failure của audit có làm use case fail không; không chỉ kiểm tra annotation có mặt.
Evidence gợi ý: Inject audit failure và process crash quanh commit; đọc cả business record lẫn audit/outbox record. Kiểm tra dữ liệu nhạy cảm được loại khỏi log.
11. Order lifecycle dùng State hay enum?
Failure window: Hai workers đều đọc cùng state/version rồi cùng quyết định transition. State object chỉ quyết định legal edge; row-version check và protocol cho external effects là lớp bảo vệ khác.
Evidence gợi ý: Dùng barrier để hai workers đọc cùng version. Xác nhận một conditional write thắng, command lặp không tạo effect mới và worker thua không coi conflict là thành công.
12. Retry nên là Decorator?
Trade-off: Retry có thể là wrapper nhưng caller vẫn phải biết outcome và budget. Phân biệt permanent error, transient error và unknown; không đổi unknown thành “an toàn thử lại”.
Evidence gợi ý: Dùng fake clock hoặc stub để kiểm attempt cap, deadline tổng, exception classification và idempotency key giữ nguyên. Test ordering ở Practice 1 chỉ minh họa wrapper, không triển khai full retry policy.
13. Repository chỉ là interface extends JpaRepository?
Góc nhìn áp dụng: Đặt các thao tác theo ngôn ngữ aggregate/use case và giới hạn quyền thay state. Không cần một lớp wrapper CRUD vô nghĩa chỉ để đặt tên Repository.
Evidence gợi ý: Có integration test với database cho transaction, constraint và query behavior; có dependency rule để domain không bị buộc dùng infrastructure API không cần thiết.
14. Saga là GoF pattern?
Phân biệt phạm vi: State/Command có thể mô tả object bên trong một participant; Saga mô tả workflow giữa participants. Outbox/Inbox xử lý một phần giao tiếp và duplicate, không biến toàn workflow thành một transaction.
Evidence gợi ý: Mô phỏng compensation thất bại, event lặp và reply tới muộn; workflow phải có trạng thái quan sát được, owner và hành động recovery rõ ràng.
15. Tránh over-engineering thế nào?
Quyết định thiết kế: Một abstraction cần giải thích được variation hoặc boundary nó bảo vệ. Ghi cả phương án “chưa refactor”, chi phí migration và điều kiện xóa abstraction.
Evidence gợi ý: Dùng một thay đổi kế tiếp có thật để đánh giá; giữ regression tests và rollback path. Không bịa benchmark để chứng minh một pattern luôn nhanh hơn.
Practice 1 · Refactor checkout
Controller ban đầu có 300 dòng: switch vendor, calculate fee, fraud, retry, save order và notification.
- Vẽ responsibilities và transaction/external boundaries trước khi tạo class.
- Tách fee bằng Strategy; vendor bằng Port + Adapter; selection bằng Registry/Factory.
- Đặt metrics/resilience bằng explicit Decorator và test ordering.
- Giữ orchestration explicit; không giấu business transition trong AOP.
- Thêm dependency rule để domain không import vendor/Spring web package.
Assertions: thêm provider không sửa checkout core; cùng contract tests chạy cho mọi adapter; lost response không double-charge.
Mục tiêu và setup thất bại trước
Giữ lại checkout behavior bằng regression tests trước khi tách class. Dùng fake provider có ledger theo operation/idempotency key, cho phép nhận lệnh, tạo charge rồi làm mất response; ghi riêng số request attempts và số charge thực tế. Khai báo giả định provider có hỗ trợ idempotency hoặc tra cứu kết quả theo reference.
| Test | Failure injection | Acceptance / evidence cần nộp |
|---|---|---|
| P1.1 · Selection | Trùng registration hoặc bỏ thiếu provider bắt buộc. | Composition thất bại có lý do rõ trước khi nhận traffic; checkout core không phải sửa khi thêm provider. |
| P1.2 · Lost response | Provider ghi ledger xong nhưng response không tới caller. | Retry giữ key hoặc reconcile theo reference; ledger thể hiện một logical payment không thành hai charges trong contract đã giả định. |
| P1.3 · Ordering | Lần đầu lỗi tạm thời, lần sau thành công. | Trace chứng minh thứ tự wrapper, logical-call count và attempt count; test thêm permanent error và deadline cạn trong implementation thật. |
| P1.4 · Boundary | Adapter trả lỗi vendor; business transition hoặc audit thất bại. | Contract suite chạy cho mọi adapter; domain không import vendor/Spring web; orchestration và transaction boundary còn nhìn thấy được. |
Kiểm tra thứ tự Decorator bằng Java thuần
Đây là test nhỏ hoàn chỉnh để kiểm tra cách lồng wrapper. breakerProbe chỉ ghi trace, không có trạng thái open/half-open; retryTwice không phải retry policy production. Chương trình không thực hiện thanh toán và không chứng minh idempotency.
import java.util.ArrayList;
import java.util.List;
public class DecoratorOrderCheck {
@FunctionalInterface
interface Call {
String run();
}
static class TemporaryFailure extends RuntimeException {}
static Call metrics(Call next, List<String> trace) {
return () -> {
trace.add("metrics:enter");
try {
return next.run();
} finally {
trace.add("metrics:exit");
}
};
}
// A tracing probe, NOT a real circuit breaker state machine.
static Call breakerProbe(Call next, List<String> trace) {
return () -> {
trace.add("breaker:enter");
try {
return next.run();
} finally {
trace.add("breaker:exit");
}
};
}
// Test-only retry: no delay, jitter, deadline or payment effects.
static Call retryTwice(Call next, List<String> trace) {
return () -> {
for (int attempt = 1; attempt <= 2; attempt++) {
trace.add("retry:" + attempt);
try {
return next.run();
} catch (TemporaryFailure failure) {
if (attempt == 2) {
throw failure;
}
}
}
throw new AssertionError("unreachable");
};
}
static void check(boolean retryOutside, List<String> expected) {
List<String> trace = new ArrayList<>();
int[] attempts = {0};
Call vendor = () -> {
trace.add("vendor:" + ++attempts[0]);
if (attempts[0] == 1) {
throw new TemporaryFailure();
}
return "ok";
};
Call inner = retryOutside
? retryTwice(breakerProbe(vendor, trace), trace)
: breakerProbe(retryTwice(vendor, trace), trace);
String result = metrics(inner, trace).run();
if (!"ok".equals(result) || !expected.equals(trace)) {
throw new AssertionError(trace);
}
System.out.println(
(retryOutside ? "retry-outside-breaker" : "breaker-outside-retry")
+ ": PASS");
}
public static void main(String[] args) {
check(true, List.of(
"metrics:enter", "retry:1", "breaker:enter",
"vendor:1", "breaker:exit", "retry:2", "breaker:enter",
"vendor:2", "breaker:exit", "metrics:exit"));
check(false, List.of(
"metrics:enter", "breaker:enter", "retry:1",
"vendor:1", "retry:2", "vendor:2",
"breaker:exit", "metrics:exit"));
}
}
javac --release 21 DecoratorOrderCheck.java
java DecoratorOrderCheck
Kết quả kỳ vọng: hai dòng retry-outside-breaker: PASS và breaker-outside-retry: PASS. Trong trace đầu, probe được vào hai lần; trong trace sau, một lần. Thêm state machine breaker thật và deadline tests ở bài làm của bạn.
Deliverables: responsibility/boundary map; diff trước/sau; contract-test report cho từng adapter; dependency-rule result; hai ordering traces và provider ledger khi mất response. Log “đã retry thành công” không thay bằng chứng không double-charge.
Practice 2 · Order state machine
- Model
PENDING → RESERVED → PAYMENT_AUTHORIZED → CONFIRMEDvà compensation states. - Implement bằng enum + transition table, sau đó bằng State objects.
- So code size, discoverability, illegal-transition handling và testability.
- Inject hai commands đồng thời; thêm optimistic version/conditional update và idempotency.
Evidence: transition matrix, tests mọi legal/illegal edge và proof chỉ một concurrent transition thắng.
Mục tiêu, setup và phạm vi
Giữ nguyên chuỗi nguồn PENDING → RESERVED → PAYMENT_AUTHORIZED → CONFIRMED. Tự định nghĩa compensation states và legal edges theo nghiệp vụ; nguồn không cung cấp tên hay ma trận compensation đầy đủ. So sánh hai implementation enum/transition table và State objects trên cùng test suite.
Setup: tạo một order ở version 0; dùng hai connections/workers và barrier để cả hai đọc cùng version trước khi update. Kiểm thử trên database/isolation level của dự án, không chỉ dùng mock repository.
CREATE TABLE pattern_order (
id VARCHAR(64) PRIMARY KEY,
state VARCHAR(32) NOT NULL,
version BIGINT NOT NULL
);
INSERT INTO pattern_order (id, state, version)
VALUES ('order-1', 'PENDING', 0);
-- Both workers must read version 0 before either worker writes.
SELECT state, version FROM pattern_order WHERE id = 'order-1';
-- Both workers issue this statement with the SAME expected version.
UPDATE pattern_order
SET state = 'RESERVED', version = version + 1
WHERE id = 'order-1'
AND state = 'PENDING'
AND version = 0;
-- Inspect the affected-row count using JDBC executeUpdate().
-- One successful committed write: 1 row; the stale write: 0 rows.
Expected observation: với hai lần update dùng cùng expected version, không có writer khác và transaction thắng commit thành công, một write cập nhật 1 row; write stale không được ghi đè. Database/isolation level có thể trả conflict/serialization failure thay vì 0 rows; implementation phải xử lý theo contract và chỉ coi thành công sau commit.
Giới hạn: predicate chỉ bảo vệ local state. Hai workers gọi payment trước khi claim/update vẫn có thể tạo hai external effects. Thêm idempotency protocol, trạng thái đang xử lý/unknown và reconciliation theo yêu cầu bài toán; không giữ một DB transaction mở qua network call chỉ để che vấn đề.
Acceptance gate
- Mọi legal edge thành công đúng một lần theo contract; mọi illegal edge bị từ chối và không phát sinh effect.
- Hai workers đọc cùng version: chỉ một transition commit; worker còn lại ghi nhận stale/conflict, đọc lại và quyết định theo command contract.
- Phát lại cùng command không tạo thêm external effect; command khác cùng key nhưng payload không tương thích phải được phát hiện.
- Crash sau provider effect nhưng trước local confirmation có recovery; compensation thất bại có trạng thái, retry/owner và ledger kiểm chứng.
Deliverables: transition matrix; hai implementation; báo cáo so code size/discoverability/testability; legal/illegal-edge tests; concurrent write report với version trước/sau; idempotency và compensation/reconciliation evidence. SQL trên là điểm bắt đầu, chưa phải toàn bộ order state machine.
Practice 3 · Pattern review
Chọn module thật và lập bảng: problem, rate of change, coupling, candidate pattern, simpler alternative, migration, failure modes và rollback. Chỉ refactor khi tests chứng minh behavior giữ nguyên và thay đổi kế tiếp thật sự đơn giản hơn.
Mục tiêu và cách làm
Chọn một module có thay đổi hoặc incident thật. Ghi lại behavior hiện có, tests bảo vệ và một thay đổi kế tiếp có thể kiểm chứng. So phương án không refactor, refactor nhỏ và pattern candidate trước khi chọn.
| Trường phải điền | Ví dụ để bắt đầu thảo luận |
|---|---|
| Problem / rate of change | Module fee có ba policy hiện tại; ghi lịch sử thay đổi thật để biết switch có đang phân tán không. |
| Coupling / boundary | Checkout đang biết enum của SDK vendor; domain cần độc lập với model bên ngoài. |
| Candidate / simpler alternative | Strategy + registry so với một switch tập trung; Adapter cho vendor boundary. |
| Migration / behavior preservation | Khóa behavior bằng tests, di chuyển từng nhánh và chạy lại cùng fixtures. |
| Failure modes / rollback | Thiếu registration, mapping sai, response mất; rollback code/config nhưng phải xử lý state/external effects đã tạo. |
| Next-change evidence | Thêm policy thật, ghi nơi phải sửa và test phải bổ sung; không chỉ đếm class. |
Checklist review bổ sung
- Nêu đúng problem, forces và scope; không lẫn GoF với architecture pattern.
- Chỉ rõ contract/invariant, owner của orchestration và transaction/external boundaries.
- So với simpler alternative; ghi indirection, config và operational cost tăng thêm.
- Có regression tests trước/sau, failure injection và evidence của thay đổi kế tiếp.
- Rollback nêu cả code/config lẫn dữ liệu, workflow đang chạy và effect đã phát sinh.
Acceptance / deliverables: decision record có đủ problem, rate of change, coupling, candidate, simpler alternative, migration, failure modes và rollback; diff đủ nhỏ để review; test report và một next-change comparison. Quyết định giữ nguyên thiết kế cũng hợp lệ khi evidence chưa ủng hộ abstraction mới.