Từ pattern tới code production
Ví dụ banking/checkout cho thấy pattern bảo vệ variation và boundary ở đâu, đồng thời phân biệt phần Spring đã làm với phần business code vẫn phải thiết kế.
Strategy + Registry/Factory
interface FeePolicy {
Money calculate(Transfer transfer);
}
record FeePolicyRegistration(TransferType type, FeePolicy policy) {}
final class FeePolicyRegistry {
private final Map<TransferType, FeePolicy> policies;
FeePolicyRegistry(List<FeePolicyRegistration> registrations) {
policies = registrations.stream().collect(Collectors.toUnmodifiableMap(
FeePolicyRegistration::type, FeePolicyRegistration::policy));
}
FeePolicy forType(TransferType type) {
return Optional.ofNullable(policies.get(type))
.orElseThrow(() -> new UnsupportedOperationException(type.name()));
}
}Strategy loại conditional phân tán; Registry tập trung mapping. Không phụ thuộc bean name string như business contract nếu domain enum/key rõ hơn.
Fact — Java 21: overload hai mapper của toUnmodifiableMap ném IllegalStateException khi trùng key và từ chối key/value null. Đây là lỗi cấu hình nên phát hiện lúc lắp registry, không phải đợi request đầu tiên. [4]
Đề xuất thiết kế: thêm validation cho registration list, key đầu vào và tập loại transfer phải hỗ trợ. Mapping không sửa được không đồng nghĩa các FeePolicy bên trong thread-safe; giữ policy stateless hoặc xác định cơ chế đồng bộ riêng. Chỉ hot-reload khi có nhu cầu thật và có chính sách thay cả snapshot.
Evidence cần có: test key hợp lệ, key chưa đăng ký, duplicate registration, null input và thiếu một loại bắt buộc. Thêm policy chỉ đổi composition root; không nhân bản switch trong các caller.
Abstract Factory cho một vendor family
interface PaymentProviderFactory {
PaymentClient client();
PaymentWebhookVerifier verifier();
PaymentErrorMapper errorMapper();
}Đây là Abstract Factory vì tạo family components phải tương thích cùng vendor. Nếu chỉ có PaymentClient create(config), đó là factory đơn giản/Factory Method tùy ownership của creation hook.
Boundary: factory cấp một bộ client, webhook verifier và error mapper của cùng vendor. Một factory đơn giản trả duy nhất một client không tự trở thành GoF Abstract Factory; tên class không quyết định intent.
Trade-off: family interface giúp ngăn phối nhầm components nhưng tăng số abstraction phải duy trì. Chỉ dùng khi các product thực sự phải đi cùng nhau; factory cũng cần phân biệt môi trường sandbox/production và cấu hình merchant.
Test gợi ý: tạo từng family rồi chạy cùng contract suite; webhook có chữ ký sai phải bị từ chối, lỗi vendor được dịch nhất quán và không thể vô tình ghép client của A với verifier của B.
Builder cho immutable object
TransferRequest request = TransferRequest.builder()
.sourceAccount(source)
.destinationAccount(destination)
.amount(amount)
.reference(reference)
.build(); // validate required fields and cross-field invariant
Builder tránh telescoping constructor và làm call site rõ. Với record nhỏ, canonical constructor hoặc named factory đơn giản hơn. Lombok @Builder không tự enforce invariant.
Fact: Lombok sinh builder và đường gọi tạo object; đó không phải bộ luật nghiệp vụ tự động. Đặt required-field và cross-field checks ở constructor/factory được builder gọi, hoặc trong build() do mình kiểm soát. [5]
Đề xuất review: thử cả builder, constructor và named factory, không chỉ happy path của một cách tạo object. Kiểm tra amount, currency, hai tài khoản trùng nhau và collection đầu vào bị sửa sau khi build. Với object nhỏ, chọn record/constructor rõ ràng thay vì thêm builder chỉ để tăng số pattern.
Adapter tại integration boundary
interface FraudPort {
FraudDecision evaluate(Transfer transfer, Duration timeout);
}
final class VendorFraudAdapter implements FraudPort {
private final VendorSdk sdk;
public FraudDecision evaluate(Transfer transfer, Duration timeout) {
try {
return map(sdk.score(toVendorRequest(transfer), timeout));
} catch (VendorTimeout ex) {
return FraudDecision.unknown(ex.requestId());
}
}
}Adapter phải translate model, errors, timeout và unknown outcome. Chỉ đổi method name nhưng để vendor exception/model chảy vào domain chưa tạo anti-corruption boundary.
Giới hạn code nguồn: VendorFraudAdapter chưa có constructor gán final VendorSdk sdk; các type và hàm mapping cũng chưa được khai báo. Đây là sketch về boundary, không phải một file Java tự biên dịch.
Đề xuất contract: tách rejected, retryable failure và unknown; unknown không có nghĩa là fraud pass hay payment failed. Caller phải quyết định fail-closed, tạm giữ để review hoặc reconciliation dựa trên yêu cầu nghiệp vụ.
Test gợi ý: response hợp lệ, response không đọc được, vendor timeout, lỗi authentication và request bị xử lý nhưng mất response. Kiểm tra domain không import SDK, timeout/deadline được truyền đúng và log không lộ secret hoặc dữ liệu thanh toán.
Decorator cho behavior có thứ tự
FraudPort observed = new MetricsFraudPort(
new RetryFraudPort(
new CircuitBreakingFraudPort(vendor)));
Thứ tự thay đổi semantics: retry ngoài circuit breaker khác breaker ngoài retry; metrics ngoài cùng đo user-visible latency. Spring AOP phù hợp concern kỹ thuật, nhưng business-critical ordering nên explicit và test được.
Suy luận từ cách lồng wrapper: với Metrics(Retry(Breaker(vendor))), metrics ngoài đo một lời gọi logic; mỗi lần retry lại đi qua breaker. Đổi thành Metrics(Breaker(Retry(vendor))) khiến breaker ngoài nhìn kết quả của cả chuỗi retry. Outcome cụ thể vẫn phụ thuộc contract và cấu hình từng wrapper.
Scope guarantee: circuit breaker không tự giới hạn số lời gọi đồng thời. Tài liệu Resilience4j tách trách nhiệm đó sang bulkhead; sliding-window size không phải concurrency limit. [12]
Test gợi ý: ghi trace enter/exit, số logical calls và số attempts; thử permanent error, transient error, breaker-open và deadline cạn. Bài Practice 1 có chương trình Java kiểm tra hai thứ tự wrapper, không phụ thuộc thư viện resilience.
Chain of Responsibility
interface TransferRule {
Optional<Rejection> check(TransferContext context);
}
for (TransferRule rule : orderedRules) {
var rejection = rule.check(context);
if (rejection.isPresent()) return rejection.get();
}Phù hợp validation/authorization pipeline. Phải định nghĩa order, short-circuit, exception và whether all violations cần collect. Servlet filters và Spring Security filter chain là ví dụ framework.
Đề xuất: viết rõ rule nào chạy trước, rejection nào kết thúc pipeline và exception nào phải làm cả request thất bại. Đừng biến exception khi kiểm tra authorization thành “không có rejection”. Thu thập mọi lỗi và short-circuit là hai contract khác nhau.
Evidence cần có: rule đầu reject thì rule sau không chạy; pipeline thành công giữ đúng thứ tự; exception không bị nuốt; duplicate rule không gây side effect hai lần. Vòng for nguồn phải nằm trong method có kiểu trả về phù hợp.
Template Method hay composition?
abstract class TransferTemplate {
final Receipt execute(Command command) {
validate(command);
var result = perform(command);
audit(result);
return result;
}
protected abstract Result perform(Command command);
}Template Method hợp khi framework sở hữu stable lifecycle. Nếu steps cần mix-and-match hoặc test độc lập, inject Strategy/collaborators thường tránh inheritance coupling.
Điểm chưa được nguồn định nghĩa: execute() trả Receipt, còn perform() trả Result, nhưng nguồn chưa cho biết quan hệ hai type. Vì vậy chưa thể kết luận dòng return result; có hợp lệ trong model gốc hay không. validate và audit cũng mới được gọi, chưa có định nghĩa.
Ví dụ bổ sung bên dưới chủ động coi Result và Receipt là hai record độc lập rồi map rõ ràng. Nó minh họa lifecycle trong bộ nhớ; không chứng minh audit và business write cùng một database transaction.
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class TemplateMethodCheck {
record Command(String id) {}
record Result(String code) {}
record Receipt(String code) {}
abstract static class TransferTemplate {
private final List<String> trace;
TransferTemplate(List<String> trace) {
this.trace = Objects.requireNonNull(trace);
}
final Receipt execute(Command command) {
validate(command);
Result result = Objects.requireNonNull(perform(command));
audit(result);
return new Receipt(result.code());
}
private void validate(Command command) {
if (command == null || command.id() == null
|| command.id().isBlank()) {
throw new IllegalArgumentException("command id is required");
}
trace.add("validate");
}
protected abstract Result perform(Command command);
private void audit(Result result) {
trace.add("audit:" + result.code());
}
}
public static void main(String[] args) {
List<String> trace = new ArrayList<>();
TransferTemplate service = new TransferTemplate(trace) {
@Override
protected Result perform(Command command) {
trace.add("perform:" + command.id());
return new Result("ok");
}
};
Receipt receipt = service.execute(new Command("cmd-1"));
if (!receipt.equals(new Receipt("ok"))
|| !trace.equals(List.of(
"validate", "perform:cmd-1", "audit:ok"))) {
throw new AssertionError(trace);
}
System.out.println("template lifecycle: PASS");
}
}
Lưu thành TemplateMethodCheck.java, chạy hai lệnh bên dưới. Kết quả kỳ vọng: template lifecycle: PASS.
javac --release 21 TemplateMethodCheck.java
java TemplateMethodCheck
Trade-off cần thử: audit thất bại sau perform thì business contract phải xử lý thế nào? Nếu skeleton phải sửa cho mọi subclass hoặc hook chỉ tồn tại để bỏ qua một bước, thử composition trước khi mở rộng hierarchy.
State cho payment lifecycle
sealed interface PaymentState permits Pending, Authorized, Captured, Failed {
PaymentState capture(PaymentContext context);
}
record Authorized(String authorizationId) implements PaymentState {
public PaymentState capture(PaymentContext context) {
return context.gateway().capture(authorizationId);
}
}State objects làm transition policy rõ hơn switch lớn. Database vẫn cần version/conditional update hoặc lock để hai workers không transition cùng payment; pattern không tự giải quyết concurrency/durability.
Giới hạn sketch: các implementation Pending, Captured, Failed, PaymentContext và gateway chưa được nguồn cung cấp. Chưa có model cho capture đang xử lý, duplicate command hoặc unknown outcome; không mặc định đồng nhất chúng với Failed.
Đề xuất protocol: lưu command/idempotency key và provider reference, kiểm tra transition hợp lệ, dùng version/conditional update tại database và có recovery cho lời gọi ngoài bị mất response. Optimistic locking trên row sau khi gọi gateway không tự ngăn hai workers cùng gọi gateway trước đó.
Evidence cần có: ma trận legal/illegal edges, hai workers tranh cùng version, command lặp và crash giữa external effect với local update. Phải đối chiếu ledger phía provider; chỉ nhìn state cuối trong database chưa đủ chứng minh không double-charge.
Command và Observer qua distributed boundary
TransferMoneyCommand đóng gói intent để validate/audit/queue, nhưng retry cần idempotency key. Spring application event dùng Observer trong-process và có sync/transaction semantics tùy API/config. Kafka/RabbitMQ có duplicate, ordering và durability; không suy luận guarantee của broker từ Observer pattern.
Fact: SimpleApplicationEventMulticaster mặc định gọi listener trên thread của caller; task executor có thể đổi cách dispatch. [8] @TransactionalEventListener mặc định ở AFTER_COMMIT; không có transaction thì không chạy, trừ khi bật fallbackExecution. Hỗ trợ reactive transaction từ Spring 6.1 cần truyền transaction context trong event source. [9]
Suy luận về failure window: chạy sau commit không đồng nghĩa event đã nằm trong durable log. Process có thể chết trước khi external effect hoàn tất. Khi yêu cầu recovery qua restart, dùng protocol có lưu bền và kiểm thử crash, không dựa vào tên Observer.
Fact bổ sung về Outbox: relay có thể publish trùng nếu chết sau publish nhưng trước khi ghi nhận thành công; consumer cần cơ chế xử lý lặp. [13] Đề xuất evidence: giữ stable event ID, retry/reconciliation ledger và thử đúng cửa sổ crash đó.
Repository, Unit of Work và Specification
- Repository: domain-facing, collection-like boundary theo aggregate/query intent; không chỉ đổi tên DAO CRUD.
- Unit of Work: theo dõi changes và commit như một unit; JPA persistence context + transaction cung cấp phần lớn cơ chế.
- Data Mapper: tách domain object khỏi storage representation; ORM thực hiện mapping với mức coupling tùy model.
- Specification: đóng gói predicate composable; không nên dùng để che query khó đọc hoặc execution plan tệ.
Fact: Spring Data JPA Specification biểu diễn predicate theo Criteria API và hỗ trợ composition. Nó là công cụ xây điều kiện query, không phải bằng chứng query đã tối ưu. [10]
Đề xuất review: đặt transaction boundary ở use case, nói rõ aggregate nào được cập nhật cùng nhau và caller nào được quyền thay state. Kiểm tra SQL thực tế, số round trips, query plan, paging và dữ liệu ngoài transaction; không chỉ mock save() rồi coi đó là chứng minh persistence đúng.
Phân loại: Repository, Unit of Work và Data Mapper thuộc nhóm enterprise/domain-data patterns; Specification trong thiết kế domain và API Specification của Spring Data cần phân biệt. Không đưa cả bốn vào danh sách 23 GoF.
Patterns trong JDK và Spring
| API/framework | Pattern lens | Điểm cần nói đúng |
|---|---|---|
Comparator | Strategy | Ordering algorithm truyền vào sort; comparator phải giữ contract. |
Iterator | Iterator | Tách traversal khỏi representation. |
InputStream wrappers | Decorator | Cùng abstraction, thêm buffering/decompression. |
Executors | Factory | Convenience creation; vẫn phải hiểu queue/thread/rejection. |
| Spring IoC | DI + Factory | Container tạo/wire/lifecycle object; DI có scope rộng hơn GoF. |
Spring AOP / @Transactional | Proxy | Call phải đi qua proxy; self-invocation ảnh hưởng behavior. |
JdbcTemplate | Template/callback | Template giữ resource/error skeleton, callback cung cấp variation. |
| Spring MVC | Front Controller + MVC | DispatcherServlet route qua mappings/adapters. |
Proxy mode: lời gọi qua this không đi lại qua Spring AOP proxy. Với class-based proxy, class final không thể bị subclass; method final/private không thể được advise bằng cơ chế override. Không suy rộng hạn chế class-based proxy sang mọi JDK interface proxy hoặc AspectJ weaving. [6]
Đề xuất integration test: lấy bean từ context rồi gọi qua dependency thật, thay vì chỉ new service. Test transaction rollback và đường self-invocation; ghi rõ proxy mode/config trong evidence. Các class final ở sketch là ví dụ Java thuần, không chỉ dẫn bật class-based AOP cho chúng.
Comparator contract: kiểm tra dấu đối xứng, tính bắc cầu và quan hệ của kết quả bằng 0; nếu ordering không consistent với equals, phải hiểu ảnh hưởng khi dùng sorted collections. [1]
Architecture patterns: khác scope GoF
- Hexagonal: core định nghĩa ports; web/DB/vendor là adapters.
- CQRS: tách write/read models khi needs khác nhau; không bắt buộc Event Sourcing.
- Event Sourcing: event log là source of truth; cần versioning, replay và privacy.
- Saga: local transactions + compensation cho distributed workflow.
- Outbox: business state và message record commit cùng local transaction.
- Circuit Breaker/Bulkhead: giới hạn failure propagation; không thay timeout/retry budget/load shedding.
| Nhóm | Ví dụ trong bài | Không tự bảo đảm |
|---|---|---|
| GoF / object collaboration | Strategy, Adapter, Decorator, State, Command, Observer | Durability, isolation và delivery qua process. |
| Creation / enterprise / framework | Registry, simple factory, DI, Repository, Unit of Work, Data Mapper | Không phải mọi ví dụ đều là một trong 23 GoF. |
| Architecture / distributed workflow | Hexagonal, CQRS, Event Sourcing, Saga, Outbox | Không có global ACID chỉ nhờ đặt đúng tên pattern. |
| Resilience | Circuit Breaker, Bulkhead | Timeout, retry budget, idempotency hoặc đủ capacity. |
Fact — Saga: saga phối hợp các local transactions và compensation; không cung cấp isolation như một ACID transaction duy nhất. Compensation là bước nghiệp vụ có thể cần retry, không phải tua ngược mọi external effect. [14]
Checklist thiết kế bổ sung: xác định source of truth; liệt kê local transaction boundaries; mô tả duplicate, out-of-order và unknown outcome; chỉ rõ ai retry/compensate/reconcile; nêu simpler alternative; gắn từng guarantee với một failure test. Với CQRS, ghi trade-off đồng bộ read model; với Event Sourcing, lập kế hoạch schema/replay và data lifecycle.
Tài liệu đối chiếu và đọc thêm
- Java SE 21 · Comparator
- Spring · Container Extension Points
- Spring · Declarative Transaction Management
- Java SE 21 · Collectors.toUnmodifiableMap
- Lombok · @Builder
- Spring · Proxying Mechanisms
- Spring · Bean Scopes
- Spring API · SimpleApplicationEventMulticaster
- Spring · Transaction-bound Events
- Spring Data JPA · Specifications
- Java SE 21 · Pattern Matching for switch
- Resilience4j · CircuitBreaker
- Microservices.io · Transactional Outbox
- Microservices.io · Saga