Facade Pattern #

Proses checkout di sebuah e-commerce terlihat sederhana dari sisi pengguna: klik “Bayar”, tunggu sebentar, pesanan terkonfirmasi. Tapi di balik layar, ada banyak langkah yang harus terjadi secara berurutan dan terkoordinasi — validasi user, pengecekan stok inventory, perhitungan harga setelah diskon, pembebanan pembayaran, update stok setelah pembelian, pengiriman invoice via email, dan notifikasi ke tim fulfillment. Tanpa Facade Pattern, kode yang memanggil checkout harus tahu semua langkah ini, memanggil tiap subsistem satu per satu dengan urutan yang tepat, dan menangani error dari masing-masing. Facade Pattern memindahkan semua kerumitan itu ke dalam satu struct yang menyediakan satu method Checkout() — client hanya memanggil satu fungsi, dan Facade yang mengurus sisanya.

Apa itu Facade Pattern? #

Facade Pattern adalah structural design pattern yang menyediakan antarmuka sederhana untuk sekumpulan antarmuka yang lebih kompleks dalam sebuah subsistem. Facade tidak menghilangkan kompleksitas — ia menyembunyikannya di balik satu titik masuk yang bersih, sehingga client tidak perlu memahami atau bergantung pada detail internal subsistem.

Analogi yang tepat: resepsionis hotel. Tamu tidak perlu menghubungi housekeeping, restoran, dan security secara terpisah untuk setiap kebutuhan — cukup hubungi resepsionis, dan resepsionis yang mengkoordinasikan semuanya. Resepsionis adalah Facade.

Tiga properti Facade Pattern:

  • Satu titik masuk — client berinteraksi dengan satu interface yang sederhana, bukan dengan banyak subsistem secara langsung
  • Orkestrasi, bukan implementasi — Facade memanggil dan mengkoordinasikan subsistem; logika bisnis tetap di subsistem masing-masing
  • Subsistem tetap bisa diakses langsung — Facade adalah convenience layer, bukan pembatas; kode yang butuh akses detail bisa tetap menggunakan subsistem langsung
flowchart LR
    subgraph "Tanpa Facade — Client Tahu Semua"
        C1[Client] --> US1[UserService]
        C1 --> IS1[InventoryService]
        C1 --> PS1[PaymentService]
        C1 --> DS1[DiscountService]
        C1 --> NS1[NotificationService]
        C1 --> LS1[LogService]
    end

    subgraph "Dengan Facade — Client Tahu Satu"
        C2[Client] -->|Checkout| F[CheckoutFacade]
        F --> US2[UserService]
        F --> IS2[InventoryService]
        F --> PS2[PaymentService]
        F --> DS2[DiscountService]
        F --> NS2[NotificationService]
        F --> LS2[LogService]
    end

Masalah yang Dipecahkan #

Facade menyelesaikan dua masalah yang berbeda tapi saling berkaitan: coupling yang tinggi antara client dan subsistem, dan duplikasi logika orkestrasi yang tersebar di banyak tempat.

Masalah 1: Client Harus Tahu Terlalu Banyak #

// ANTI-PATTERN: client melakukan orkestrasi sendiri — penuh dengan detail teknis
func HandleCheckout(w http.ResponseWriter, r *http.Request) {
    req := parseCheckoutRequest(r)

    // Client harus tahu urutan yang tepat
    userSvc := user.NewService(db)
    if err := userSvc.Validate(req.UserID); err != nil {
        http.Error(w, err.Error(), 400)
        return
    }

    inventorySvc := inventory.NewService(db)
    items, err := inventorySvc.CheckAndReserve(req.Items)
    if err != nil {
        http.Error(w, err.Error(), 422)
        return
    }

    discountSvc := discount.NewService(db)
    finalPrice := discountSvc.Apply(req.TotalPrice, req.CouponCode, req.UserID)

    paymentSvc := payment.NewService(cfg.PaymentKey)
    txID, err := paymentSvc.Charge(req.UserID, finalPrice, req.PaymentMethod)
    if err != nil {
        inventorySvc.ReleaseReservation(items) // harus ingat rollback!
        http.Error(w, err.Error(), 402)
        return
    }

    notifSvc := notification.NewService(cfg.SMTPConfig)
    notifSvc.SendInvoice(req.UserID, txID, items, finalPrice)

    logSvc := log.NewAuditLogger(db)
    logSvc.LogCheckout(req.UserID, txID, finalPrice)

    // Handler ini tahu terlalu banyak tentang subsistem
    // Jika urutan berubah, semua handler yang punya kode serupa harus diupdate
}

