Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

facade pattern

Tung Bui Thanh (TungDaDev)10 mins read

Facade pattern.webp

Sách giáo khoa hay lấy ví dụ Facade bằng cái remote rạp hát tại gia (Home Theater Facade: bấm một nút là bật máy chiếu, bật loa). Nhưng ở dự án thật, Facade là một trong những công cụ Solution Architect dùng nhiều nhất: giấu đi mấy thư viện legacy khó xài (như Apache POI xuất Excel hay JavaMail), làm API Gateway Facade gom API cho cả chục microservice, rồi tới mức làm Anti-Corruption Layer (ACL) để giữ cho hệ thống mới khỏi bị nhiễm data model của mấy hệ thống Core Banking / ERP đời cũ.

Hệ thống càng sống lâu thì số module, class và API bên trong càng phình ra. Nếu để Client (Web/Mobile) hay các service bên ngoài chọc thẳng vào từng ngóc ngách của hệ thống con, bạn đã vi phạm nặng Law of Demeter (Principle of Least Knowledge), và kết quả là một mớ phụ thuộc chằng chịt, sau này muốn refactor cũng không nổi.

Bài này nói về ranh giới của Facade, tách bạch nó với Adapter / Proxy / Decorator, rồi dựng thử một Enterprise Onboarding Facade (E-KYC) cho ra hồn.


# Facade pattern lớn lên qua từng tầng

# law of demeter nói gì

Một object chỉ nên nói chuyện với bạn thân của nó, đừng bao giờ nói chuyện với người lạ.

Không có Facade thì code phía Client sẽ ra thế này:

// VI PHẠM NGHIÊM TRỌNG ĐỊNH LUẬT DEMETER:
customer.getProfile().getAddress().getCity().getShippingZone().calculateTax();

Client phải biết rõ bên trong của 5 object lồng nhau. Chỉ cần Address đổi cách lưu là code Client gãy cả loạt.

Facade đưa ra một interface "hạt thô" (Coarse-Grained Interface): taxCalculatorFacade.calculateTax(customerId), còn chuyện bên dưới chạy thế nào thì Client không cần biết.


# Facade vs Adapter vs Proxy vs Decorator

Bốn pattern này hay bị nhầm vì cùng là kiểu "bọc object" (Wrapper):

# bảng so sánh chi tiết

PatternDùng để làm gìBọc bao nhiêu objectInterface có đổi không?
FacadeLàm cho client gọi đơn giản hơnNhiều (Subsystem Graph)CÓ: tạo ra một interface mới gọn hơn
AdapterLàm cho 2 interface khớp nhauMộtCÓ: dịch từ Interface A sang Interface B
ProxyKiểm soát truy cập vào objectMột❌ KHÔNG: giữ nguyên 100% interface của object gốc
DecoratorGắn thêm tính năng lúc chạy mà không kế thừaMột❌ KHÔNG: giữ nguyên 100% interface của object gốc

# ở tầm doanh nghiệp: anti-corruption layer (ACL trong DDD)

Lấy ví dụ dự án hiện đại hoá ngân hàng (Core Banking Transformation): bạn viết microservice mới bằng Spring Boot và Java 21, nhưng vẫn phải nói chuyện với Core Banking đời cũ chạy COBOL trên mainframe IBM Z hoặc IBM i (AS/400) đã chạy 30 năm:

  • Hệ thống Core cũ đặt tên field kỳ quặc: CUST_ID_01, FLG_VLD_99, toàn chuỗi cố định độ dài (fixed-length string), chẳng có type-safety gì.
  • Để DTO của Mainframe tràn vào domain model mới thì cái Clean Architecture của bạn coi như bị "nhiễm bẩn" (corrupted).

Anti-Corruption Layer (thực chất là Facade kết hợp Adapter) giống như một cái "đại sứ quán" đứng giữa hai thế giới:

  1. Nhận lời gọi gọn gàng từ hệ thống mới: bankingFacade.openAccount(customer).
  2. Dịch dữ liệu sang cái định dạng thô kệch mà Mainframe hiểu.
  3. Gọi Mainframe, nhận về mã lỗi khó hiểu (kiểu ERR_9981) rồi dịch ngược thành domain exception sạch sẽ (AccountOverdraftException).
  4. Nhờ vậy code của hệ thống mới vẫn sạch, không dính tí gì tới mớ legacy.

# ví dụ thật: Facade cho E-KYC onboarding

Làm luồng mở tài khoản ngân hàng số (E-KYC Onboarding) là phải làm việc với 5 hệ thống phụ trợ độc lập:

  1. OCR Service: đọc và bóc dữ liệu từ ảnh chụp Căn cước công dân (CCCD).
  2. Facial Recognition Service: kiểm tra người trong video selfie là người thật đang đứng trước camera (Liveness Detection, chống dùng ảnh in hay video quay sẵn), rồi so khuôn mặt đó với ảnh trên thẻ (Face Match).
  3. National ID Database: đối chiếu thông tin CCCD với cơ sở dữ liệu định danh và danh sách cảnh báo (blacklist).
  4. Credit Bureau Service (CIC): tra điểm tín dụng quốc gia.
  5. Account Provisioning Engine: tạo số tài khoản, phát hành thẻ ảo, kích hoạt Internet Banking.

# code mẫu

public record OnboardingRequest(byte[] idFront, byte[] idBack, byte[] selfieVideo, String phone) {}
 
public record OnboardingResult(String accountNumber, String cifNumber, boolean success, String message) {}
 
@Service
public class CustomerOnboardingFacade {
 
