Part 01 · Java Core · 1.1.05.02 · Scenarios & practice
Design Patterns / Interview scenarios

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.

15 scenarios3 practice7 smell/pressure rowsProblem → consequence → evidence
Cách luyện: trả lời trước khi mở từng câu. Đáp án nguồn được giữ nguyên; phần bổ sung đặt riêng bên dưới để luyện trade-off và test/evidence. Mười lăm câu dùng details/summary gốc của trình duyệt, không cần JavaScript.

Smell → options

Smell/pressurePattern có thể xétCâu hỏi trước khi dùng
Switch theo loại tăng liên tụcStrategy, Factory, polymorphismVariation có cùng contract và cần runtime selection?
Constructor nhiều optional argumentsBuilder, parameter object, static factoryObject có invariant hay model đang sai?
Vendor API rò vào domainAdapter / anti-corruption layerCần translate semantics/error/timeout nào?
Logging/retry/cache lặp quanh clientDecorator, Proxy, AOPOrdering và business visibility cần explicit?
Lifecycle switch khổng lồState, state machineTransition có concurrency/durability/audit?
Nhiều handlers tuần tựChain of ResponsibilityShort-circuit, order và aggregation?
Workflow nhiều servicesSaga + Outbox/InboxLocal boundary, compensation và unknown outcome?

Bảng trên là danh sách options để điều tra pressure, không phải quy tắc “có smell là phải thêm pattern”. Cần nói rõ simpler alternative và boundary trước khi refactor.

Câu hỏi GoF và Java

1. Design Pattern là gì?
Vocabulary/solution structure đã được kiểm nghiệm cho recurring context. Nó giúp trao đổi intent/trade-off nhưng thêm indirection; chỉ dùng khi problem hoặc variation đủ thật.
Bổ sung · Luyện lập luận

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?
GoF Singleton kiểm soát construction/global access. Spring singleton là một bean instance cho mỗi ApplicationContext/bean definition; nhiều context/process vẫn có nhiều instance và scope không bảo đảm thread-safety.
Bổ sung · Luyện lập luận

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?
Factory chủ động tạo/trả product; DI cung cấp dependency từ ngoài và tách consumer khỏi construction. DI container có factory responsibility; business factory vẫn hữu ích khi chọn product theo runtime domain input.
Bổ sung · Luyện lập luận

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?
Adapter đổi interface; Decorator giữ interface và thêm behavior; Proxy giữ interface và kiểm soát access/location/lifecycle. Structure giống không quyết định pattern—intent quyết định.
Bổ sung · Luyện lập luận

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?
Khi algorithms thay đổi độc lập, có cùng contract, cần inject/test/select và switch đang phân tán. Một switch tập trung, exhaustive trên sealed type đôi khi rõ hơn hierarchy.
Bổ sung · Luyện lập luận

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?
Có nếu long-lived publisher giữ references tới subscribers không unsubscribe. Cần lifecycle/registration token và kiểm soát reentrancy, error, order, sync/async.
Bổ sung · Luyện lập luận

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ì?
Subclass phụ thuộc protected hooks/skeleton, dễ fragile base class. Composition/Strategy linh hoạt hơn; Template tốt khi framework thật sự sở hữu stable lifecycle.
Bổ sung · Luyện lập luận

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?
Visitor hợp khi variants ổn định nhưng thường thêm operations. Nếu thường thêm variants, visitor gây sửa mọi visitor; sealed hierarchy + pattern matching có thể ngắn và exhaustive hơn.
Bổ sung · Luyện lập luận

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?
Domain port + Adapter mỗi vendor; Strategy/Factory chọn provider; Decorator cho metrics/resilience. Persist idempotency/provider reference; pattern không thay unknown-outcome reconciliation.
Bổ sung · Luyện lập luận

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?
AOP hợp telemetry/technical audit. Business transition/audit bắt buộc atomic thường nên explicit service/decorator/outbox; nếu dùng AOP phải xét proxy boundary, ordering và sensitive data.
Bổ sung · Luyện lập luận

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?
Enum + transition table đủ khi behavior nhỏ. State objects hợp khi behavior/transition phức tạp. DB conditional update/version và idempotency mới bảo vệ concurrent transitions.
Bổ sung · Luyện lập luận

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?
Có thể nếu contract phân biệt retryable/unknown outcome, có deadline/budget/jitter và operation idempotent. Generic retry quanh mọi exception rất nguy hiểm với payment.
Bổ sung · Luyện lập luận

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?
Không. Repository là domain-facing boundary theo aggregate/query intent. Expose generic CRUD có thể cho caller phá invariant; Spring Data là công cụ implement chứ không định nghĩa pattern.
Bổ sung · Luyện lập luận

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?
Không; Saga là distributed workflow pattern. Nó phối hợp local transactions/compensations và thường kết hợp State, Command, Outbox, Inbox.
Bổ sung · Luyện lập luận

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?
Giữ đơn giản tới khi variation/boundary rõ, đo change pain, refactor qua tests. So cost conditional trực tiếp với indirection/config/operations và xóa abstraction không còn bảo vệ gì.
Bổ sung · Luyện lập luận

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.

  1. Vẽ responsibilities và transaction/external boundaries trước khi tạo class.
  2. Tách fee bằng Strategy; vendor bằng Port + Adapter; selection bằng Registry/Factory.
  3. Đặt metrics/resilience bằng explicit Decorator và test ordering.
  4. Giữ orchestration explicit; không giấu business transition trong AOP.
  5. 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.