// BENAR: client hanya tahu satu method
func HandleCheckout(w http.ResponseWriter, r *http.Request) {
    req := parseCheckoutRequest(r)
    result, err := checkoutFacade.Checkout(r.Context(), req)
    if err != nil {
        http.Error(w, err.Error(), resolveStatusCode(err))
        return
    }
    writeJSON(w, result)
}

Masalah 2: Orkestrasi yang Sama Diulang di Banyak Tempat #

Tanpa Facade, logika “periksa user → periksa stok → proses pembayaran” mungkin diimplementasikan ulang di HTTP handler, di CLI command, di background job, dan di integration test. Setiap perubahan urutan atau penambahan langkah harus diupdate di semua tempat.


Struktur dan Komponen #

Facade Pattern melibatkan tiga komponen sederhana, lebih sedikit dari kebanyakan pattern lain.

flowchart TD
    C[Client\nHTTP Handler, CLI, Background Job] -->|high-level call| F

    subgraph F[Facade]
        direction TB
        FM["Checkout(ctx, req) error\nProcessRefund(ctx, req) error\nCancelOrder(ctx, orderID) error"]
    end

    F --> S1[UserService\nvalidasi & profil]
    F --> S2[InventoryService\nstok & reservasi]
    F --> S3[PaymentService\ncharge & refund]
    F --> S4[DiscountService\nhitung diskon]
    F --> S5[NotificationService\nemail & SMS]
    F --> S6[AuditLogService\nrecord semua events]
Komponen Peran Karakteristik
Facade Satu titik masuk, mengorkestrasikan subsistem Tidak berisi business logic sendiri
Subsystem Implementasi logika yang sebenarnya Tidak tahu keberadaan Facade
Client Memanggil Facade untuk operasi tingkat tinggi Tidak bergantung pada subsistem langsung

Implementasi Lengkap: Checkout System #

Studi kasus yang lebih lengkap dari artikel asli — checkout e-commerce yang melibatkan enam subsistem dengan error handling, rollback, dan audit logging.

Subsystem Interfaces #

package checkout

import (
    "context"
    "time"
)

// CheckoutRequest berisi semua data yang dibutuhkan untuk proses checkout.
type CheckoutRequest struct {
    UserID        string
    Items         []OrderItem
    CouponCode    string
    PaymentMethod string
    ShippingAddr  Address
}

// OrderItem merepresentasikan satu item dalam pesanan.
type OrderItem struct {
    ProductID string
    Quantity  int
    UnitPrice float64
}

// Address merepresentasikan alamat pengiriman.
type Address struct {
    Street  string
    City    string
    ZIP     string
    Country string
}

// CheckoutResult berisi ringkasan hasil checkout yang berhasil.
type CheckoutResult struct {
    OrderID       string
    TransactionID string
    FinalPrice    float64
    EstimatedDelivery time.Time
}

// --- Subsystem interfaces --- //

// UserValidator memvalidasi status dan otorisasi user.
type UserValidator interface {
    Validate(ctx context.Context, userID string) error
    GetProfile(ctx context.Context, userID string) (*UserProfile, error)
}

// UserProfile menyimpan informasi profil user.
type UserProfile struct {
    ID    string
    Name  string
    Email string
    Phone string
    Tier  string // "regular", "silver", "gold"
}

// InventoryService menangani stok dan reservasi barang.
type InventoryService interface {
    CheckAvailability(ctx context.Context, items []OrderItem) error
    Reserve(ctx context.Context, items []OrderItem) (string, error) // mengembalikan reservationID
    ReleaseReservation(ctx context.Context, reservationID string) error
    Commit(ctx context.Context, reservationID string) error
}

// DiscountService menghitung harga setelah diskon.
type DiscountService interface {
    Calculate(ctx context.Context, userID, couponCode string, items []OrderItem) (float64, error)
}