    private static final Logger log = LoggerFactory.getLogger(CustomerOnboardingFacade.class);
 
    private final OcrService ocrService;
    private final BiometricService biometricService;
    private final NationalDatabaseClient nationalDatabaseClient;
    private final CreditBureauClient creditBureauClient;
    private final AccountProvisioningService provisioningService;
 
    public CustomerOnboardingFacade(
            OcrService ocrService,
            BiometricService biometricService,
            NationalDatabaseClient nationalDatabaseClient,
            CreditBureauClient creditBureauClient,
            AccountProvisioningService provisioningService) {
        this.ocrService = ocrService;
        this.biometricService = biometricService;
        this.nationalDatabaseClient = nationalDatabaseClient;
        this.creditBureauClient = creditBureauClient;
        this.provisioningService = provisioningService;
    }
 
    // Không đặt @Transactional ở đây: các bước 1-4 là lời gọi mạng chậm, giữ transaction (và DB connection)
    // suốt thời gian đó là phí, mà rollback DB cũng không huỷ được việc đã làm ở hệ thống bên ngoài.
    public OnboardingResult onboardCustomer(OnboardingRequest request) {
        log.info("Bắt đầu quy trình E-KYC Onboarding cho SĐT: {}", maskPhone(request.phone()));
 
        // 1. Trích xuất thông tin giấy tờ
        var idData = ocrService.extractIdentity(request.idFront(), request.idBack());
 
        // 2. Nhận diện sinh trắc học
        double matchScore = biometricService.verifyFace(request.selfieVideo(), request.idFront());
        if (matchScore < 0.90) {
            throw new BiometricVerificationFailedException("Xác thực khuôn mặt không trùng khớp CCCD");
        }
 
        // 3. Kiểm tra danh sách cảnh báo
        if (nationalDatabaseClient.isBlacklisted(idData.nationalId())) {
            throw new SecurityRiskException("Căn cước công dân thuộc danh sách rủi ro an ninh");
        }
 
        // 4. Tra cứu hạn mức tín dụng
        var creditTier = creditBureauClient.assessCreditTier(idData.nationalId());
 
        // 5. Khởi tạo tài khoản lõi
        var account = provisioningService.provisionNewAccount(idData, creditTier, request.phone());
 
        log.info("E-KYC thành công, CIF: {}", account.cif());
        return new OnboardingResult(account.accountNumber(), account.cif(), true, "Mở tài khoản thành công");
    }
 
    // Số điện thoại là dữ liệu cá nhân, không ghi nguyên văn ra log
    private static String maskPhone(String phone) {
        return phone == null || phone.length() < 4 ? "****" : "****" + phone.substring(phone.length() - 4);
    }
}

Để ý chỗ không có @Transactional. Đặt nó lên cả method Facade thì nhìn có vẻ an toàn, nhưng thực ra transaction chỉ bao được phần ghi vào database của chính service này. OCR, so khuôn mặt, tra CIC đều là lời gọi mạng có thể mất vài giây, trong lúc đó connection DB bị giữ không làm gì, tải cao là cạn connection pool. Còn nếu bước 5 gọi sang một hệ thống core khác thì lỗi xảy ra sau đó cũng không rollback được tài khoản đã tạo bên kia. Transaction nên nằm ở provisionNewAccount() (nếu nó ghi DB cục bộ), còn chuyện nhất quán giữa nhiều hệ thống thì xem mục 4 của checklist dưới.

Log cũng vậy: luồng E-KYC đụng toàn dữ liệu cá nhân (số điện thoại, số CCCD, số tài khoản), nên chỉ ghi dạng đã che hoặc mã định danh nội bộ.


# checklist trước khi đưa lên production (architect's checklist)

  • 1. Giữ interface hạt thô (coarse-grained): method của Facade không bắt Client truyền vào mấy tham số nội bộ chi tiết của từng subsystem.
  • 2. Đừng biến Facade thành God Object: mỗi Facade chỉ nên đại diện cho một mảng nghiệp vụ cụ thể (ví dụ OnboardingFacade, PaymentFacade). Tránh kiểu một class AppFacade to đùng ôm hết mọi thứ.
  • 3. Giấu các class subsystem: cân nhắc để các class subsystem ở mức package-private (không có từ khoá public), chỉ class Facade là public, để module khác buộc phải đi qua Facade.
  • 4. Lo giao dịch phân tán: nếu các subsystem là REST API độc lập, Facade phải có cơ chế Saga Compensating Transactions để bù trừ khi một bước ở giữa bị lỗi, và các bước có tác dụng phụ (tạo tài khoản, phát hành thẻ) phải idempotent để client retry không tạo ra hai tài khoản.
  • 5. Đo metrics end-to-end: bọc cả method Facade trong một Trace Span duy nhất để đo tổng thời gian người dùng phải chờ (Overall SLA Duration).

# lời kết

Facade làm một việc đơn giản: bên trong có rối cỡ nào thì bên ngoài vẫn là một cánh cửa gọn gàng, dễ gọi.

Từ một class Java nhỏ trong cùng process cho tới API Gateway hay Anti-Corruption Layer ở tầm hệ thống phân tán, ý tưởng vẫn vậy. Cái khó là giữ cho Facade mỏng: nó chỉ điều phối và dịch dữ liệu, còn logic nghiệp vụ thật thì để ở các service bên dưới. Muốn đọc thêm về ACL thì chương "Maintaining Model Integrity" trong cuốn Domain-Driven Design của Eric Evans là chỗ nên bắt đầu.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.