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 ServiceatauUse Caseclass 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 case —
Checkout(),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.