// PaymentService menangani pembayaran.
type PaymentService interface {
    Charge(ctx context.Context, userID string, amount float64, method string) (string, error) // mengembalikan transactionID
    Refund(ctx context.Context, transactionID string, amount float64) error
}

// NotificationService mengirim notifikasi ke user.
type NotificationService interface {
    SendOrderConfirmation(ctx context.Context, profile *UserProfile, result *CheckoutResult) error
    SendOrderCancellation(ctx context.Context, profile *UserProfile, orderID string) error
}

// AuditLogger mencatat semua event penting untuk keperluan audit.
type AuditLogger interface {
    LogCheckout(ctx context.Context, userID string, result *CheckoutResult)
    LogCheckoutFailure(ctx context.Context, userID string, reason string)
    LogRefund(ctx context.Context, transactionID string, amount float64)
}

// OrderRepository menyimpan data pesanan.
type OrderRepository interface {
    Create(ctx context.Context, req CheckoutRequest, result CheckoutResult) error
    UpdateStatus(ctx context.Context, orderID, status string) error
}

Facade Implementation #

package checkout

import (
    "context"
    "fmt"
    "time"
)

// CheckoutFacade adalah facade untuk seluruh proses checkout.
// Client hanya berinteraksi dengan struct ini — tidak langsung ke subsistem.
type CheckoutFacade struct {
    userValidator  UserValidator
    inventory      InventoryService
    discount       DiscountService
    payment        PaymentService
    notification   NotificationService
    auditLog       AuditLogger
    orderRepo      OrderRepository
}

// CheckoutFacadeConfig menyimpan semua dependency yang dibutuhkan Facade.
type CheckoutFacadeConfig struct {
    UserValidator  UserValidator
    Inventory      InventoryService
    Discount       DiscountService
    Payment        PaymentService
    Notification   NotificationService
    AuditLog       AuditLogger
    OrderRepo      OrderRepository
}

// NewCheckoutFacade membuat instance Facade dengan dependency yang sudah dikonfigurasi.
func NewCheckoutFacade(cfg CheckoutFacadeConfig) *CheckoutFacade {
    return &CheckoutFacade{
        userValidator: cfg.UserValidator,
        inventory:     cfg.Inventory,
        discount:      cfg.Discount,
        payment:       cfg.Payment,
        notification:  cfg.Notification,
        auditLog:      cfg.AuditLog,
        orderRepo:     cfg.OrderRepo,
    }
}