Bổ sung · Kế hoạch thực hành và acceptance gate

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.

Practice 1 — kiểm tra cả cấu trúc lẫn business outcome
TestFailure injectionAcceptance / evidence cần nộp
P1.1 · SelectionTrù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 responseProvider 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 · OrderingLầ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 · BoundaryAdapter 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.

Ví dụ hoàn chỉnh bổ sung · DecoratorOrderCheck.java · Java 21
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: PASSbreaker-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

  1. Model PENDING → RESERVED → PAYMENT_AUTHORIZED → CONFIRMED và compensation states.
  2. Implement bằng enum + transition table, sau đó bằng State objects.
  3. So code size, discoverability, illegal-transition handling và testability.
  4. 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.

Bổ sung · Concurrent transition drill

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.

SQL bổ sung · Predicate tối thiểu cho một local transition
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.

Interview red flag: thuộc 23 định nghĩa nhưng không phân biệt intent/consequence, hoặc gắn pattern tùy tiện vào mọi annotation/framework class.
Bổ sung · Decision record và review gate

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.

Ví dụ decision record — giả định minh họa, không phải dữ liệu của dự án
Trường phải điềnVí dụ để bắt đầu thảo luận
Problem / rate of changeModule 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 / boundaryCheckout đang biết enum của SDK vendor; domain cần độc lập với model bên ngoài.
Candidate / simpler alternativeStrategy + registry so với một switch tập trung; Adapter cho vendor boundary.
Migration / behavior preservationKhóa behavior bằng tests, di chuyển từng nhánh và chạy lại cùng fixtures.
Failure modes / rollbackThiếu registration, mapping sai, response mất; rollback code/config nhưng phải xử lý state/external effects đã tạo.
Next-change evidenceThê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.

Tài liệu đối chiếu và đọc thêm

Tra cứu ngày 12/09/2026. Java API và ngôn ngữ ghim ở Java 21; tài liệu Spring, Lombok và Resilience4j dùng URL tài liệu hiện hành, không phải cam kết một version cho mọi dự án. Kiểm tra version, proxy mode và cấu hình của ứng dụng trước khi áp dụng.

  1. Spring · Proxying Mechanisms
  2. Spring · Bean Scopes
  3. Java SE 21 · Pattern Matching for switch
  4. Microservices.io · Transactional Outbox
  5. Microservices.io · Saga