Implementasi Facade Pattern di Proyek Nyata: Studi Kasus API Gateway Sederhana
Pendahuluan
Bayangkan kamu sedang membangun aplikasi e-commerce seperti Tokopedia — ada layanan produk, layanan pembayaran, layanan notifikasi, layanan inventaris, dan banyak lagi. Setiap layanan punya cara komunikasi sendiri, format data yang berbeda, dan endpoint yang tersebar di mana-mana. Kalau frontend langsung memanggil semua layanan itu satu per satu, kode kamu akan cepat menjadi mimpi buruk.
Di sinilah Facade Pattern hadir sebagai penyelamat. Artikel ini akan membahas bagaimana mengimplementasikan Facade Pattern secara nyata, khususnya untuk membangun API Gateway sederhana yang menyederhanakan kompleksitas sistem microservices.
Apa Itu Facade Pattern?
Facade Pattern adalah salah satu structural design pattern yang menyediakan antarmuka sederhana untuk kumpulan subsistem yang kompleks. Kata “facade” sendiri berasal dari bahasa Prancis yang berarti “tampak muka bangunan” — yaitu sisi yang kamu lihat dari luar, bukan seluruh struktur internal di dalamnya.
Analoginya seperti memesan kopi di kedai kopi. Kamu cukup bilang “satu latte” ke kasir — kamu tidak perlu tahu bahwa di balik layar ada proses grinding biji kopi, mengukur takaran susu, memanaskan mesin espresso, dan lain-lain. Kasir adalah facade-nya.
Tiga komponen utama Facade Pattern:
- Facade — kelas antarmuka tunggal yang diekspos ke klien
- Subsystems — kelas-kelas kompleks yang bekerja di balik layar
- Client — kode yang hanya berinteraksi dengan Facade
Memahami Masalah: Kompleksitas Sistem Microservices
Perhatikan skenario berikut: frontend memanggil tiga layanan berbeda hanya untuk menampilkan halaman checkout.
// Tanpa Facade — frontend harus tahu semua endpoint
const user = await fetch('http://user-service:3001/api/users/123');
const cart = await fetch('http://cart-service:3002/api/cart/123');
const inventory = await fetch('http://inventory-service:3003/api/stock');
const shipping = await fetch('http://shipping-service:3004/api/rates');
// Lalu harus menggabungkan datanya sendiri...
const userData = await user.json();
const cartData = await cart.json();
// dan seterusnya...
Masalah yang muncul:
- Frontend tightly coupled ke semua internal service
- Jika satu service ganti URL atau format, semua klien harus diupdate
- Tidak ada satu titik untuk autentikasi, logging, atau rate limiting
- Latency bertambah karena banyak round-trip dari browser
Solusinya: buat satu API Gateway yang bertindak sebagai Facade untuk semua microservices itu.
Demo Mandiri: Facade Pattern Tanpa Server Eksternal
Sebelum masuk ke implementasi penuh, mari kita pahami konsepnya dengan contoh yang bisa langsung kamu jalankan di lokal tanpa perlu microservice sungguhan.
# demo_facade.py — jalankan langsung: python demo_facade.py
import asyncio
from unittest.mock import AsyncMock
# --- Simulasi Subsystem (mock tanpa server nyata) ---
class UserService:
async def get_user(self, user_id: str) -> dict:
# Di produksi ini akan memanggil http://user-service:3001
return {"id": user_id, "name": "Budi Santoso", "email": "[email protected]"}
class CartService:
async def get_cart(self, user_id: str) -> dict:
return {
"cart_id": "cart-001",
"items": [
{"id": "item-1", "name": "Laptop", "weight": 2.5, "price": 8000000},
{"id": "item-2", "name": "Mouse", "weight": 0.2, "price": 150000},
]
}
async def get_total(self, cart_id: str) -> float:
return 8150000.0
class InventoryService:
async def check_availability(self, item_ids: list) -> dict:
return {item_id: True for item_id in item_ids}
class ShippingService:
async def get_shipping_rates(self, destination: str, weight: float) -> list:
return [
{"courier": "JNE", "price": 25000, "eta": "2-3 hari"},
{"courier": "SiCepat", "price": 20000, "eta": "1-2 hari"},
]
# --- Facade ---
class CheckoutFacade:
"""Klien hanya perlu tahu class ini — semua kompleksitas tersembunyi di sini."""
def __init__(self):
self.user_service = UserService()
self.cart_service = CartService()
self.inventory_service = InventoryService()
self.shipping_service = ShippingService()
async def prepare_checkout(self, user_id: str, destination: str) -> dict:
# Panggil user dan cart secara paralel
user_data, cart_data = await asyncio.gather(
self.user_service.get_user(user_id),
self.cart_service.get_cart(user_id)
)
cart_id = cart_data["cart_id"]
item_ids = [item["id"] for item in cart_data["items"]]
total_weight = sum(item["weight"] for item in cart_data["items"])
# Panggil tiga service sekaligus secara paralel
availability, shipping_rates, total_price = await asyncio.gather(
self.inventory_service.check_availability(item_ids),
self.shipping_service.get_shipping_rates(destination, total_weight),
self.cart_service.get_total(cart_id)
)
return {
"user": {"name": user_data["name"], "email": user_data["email"]},
"cart": {"items": cart_data["items"], "total_price": total_price},
"availability": availability,
"shipping_options": shipping_rates
}
# --- Client ---
async def main():
facade = CheckoutFacade()
result = await facade.prepare_checkout(user_id="123", destination="Jakarta")
print("=== Hasil Checkout ===")
print(f"Pelanggan : {result['user']['name']} ({result['user']['email']})")
print(f"Total Harga: Rp {result['cart']['total_price']:,.0f}")
print(f"Ketersediaan Barang: {result['availability']}")
print("Opsi Pengiriman:")
for opt in result['shipping_options']:
print(f" - {opt['courier']}: Rp {opt['price']:,} ({opt['eta']})")
asyncio.run(main())
Output yang dihasilkan:
=== Hasil Checkout ===
Pelanggan : Budi Santoso ([email protected])
Total Harga: Rp 8,150,000
Ketersediaan Barang: {'item-1': True, 'item-2': True}
Opsi Pengiriman:
- JNE: Rp 25,000 (2-3 hari)
- SiCepat: Rp 20,000 (1-2 hari)
Perhatikan bahwa main() (client) hanya memanggil satu method: prepare_checkout(). Seluruh koordinasi antar-service sepenuhnya tersembunyi di dalam CheckoutFacade.
Implementasi API Gateway dengan Facade Pattern
Setelah paham konsepnya, kita lanjutkan ke implementasi production-ready menggunakan Python dengan framework FastAPI. Struktur proyeknya:
api-gateway/
├── main.py
├── facade/
│ └── checkout_facade.py
├── services/
│ ├── user_service.py
│ ├── cart_service.py
│ ├── inventory_service.py
│ └── shipping_service.py
└── models/
└── checkout.py
Langkah 1: Buat masing-masing subsystem (service client)
# services/user_service.py
import httpx
class UserService:
BASE_URL = "http://user-service:3001"
async def get_user(self, user_id: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(f"{self.BASE_URL}/api/users/{user_id}")
response.raise_for_status()
return response.json()
async def validate_token(self, token: str) -> bool:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.BASE_URL}/api/auth/validate",
json={"token": token}
)
return response.status_code == 200
# services/cart_service.py
import httpx
class CartService:
BASE_URL = "http://cart-service:3002"
async def get_cart(self, user_id: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(f"{self.BASE_URL}/api/cart/{user_id}")
response.raise_for_status()
return response.json()
async def get_total(self, cart_id: str) -> float:
async with httpx.AsyncClient() as client:
response = await client.get(f"{self.BASE_URL}/api/cart/{cart_id}/total")
response.raise_for_status()
data = response.json()
return data["total"]
# services/inventory_service.py
import httpx
from typing import List
class InventoryService:
BASE_URL = "http://inventory-service:3003"
async def check_availability(self, item_ids: List[str]) -> dict:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.BASE_URL}/api/stock/check",
json={"item_ids": item_ids}
)
response.raise_for_status()
return response.json()
# services/shipping_service.py
import httpx
class ShippingService:
BASE_URL = "http://shipping-service:3004"
async def get_shipping_rates(self, destination: str, weight: float) -> list:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.BASE_URL}/api/rates",
params={"destination": destination, "weight": weight}
)
response.raise_for_status()
return response.json()["rates"]
Langkah 2: Bangun Facade-nya
# facade/checkout_facade.py
import asyncio
from services.user_service import UserService
from services.cart_service import CartService
from services.inventory_service import InventoryService
from services.shipping_service import ShippingService
class CheckoutFacade:
"""
Facade yang menyederhanakan proses checkout.
Klien cukup memanggil satu method tanpa tahu detail masing-masing service.
"""
def __init__(self):
self.user_service = UserService()
self.cart_service = CartService()
self.inventory_service = InventoryService()
self.shipping_service = ShippingService()
async def prepare_checkout(self, user_id: str, destination: str) -> dict:
"""
Satu method yang mengkoordinasikan semua subsystem sekaligus.
"""
# Jalankan request paralel untuk efisiensi
user_data, cart_data = await asyncio.gather(
self.user_service.get_user(user_id),
self.cart_service.get_cart(user_id)
)
cart_id = cart_data["cart_id"]
item_ids = [item["id"] for item in cart_data["items"]]
total_weight = sum(item["weight"] for item in cart_data["items"])
# Cek stok dan ongkos kirim secara paralel
availability, shipping_rates, total_price = await asyncio.gather(
self.inventory_service.check_availability(item_ids),
self.shipping_service.get_shipping_rates(destination, total_weight),
self.cart_service.get_total(cart_id)
)
# Gabungkan semua data menjadi satu response bersih
return {
"user": {
"name": user_data["name"],
"email": user_data["email"]
},
"cart": {
"items": cart_data["items"],
"total_price": total_price
},
"availability": availability,
"shipping_options": shipping_rates
}
Langkah 3: Gunakan Facade di endpoint API Gateway
# main.py
import httpx
from fastapi import FastAPI, HTTPException, Header
from facade.checkout_facade import CheckoutFacade
app = FastAPI(title="API Gateway - KamusNgoding Demo")
checkout_facade = CheckoutFacade()
@app.get("/api/checkout/prepare/{user_id}")
async def prepare_checkout(
user_id: str,
destination: str,
authorization: str = Header(...)
):
"""
Klien hanya perlu hit satu endpoint ini.
Semua kompleksitas tersembunyi di balik Facade.
"""
try:
result = await checkout_facade.prepare_checkout(user_id, destination)
return {"success": True, "data": result}
except httpx.HTTPStatusError as e:
raise HTTPException(
status_code=e.response.status_code,
detail=f"Service error: {str(e)}"
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Contoh response JSON dari endpoint di atas:
{
"success": true,
"data": {
"user": {
"name": "Budi Santoso",
"email": "[email protected]"
},
"cart": {
"items": [
{"id": "item-1", "name": "Laptop", "weight": 2.5, "price": 8000000},
{"id": "item-2", "name": "Mouse", "weight": 0.2, "price": 150000}
],
"total_price": 8150000.0
},
"availability": {"item-1": true, "item-2": true},
"shipping_options": [
{"courier": "JNE", "price": 25000, "eta": "2-3 hari"},
{"courier": "SiCepat", "price": 20000, "eta": "1-2 hari"}
]
}
}
Contoh Kasus Nyata
Jika ingin membangun layanan seperti Gojek dengan puluhan microservices, Facade Pattern sangat berguna untuk fitur seperti “Order Summary” — satu layar yang membutuhkan data dari service rider, service harga, service promo, dan service peta secara bersamaan.
Dengan pola yang sama seperti di atas, kamu bisa membuat OrderSummaryFacade yang memanggil semua service itu di belakang layar, lalu mengembalikan satu objek bersih ke aplikasi mobile. Jika suatu hari service promo berganti format API-nya, kamu cukup update satu file di Facade tanpa menyentuh kode mobile sama sekali.
Untuk pengelolaan konfigurasi service seperti URL, token, dan environment variable-nya, kamu bisa mempelajari cara terstruktur dari Secrets vs Variables di GitHub Actions: Perbedaan dan Kapan Menggunakannya — konsep pemisahan konfigurasi sensitif dari konfigurasi biasa sangat relevan di sini.
Kamu juga bisa otomasi deployment API Gateway-mu menggunakan CI/CD. Artikel Memahami Komponen Dasar GitHub Actions: Workflow, Job, dan Step bisa jadi panduan yang tepat untuk memulainya.
Troubleshooting: Error yang Sering Muncul
httpx.ConnectError: All connection attempts failed
Penyebab: Facade mencoba menghubungi subsystem service yang belum berjalan atau URL-nya salah. Ini sering terjadi saat development lokal atau saat environment variable tidak di-set dengan benar.
Solusi:
# services/user_service.py
# Tambahkan timeout dan retry logic di setiap service client
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
class UserService:
BASE_URL = "http://user-service:3001"
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=4))
async def get_user(self, user_id: str) -> dict:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(f"{self.BASE_URL}/api/users/{user_id}")
response.raise_for_status()
return response.json()
asyncio.TimeoutError saat Facade memanggil banyak service sekaligus
Penyebab: Salah satu subsystem lambat merespons dan menyebabkan seluruh asyncio.gather() tertahan. Tanpa timeout eksplisit, seluruh request bisa hang selamanya.
Solusi:
# facade/checkout_facade.py
import asyncio
async def prepare_checkout(self, user_id: str, destination: str) -> dict:
try:
# Bungkus gather dengan timeout global
result = await asyncio.wait_for(
asyncio.gather(
self.user_service.get_user(user_id),
self.cart_service.get_cart(user_id)
),
timeout=5.0 # maksimal 5 detik untuk semua service
)
user_data, cart_data = result
return {"user": user_data, "cart": cart_data}
except asyncio.TimeoutError:
raise Exception("Satu atau lebih service tidak merespons tepat waktu")
KeyError atau TypeError saat menggabungkan response dari subsystem
Penyebab: Format response dari salah satu service berubah (misal field "total" berganti menjadi "grand_total"), tapi Facade belum diupdate. Ini risiko umum di sistem microservices yang berkembang.
Solusi:
# models/checkout.py
# Gunakan Pydantic untuk validasi dan tangani perubahan field
from pydantic import BaseModel
from typing import Optional
class CartResponse(BaseModel):
cart_id: str
items: list
total: Optional[float] = None
grand_total: Optional[float] = None # tangani perubahan field nama
@property
def final_total(self) -> float:
# Fallback logic jika field berubah di service lain
return self.total or self.grand_total or 0.0
# Penggunaan di facade:
# cart_response = CartResponse(**raw_cart_data)
# total = cart_response.final_total # aman walau field berubah
Pertanyaan yang Sering Diajukan
Apa perbedaan Facade Pattern dengan Adapter Pattern?
Facade menyederhanakan antarmuka dari sekumpulan subsystem menjadi satu antarmuka tunggal yang lebih mudah digunakan. Adapter, di sisi lain, mengubah antarmuka satu objek agar kompatibel dengan antarmuka lain yang sudah ada. Singkatnya: Facade tentang menyederhanakan banyak hal, Adapter tentang mengubah satu hal agar cocok.
Apakah Facade Pattern membuat sistem menjadi monolitik?
Tidak. Facade hanya menyederhanakan cara mengakses subsystem, bukan menggabungkan subsystem itu sendiri menjadi satu unit. Setiap service tetap independen dan bisa di-deploy secara terpisah. Facade hanyalah lapisan koordinasi di atasnya.
Bagaimana cara menangani autentikasi di API Gateway berbasis Facade?
Tempatkan logika autentikasi di layer Facade atau sebagai middleware sebelum Facade dipanggil, bukan di masing-masing subsystem. Dengan begitu, setiap service internal tidak perlu mengurus token validation secara berulang.
# main.py
# Contoh middleware autentikasi di FastAPI
from fastapi import Depends, HTTPException, Header
from services.user_service import UserService
async def verify_token(authorization: str = Header(...)):
user_service = UserService()
is_valid = await user_service.validate_token(authorization)
if not is_valid:
raise HTTPException(status_code=401, detail="Token tidak valid")
@app.get("/api/checkout/prepare/{user_id}", dependencies=[Depends(verify_token)])
async def prepare_checkout(user_id: str, destination: str):
result = await checkout_facade.prepare_checkout(user_id, destination)
return {"success": True, "data": result}
Kapan sebaiknya TIDAK menggunakan Facade Pattern?
Jika sistemmu hanya memiliki satu atau dua subsystem yang sederhana, menambahkan Facade justru menambah lapisan yang tidak perlu. Facade paling berguna ketika kamu memiliki tiga subsystem atau lebih dengan interaksi yang kompleks dan klien yang perlu dilindungi dari kompleksitas tersebut.
Apakah Facade Pattern bisa dikombinasikan dengan pattern lain?
Sangat bisa. Facade sering dikombinasikan dengan Singleton (agar hanya ada satu instance Facade) dan Factory (untuk membuat instance service yang tepat berdasarkan environment). Di sistem yang lebih besar, Facade bisa menggunakan data yang di-cache dengan Proxy Pattern untuk mengurangi beban ke subsystem.
Kesimpulan
Facade Pattern adalah salah satu pattern yang paling praktis dan langsung terasa manfaatnya di proyek nyata. Dengan membangun API Gateway sederhana menggunakan Facade, kamu berhasil:
- Menyembunyikan kompleksitas komunikasi antar microservices dari klien
- Membuat satu titik koordinasi untuk autentikasi, logging, dan error handling
- Mengurangi coupling antara frontend dan internal services
- Memudahkan perubahan internal service tanpa berdampak ke klien
Pola ini sangat relevan jika kamu ingin membangun sistem berskala besar seperti platform ride-hailing atau marketplace — di mana puluhan service harus berkoordinasi untuk menghasilkan satu respons yang kohesif ke pengguna.
Selamat belajar dan terus eksplorasi! Jika ada pertanyaan atau kamu ingin mencoba implementasi dengan stack yang berbeda, jangan ragu untuk menjelajahi artikel-artikel lainnya di KamusNgoding — banyak konsep menarik yang menunggumu.