// Checkout mengorkestrasikan seluruh proses checkout dari awal hingga selesai.
// Ini adalah satu-satunya method yang perlu dikenal client.
func (f *CheckoutFacade) Checkout(ctx context.Context, req CheckoutRequest) (*CheckoutResult, error) {
    // Langkah 1: Validasi user
    profile, err := f.validateUser(ctx, req.UserID)
    if err != nil {
        f.auditLog.LogCheckoutFailure(ctx, req.UserID, fmt.Sprintf("user validation failed: %v", err))
        return nil, fmt.Errorf("user validation failed: %w", err)
    }

    // Langkah 2: Periksa dan reservasi stok
    reservationID, err := f.reserveInventory(ctx, req.Items)
    if err != nil {
        f.auditLog.LogCheckoutFailure(ctx, req.UserID, fmt.Sprintf("inventory check failed: %v", err))
        return nil, fmt.Errorf("inventory not available: %w", err)
    }

    // Langkah 3: Hitung harga final setelah diskon
    finalPrice, err := f.discount.Calculate(ctx, req.UserID, req.CouponCode, req.Items)
    if err != nil {
        // Gagal hitung diskon → lepas reservasi
        _ = f.inventory.ReleaseReservation(ctx, reservationID)
        f.auditLog.LogCheckoutFailure(ctx, req.UserID, fmt.Sprintf("discount calculation failed: %v", err))
        return nil, fmt.Errorf("failed to calculate price: %w", err)
    }

    // Langkah 4: Proses pembayaran
    transactionID, err := f.payment.Charge(ctx, req.UserID, finalPrice, req.PaymentMethod)
    if err != nil {
        // Gagal bayar → lepas reservasi
        _ = f.inventory.ReleaseReservation(ctx, reservationID)
        f.auditLog.LogCheckoutFailure(ctx, req.UserID, fmt.Sprintf("payment failed: %v", err))
        return nil, fmt.Errorf("payment failed: %w", err)
    }

    // Langkah 5: Commit reservasi stok (tidak bisa di-rollback setelah ini)
    if err := f.inventory.Commit(ctx, reservationID); err != nil {
        // Commit gagal tapi payment sudah berhasil — perlu refund
        _ = f.payment.Refund(ctx, transactionID, finalPrice)
        f.auditLog.LogCheckoutFailure(ctx, req.UserID, fmt.Sprintf("inventory commit failed: %v", err))
        return nil, fmt.Errorf("order processing failed: %w", err)
    }

    // Langkah 6: Buat record pesanan
    result := &CheckoutResult{
        OrderID:           generateOrderID(),
        TransactionID:     transactionID,
        FinalPrice:        finalPrice,
        EstimatedDelivery: time.Now().Add(3 * 24 * time.Hour),
    }

    if err := f.orderRepo.Create(ctx, req, *result); err != nil {
        // Log tapi jangan gagalkan — pesanan sudah terjadi
        f.auditLog.LogCheckoutFailure(ctx, req.UserID, fmt.Sprintf("order record failed: %v", err))
    }

    // Langkah 7: Kirim notifikasi (best-effort — tidak gagalkan checkout)
    go func() {
        bgCtx := context.Background() // gunakan context baru agar tidak ter-cancel
        _ = f.notification.SendOrderConfirmation(bgCtx, profile, result)
    }()

    // Langkah 8: Audit log
    f.auditLog.LogCheckout(ctx, req.UserID, result)

    return result, nil
}

// ProcessRefund mengorkestrasikan proses refund untuk sebuah pesanan.
func (f *CheckoutFacade) ProcessRefund(ctx context.Context, orderID, transactionID string, amount float64, userID string) error {
    // Refund pembayaran
    if err := f.payment.Refund(ctx, transactionID, amount); err != nil {
        return fmt.Errorf("refund failed: %w", err)
    }

    // Update status pesanan
    if err := f.orderRepo.UpdateStatus(ctx, orderID, "refunded"); err != nil {
        f.auditLog.LogCheckoutFailure(ctx, userID, fmt.Sprintf("order status update failed after refund: %v", err))
        // Lanjutkan — refund sudah berhasil
    }

    f.auditLog.LogRefund(ctx, transactionID, amount)

    // Kirim notifikasi pembatalan
    profile, _ := f.userValidator.GetProfile(ctx, userID)
    if profile != nil {
        go func() {
            _ = f.notification.SendOrderCancellation(context.Background(), profile, orderID)
        }()
    }

    return nil
}

// helper: validateUser memvalidasi user dan mengambil profilnya.
func (f *CheckoutFacade) validateUser(ctx context.Context, userID string) (*UserProfile, error) {
    if err := f.userValidator.Validate(ctx, userID); err != nil {
        return nil, err
    }
    return f.userValidator.GetProfile(ctx, userID)
}

// helper: reserveInventory memeriksa ketersediaan dan mereservasi stok.
func (f *CheckoutFacade) reserveInventory(ctx context.Context, items []OrderItem) (string, error) {
    if err := f.inventory.CheckAvailability(ctx, items); err != nil {
        return "", err
    }
    return f.inventory.Reserve(ctx, items)
}

func generateOrderID() string {
    return fmt.Sprintf("ORD-%d", time.Now().UnixNano())
}

Client Code: Sangat Sederhana #

// HTTP Handler — hanya tahu Facade
func (h *CheckoutHandler) Handle(w http.ResponseWriter, r *http.Request) {
    var req checkout.CheckoutRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "invalid request body", 400)
        return
    }
    req.UserID = getUserIDFromContext(r.Context())

    result, err := h.facade.Checkout(r.Context(), req)
    if err != nil {
        writeError(w, err)
        return
    }
    writeJSON(w, result)
}

// Background Job — juga hanya tahu Facade
func (j *RetryCheckoutJob) Run(ctx context.Context) {
    pendingOrders := j.orderRepo.GetPending()
    for _, order := range pendingOrders {
        _, err := j.facade.Checkout(ctx, order.AsRequest())
        if err != nil {
            log.Printf("retry checkout failed for order %s: %v", order.ID, err)
        }
    }
}

// CLI Command — juga hanya tahu Facade
func runCheckoutCommand(cmd *cobra.Command, args []string) {
    req := buildRequestFromArgs(args)
    result, err := facade.Checkout(context.Background(), req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "checkout failed: %v\n", err)
        os.Exit(1)
    }
    fmt.Printf("Order created: %s (Rp %.0f)\n", result.OrderID, result.FinalPrice)
}

Studi Kasus Kedua: User Onboarding #

Facade tidak hanya berguna untuk checkout — setiap proses bisnis yang melibatkan banyak subsistem adalah kandidat kuat. User onboarding adalah contoh lain yang sangat umum.

// OnboardingFacade mengorkestrasikan seluruh proses registrasi user baru.
type OnboardingFacade struct {
    userRepo       UserRepository
    profileService ProfileService
    emailVerifier  EmailVerificationService
    welcomeMailer  WelcomeEmailService
    analyticsTrack AnalyticsService
    roleAssigner   RoleService
}

func NewOnboardingFacade(
    userRepo UserRepository,
    profile ProfileService,
    email EmailVerificationService,
    mailer WelcomeEmailService,
    analytics AnalyticsService,
    roles RoleService,
) *OnboardingFacade {
    return &OnboardingFacade{
        userRepo:       userRepo,
        profileService: profile,
        emailVerifier:  email,
        welcomeMailer:  mailer,
        analyticsTrack: analytics,
        roleAssigner:   roles,
    }
}

// RegisterUser mengorkestrasikan seluruh proses pendaftaran user baru.
// Client cukup memanggil satu method ini.
func (f *OnboardingFacade) RegisterUser(ctx context.Context, req RegistrationRequest) (*User, error) {
    // 1. Validasi input
    if err := req.Validate(); err != nil {
        return nil, fmt.Errorf("invalid registration data: %w", err)
    }

    // 2. Buat akun user
    user, err := f.userRepo.Create(ctx, req.Name, req.Email, hashPassword(req.Password))
    if err != nil {
        return nil, fmt.Errorf("failed to create account: %w", err)
    }

    // 3. Buat profil lengkap
    if err := f.profileService.Initialize(ctx, user.ID, req.ProfileData); err != nil {
        _ = f.userRepo.Delete(ctx, user.ID) // rollback
        return nil, fmt.Errorf("failed to initialize profile: %w", err)
    }

    // 4. Kirim email verifikasi
    verificationToken, err := f.emailVerifier.SendVerification(ctx, user.Email)
    if err != nil {
        // Non-fatal: akun tetap dibuat, verifikasi bisa di-retry
        log.Printf("warning: failed to send verification email to %s: %v", user.Email, err)
    }
    _ = verificationToken

    // 5. Assign role default
    _ = f.roleAssigner.AssignDefault(ctx, user.ID)

    // 6. Kirim welcome email (best-effort)
    go func() {
        _ = f.welcomeMailer.Send(context.Background(), user.Email, user.Name)
    }()

    // 7. Track event di analytics (best-effort)
    go func() {
        f.analyticsTrack.Track(context.Background(), "user_registered", map[string]string{
            "user_id": user.ID,
            "source":  req.RegistrationSource,
        })
    }()

    return user, nil
}

Rollback dan Error Handling di Facade #

Salah satu tanggung jawab terpenting Facade adalah mengelola rollback ketika salah satu langkah dalam orkestrasi gagal. Ini adalah kerumitan yang disembunyikan dari client.

flowchart TD
    A[Validate User] --> B{OK?}
    B -- Tidak --> FAIL1[Return Error]

    B -- Ya --> C[Reserve Inventory]
    C --> D{OK?}
    D -- Tidak --> FAIL2[Return Error]

    D -- Ya --> E[Calculate Discount]
    E --> F{OK?}
    F -- Tidak --> G[Release Reservation]
    G --> FAIL3[Return Error]

    F -- Ya --> H[Charge Payment]
    H --> I{OK?}
    I -- Tidak --> J[Release Reservation]
    J --> FAIL4[Return Error]

    I -- Ya --> K[Commit Inventory]
    K --> L{OK?}
    L -- Tidak --> M[Refund Payment]
    M --> FAIL5[Return Error]

    L -- Ya --> N[Create Order Record]
    N --> O[Send Notification]
    O --> P[Audit Log]
    P --> SUCCESS[Return Result ✓]

    style FAIL1 fill:#ff6b6b
    style FAIL2 fill:#ff6b6b
    style FAIL3 fill:#ff6b6b
    style FAIL4 fill:#ff6b6b
    style FAIL5 fill:#ff6b6b
    style SUCCESS fill:#51cf66

Kompleksitas alur rollback ini adalah salah satu alasan terkuat mengapa Facade dibutuhkan — tanpa Facade, setiap client harus mengimplementasikan alur ini sendiri, dan hampir pasti ada yang melewatkan rollback di suatu tempat.


Testing Facade #

Karena semua dependency Facade adalah interface, testing menjadi sangat bersih menggunakan mock.

// Mock untuk testing — implementasi sederhana yang mencatat panggilan
type MockInventoryService struct {
    CheckAvailabilityFn  func(ctx context.Context, items []OrderItem) error
    ReserveFn            func(ctx context.Context, items []OrderItem) (string, error)
    ReleaseReservationFn func(ctx context.Context, reservationID string) error
    CommitFn             func(ctx context.Context, reservationID string) error
    ReserveCallCount     int
    ReleaseCallCount     int
}

func (m *MockInventoryService) CheckAvailability(ctx context.Context, items []OrderItem) error {
    if m.CheckAvailabilityFn != nil {
        return m.CheckAvailabilityFn(ctx, items)
    }
    return nil
}

func (m *MockInventoryService) Reserve(ctx context.Context, items []OrderItem) (string, error) {
    m.ReserveCallCount++
    if m.ReserveFn != nil {
        return m.ReserveFn(ctx, items)
    }
    return "reservation-001", nil
}

func (m *MockInventoryService) ReleaseReservation(ctx context.Context, reservationID string) error {
    m.ReleaseCallCount++
    if m.ReleaseReservationFn != nil {
        return m.ReleaseReservationFn(ctx, reservationID)
    }
    return nil
}

func (m *MockInventoryService) Commit(ctx context.Context, reservationID string) error {
    if m.CommitFn != nil {
        return m.CommitFn(ctx, reservationID)
    }
    return nil
}


func buildTestFacade(inventory InventoryService, payment PaymentService) *CheckoutFacade {
    return NewCheckoutFacade(CheckoutFacadeConfig{
        UserValidator:  &MockUserValidator{},
        Inventory:      inventory,
        Discount:       &MockDiscountService{},
        Payment:        payment,
        Notification:   &MockNotificationService{},
        AuditLog:       &MockAuditLogger{},
        OrderRepo:      &MockOrderRepository{},
    })
}

func TestCheckoutFacade_Success(t *testing.T) {
    facade := buildTestFacade(
        &MockInventoryService{},
        &MockPaymentService{TransactionIDToReturn: "txn-123"},
    )

    req := CheckoutRequest{
        UserID: "user-1",
        Items:  []OrderItem{{ProductID: "prod-1", Quantity: 2, UnitPrice: 50000}},
    }

    result, err := facade.Checkout(context.Background(), req)

    if err != nil {
        t.Fatalf("expected no error, got: %v", err)
    }
    if result.TransactionID != "txn-123" {
        t.Errorf("unexpected transaction ID: %s", result.TransactionID)
    }
    if result.OrderID == "" {
        t.Error("expected non-empty order ID")
    }
}

func TestCheckoutFacade_ReleasesReservationWhenPaymentFails(t *testing.T) {
    inventory := &MockInventoryService{}
    payment := &MockPaymentService{
        ChargeFn: func(ctx context.Context, userID string, amount float64, method string) (string, error) {
            return "", fmt.Errorf("insufficient balance")
        },
    }

    facade := buildTestFacade(inventory, payment)

    _, err := facade.Checkout(context.Background(), CheckoutRequest{
        UserID: "user-1",
        Items:  []OrderItem{{ProductID: "prod-1", Quantity: 1, UnitPrice: 100000}},
    })

    if err == nil {
        t.Error("expected error when payment fails")
    }
    // Verifikasi rollback: reservasi harus dilepas
    if inventory.ReleaseCallCount != 1 {
        t.Errorf("expected ReleaseReservation to be called once, got %d", inventory.ReleaseCallCount)
    }
}

func TestCheckoutFacade_UserValidationFailure(t *testing.T) {
    facade := NewCheckoutFacade(CheckoutFacadeConfig{
        UserValidator: &MockUserValidator{
            ValidateFn: func(ctx context.Context, userID string) error {
                return fmt.Errorf("user is banned")
            },
        },
        Inventory:    &MockInventoryService{},
        Discount:     &MockDiscountService{},
        Payment:      &MockPaymentService{},
        Notification: &MockNotificationService{},
        AuditLog:     &MockAuditLogger{},
        OrderRepo:    &MockOrderRepository{},
    })

    _, err := facade.Checkout(context.Background(), CheckoutRequest{UserID: "banned-user"})

    if err == nil {
        t.Error("expected error for banned user")
    }
    // Verifikasi subsistem selanjutnya tidak dipanggil (tidak ada reservasi)
}

Facade Bukan God Object #

Kesalahan paling umum dengan Facade Pattern adalah membiarkannya berkembang menjadi God Object — satu struct yang tahu dan melakukan segalanya.

// ANTI-PATTERN: Facade yang berisi business logic
func (f *CheckoutFacade) Checkout(ctx context.Context, req CheckoutRequest) (*CheckoutResult, error) {
    // Logic diskon langsung di Facade — salah tempat
    var discount float64
    if req.CouponCode == "SALE50" {
        discount = req.TotalPrice * 0.5
    } else if req.CouponCode == "NEWUSER" {
        discount = 10000
    }
    finalPrice := req.TotalPrice - discount

    // Logic validasi langsung di Facade — salah tempat
    if finalPrice < 0 {
        return nil, errors.New("negative price not allowed")
    }

    // Facade makin besar dan makin sulit di-test
}

// BENAR: Facade mendelegasikan semua logic ke subsistem yang bertanggung jawab
func (f *CheckoutFacade) Checkout(ctx context.Context, req CheckoutRequest) (*CheckoutResult, error) {
    // Diskon? → DiscountService yang tahu caranya
    finalPrice, err := f.discount.Calculate(ctx, req.UserID, req.CouponCode, req.Items)
    if err != nil {
        return nil, fmt.Errorf("discount calculation failed: %w", err)
    }
    // Facade tidak tahu algoritma diskon sama sekali
}

Panduan memastikan Facade tetap bersih:

Facade BOLEH:
  ✓ Memanggil subsistem dalam urutan tertentu
  ✓ Menangani error dan mengorkestrasikan rollback
  ✓ Menentukan apakah langkah tertentu bersifat fatal atau best-effort
  ✓ Meneruskan data dari satu subsistem ke subsistem berikutnya

Facade TIDAK BOLEH:
  ✗ Berisi kalkulasi bisnis (harga, diskon, pajak)
  ✗ Berisi validasi domain (apakah email valid, apakah quantity positif)
  ✗ Mengakses database secara langsung
  ✗ Berisi if/else yang berbasis aturan bisnis

Facade vs Pattern Lain #

Facade sering dikacaukan dengan Adapter dan Mediator karena ketiganya berkaitan dengan menghubungkan komponen.

Aspek Facade Adapter Mediator
Tujuan Sederhanakan subsistem kompleks Sesuaikan interface yang tidak kompatibel Koordinasikan komunikasi antar komponen
Arah komunikasi Satu arah: client → subsistem Satu arah: client → adaptee Dua arah: komponen ↔ mediator ↔ komponen
Interface baru? Ya — lebih sederhana dari subsistem Ya — sesuai dengan yang diharapkan client Ya — terpusat di mediator
Subsistem tahu Facade? Tidak Tidak Komponen tahu mediator
Contoh CheckoutFacade MidtransAdapter EventBus, ChatRoom

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Facade jika:
  ✓ Ada beberapa subsistem yang harus dipanggil dalam urutan tertentu untuk satu use-case
  ✓ Client code penuh dengan detail teknis yang seharusnya tidak ia ketahui
  ✓ Logic orkestrasi yang sama diulang di banyak tempat (handler, job, CLI)
  ✓ Ingin menyembunyikan sistem legacy di balik API yang lebih modern
  ✓ Sedang membangun SDK atau library untuk digunakan tim lain

HINDARI Facade jika:
  ✗ Hanya ada satu atau dua subsistem — tidak cukup kompleks untuk dijustifikasi
  ✗ Setiap client butuh orkestrasi yang sangat berbeda — Facade tidak bisa melayani semua
  ✗ Facade mulai berisi business logic — refactor logika itu ke subsistem yang tepat
  ✗ Facade tumbuh menjadi God Object — pertanda sudah terlalu banyak tanggung jawab

Facade di Application Service Layer

Di arsitektur berlapis, Application Service atau Use Case class sering berfungsi sebagai Facade — mereka mengorkestrasikan domain objects, repository, dan infrastructure services untuk mengeksekusi satu use case. Jika kamu sudah mengikuti Clean Architecture atau Hexagonal Architecture, kamu sudah menggunakan Facade Pattern meski mungkin tidak menamakannya demikian.


Checklist Review Facade #

DESAIN:
  □ Facade menyediakan method yang sesuai dengan use case bisnis, bukan dengan subsistem teknis
  □ Semua dependency Facade adalah interface — tidak ada concrete type di constructor
  □ Facade tidak berisi business logic — hanya orkestrasi dan error handling
  □ Rollback diimplementasikan dengan benar untuk setiap langkah yang bisa gagal

IMPLEMENTASI:
  □ Notifikasi dan operasi non-kritis dijalankan sebagai goroutine (best-effort)
  □ Context di-propagate ke semua subsistem
  □ Error dari subsistem di-wrap dengan pesan konteks yang informatif
  □ Operasi yang idempotent ditandai dengan jelas

UKURAN FACADE:
  □ Facade tidak memiliki lebih dari 5-7 method publik
  □ Setiap method Facade bisa dijelaskan dalam satu kalimat tanpa menyebut "dan"
  □ Tidak ada state internal yang berubah setelah inisialisasi

TESTING:
  □ Semua subsistem di-mock melalui interface
  □ Skenario rollback di-test (payment gagal → inventory direlease)
  □ Skenario best-effort di-test (notifikasi gagal tidak menggagalkan checkout)
  □ Happy path dan semua failure path di-test

Ringkasan #

  • Facade menyembunyikan kompleksitas, bukan menghilangkannya — subsistem tetap kompleks; Facade hanya memastikan client tidak perlu tahu tentang kerumitan itu.
  • Satu titik masuk untuk satu use caseCheckout(), RegisterUser(), ProcessRefund() adalah contoh API Facade yang fokus pada apa yang dibutuhkan client, bukan pada apa yang tersedia di subsistem.
  • Facade hanya mengorkestrasikan, tidak mengimplementasikan — business logic harus tetap di subsistem masing-masing; Facade hanya mengatur urutan pemanggilan dan rollback.
  • Rollback adalah tanggung jawab Facade — salah satu nilai terbesar Facade adalah memusatkan logika rollback di satu tempat, menghindari bug di mana client lupa melepas resource setelah kegagalan.
  • Semua dependency harus interface — ini yang memungkinkan unit testing yang bersih; tanpa interface, testing Facade membutuhkan semua subsistem nyata.
  • Best-effort operations sebagai goroutine — notifikasi, analytics tracking, dan operasi non-kritis yang tidak boleh menggagalkan transaksi utama bisa dijalankan secara async.
  • Facade adalah Application Service — di Clean Architecture dan Hexagonal Architecture, Application Service layer menjalankan peran yang sama persis; ini bukan kebetulan.
  • Waspada God Object — jika Facade mulai punya lebih dari 7 method atau mulai berisi if/else berbasis aturan bisnis, itu sinyal bahwa Facade sudah terlalu besar dan perlu dipecah.

← Sebelumnya: Decorator   Berikutnya: Flyweight →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact