Ports & Adapters, juga disebut Hexagonal Architecture, menaruh logika bisnis (core) di tengah, dan semua yang ada di luarnya (database, server API, SMS, WhatsApp, tampilan) di pinggir. Core bicara ke luar cuma lewat port: daftar method yang core butuhkan, ditulis dari sudut pandang core. Tiap teknologi luar dipasang lewat adapter yang memenuhi port itu. Bayangin laptop yang kamu bawa ke luar negeri. Laptopnya punya satu port pengisian, dan di Indonesia, Jepang, atau Inggris kamu cukup mengganti adaptor ke stop kontak setempat. Laptopnya gak perlu dibongkar. Mau mengetes baterainya di meja? Colokkan power bank ke port yang sama.
🧪 Tebakan cepat: Kurir Kancil pindah dari SMS ke WhatsApp untuk pemberitahuan. Di versi ports & adapters, berapa baris di dalam
createBooking(core) yang berubah? Tebak dulu, jawabannya ada di bagian "Ganti SMS ke WhatsApp".
Masalahnya: core yang kenal vendor
Core pemesanan Kurir Kancil menghitung ongkir, mengambil resi, menyimpan kiriman, lalu memberi tahu pengirim. Versi pertamanya memanggil server dan vendor SMS langsung:
const TARIF = { reguler: 10000, kilat: 18000, sameday: 25000, kargo: 15000 } // per kg
function createBooking() {
return {
pesan({ penerima, hp, service, kg }) {
if (!Object.hasOwn(TARIF, service)) throw new Error("Unknown service: " + service)
const resi = api.post("/resi").resi // core kenal server API
const shipment = { resi, penerima, hp, service, kg, ongkir: kg * TARIF[service], status: "dijemput" }
api.put("/shipments/" + resi, shipment)
smsGateway.send({ to: hp, text: resi + " dijemput, ongkir Rp " + shipment.ongkir }) // core kenal vendor SMS
return shipment
},
}
}
Aturan ongkirnya cuma satu baris, tapi untuk mengetesnya kamu butuh server API yang jalan, dan tiap test mengirim SMS sungguhan. Pindah vendor berarti mengubah isi pesan, tempat aturan bisnisnya tinggal.
Port: kebutuhan core, ditulis dari sudut pandang core
// core/booking.js: gak ada SMS, WhatsApp, server, atau database di sini
const TARIF = { reguler: 10000, kilat: 18000, sameday: 25000, kargo: 15000 } // per kg
// Port, ditulis dari kebutuhan core:
// ShipmentStore: nextResi(), save(shipment)
// NotificationPort: kirim(hp, pesan)
function createBooking({ store, notifier }) {
return {
pesan({ penerima, hp, service, kg }) {
if (!Object.hasOwn(TARIF, service)) throw new Error("Unknown service: " + service)
const resi = store.nextResi()
const shipment = { resi, penerima, hp, service, kg, ongkir: kg * TARIF[service], status: "dijemput" }
store.save(shipment)
notifier.kirim(hp, resi + " dijemput, ongkir Rp " + shipment.ongkir)
return shipment
},
}
}
- Port adalah kontrak yang dibutuhkan core:
ShipmentStore(ambil resi baru, simpan kiriman) danNotificationPort(kirim pesan ke nomor HP). JavaScript gak punyainterface, jadi port-nya ditulis sebagai komentar; di TypeScript, port biasanya ditulis sebagaiinterface. - Port ditulis dengan bahasa core:
kirim(hp, pesan), bukansendMessage(to, body)milik WhatsApp atausend({ to, text })milik vendor SMS. createBookingmenerimastoredannotifierdari luar. Dia gak tahu siapa mereka, cuma tahu mereka memenuhi port-nya.
Adapter: memasang teknologi luar ke port
// Adapter untuk ShipmentStore
function createApiStore(api) {
return {
nextResi: () => api.post("/resi").resi,
save: (s) => api.put("/shipments/" + s.resi, s),
}
}
function createInMemoryStore() {
const rows = new Map()
let n = 0
return {
nextResi: () => "KCL-" + String(++n).padStart(4, "0"),
save: (s) => rows.set(s.resi, s),
}
}
// Adapter untuk NotificationPort
const smsAdapter = { kirim: (hp, pesan) => smsGateway.send({ to: hp, text: pesan }) }
const waAdapter = { kirim: (hp, pesan) => waApi.sendMessage(hp.replace(/^0/, "62"), pesan) } // WhatsApp minta 62, bukan 0
// main.js: satu-satunya tempat yang memilih adapter
const booking = createBooking({ store: createApiStore(api), notifier: smsAdapter })
booking.pesan({ penerima: "Bu Sari", hp: "081234567890", service: "kilat", kg: 2 })
// POST /resi
// PUT /shipments/KCL-0001
// SMS gateway → 081234567890: KCL-0001 dijemput, ongkir Rp 36000
- Tiap adapter menerjemahkan panggilan core ke teknologi tertentu. Ini Adapter Pattern dari lesson Adapter, dipasang di batas aplikasi.
createApiStorejuga adalah repository dari lesson Repository. main.jsadalah tempat merakit (composition root): cuma di sini adapter dipilih dan dititipkan ke core.- Detail milik vendor tinggal di adapter-nya. Format nomor
62...yang diminta WhatsApp diuruswaAdapter, bukan core.
Ganti SMS ke WhatsApp
Satu baris di main.js berubah: notifier: smsAdapter jadi notifier: waAdapter. Di dalam createBooking: 0 baris berubah. Itu jawaban tebakannya. Pesanan yang sama sekarang mengirim WhatsApp API → 6281234567890: KCL-0001 dijemput, ongkir Rp 36000. Di versi tanpa port, perpindahan ini mengubah isi pesan, dan tiap test core ikut memanggil WhatsApp API.
Mengetes core tanpa adapter asli
const terkirim = []
const booking = createBooking({
store: createInMemoryStore(),
notifier: { kirim: (hp, pesan) => terkirim.push(pesan) }, // fake: cuma mencatat
})
const s = booking.pesan({ penerima: "Bu Sari", hp: "081234567890", service: "kilat", kg: 2 })
console.log(s.resi, s.ongkir) // KCL-0001 36000
console.log(terkirim[0]) // KCL-0001 dijemput, ongkir Rp 36000
Gak ada server, gak ada vendor, 0 request ke luar, dan test-nya selesai dalam hitungan milidetik. Itu keuntungan terbesar port untuk testing: aturan bisnis dites dengan fake atau penyimpanan in-memory yang memenuhi port yang sama. Adapter aslinya dites terpisah, lebih jarang, terhadap layanan sungguhan.
Arah ketergantungan
Alistair Cockburn menulis pola ini pada 2005 dengan nama Hexagonal Architecture. Bentuk segi enamnya cuma memberi ruang untuk menggambar banyak port; angka enamnya gak punya arti. Aturan utamanya soal arah:
- Core gak bergantung pada apa pun di luar. Gak ada
importframework, database, atau SDK vendor di core. - Adapter bergantung pada core, karena adapter yang menyesuaikan diri dengan port milik core.
- Wiring di pinggir: adapter dipilih di
main.js, bukan di dalam core.
Di sisi lain core ada adapter yang memanggil core: controller HTTP, perintah CLI, atau test. Mereka semua memanggil booking.pesan(...) yang sama, jadi aturan ongkir gak ditulis ulang untuk tiap pintu masuk. Lesson berikutnya, Dependency Injection, membahas cara menitipkan adapter seperti ini dengan rapi.
Kapan berlebihan
Prototype seminggu, script sekali jalan, atau CRUD tanpa logika bisnis gak butuh port: lapisannya menambah kode tanpa ada yang dilindungi. Port terbayar saat logika bisnisnya hidup lama, perlu dites cepat, atau vendornya mungkin berganti (SMS, payment gateway, database).
Latihan: pendaftaran user dengan dua port
Latihan nanti memakai pola yang sama untuk pendaftaran user, dengan tiga function:
createInMemoryUserRepo(): adapter penyimpanan. Kembalikan{ findById, save, all }:findById(id)mengembalikan user ataunull,save(user)menyimpan,all()mengembalikan array semua user.createFakeEmailer(): adapter email palsu. Kembalikan{ sent, send }:send(id, msg)push{ id, msg }ke arraysent.createRegisterUserService(userRepo, emailer): core-nya. Kembalikan functionregister(id, name)yang mengecek duluuserRepo.findById(id)(kalau ada,throw new Error("User exists")), laluuserRepo.save({ id, name }), laluemailer.send(id, "Welcome " + name), dan mengembalikan user-nya.
Console-nya jadi Budi, 1, u1, Welcome Budi, 2, User exists, lalu 1 dari service kedua dengan adapter baru, tanpa mengubah createRegisterUserService.
⚠️ Jebakan yang sering ketemu
- Port yang mengikuti vendor. Core yang memanggil
notifier.sendMessage(...)milik WhatsApp berhenti denganTypeError: notifier.sendMessage is not a functionsaat adapter SMS dipasang. Tulis port dari kebutuhan core. - Core yang memilih adapter sendiri.
createBookingyang menulissmsAdapterdi dalamnya gak bisa diberi fake. Titipkan darimain.js. - Satu
importvendor "sekali ini saja" di core. Core-nya kembali butuh vendor itu untuk dites. - Di latihan: menyimpan sebelum mengecek.
findByIdlalu menemukan user yang baru disimpan, dan semua pendaftaran ditolak denganUser exists.
TL;DR: Ports & Adapters (Hexagonal Architecture, Cockburn 2005) menaruh logika bisnis di core yang gak bergantung pada apa pun di luar. Core bicara lewat port yang ia definisikan sendiri (ShipmentStore: nextResi, save; NotificationPort: kirim(hp, pesan)), dan tiap teknologi dipasang lewat adapter (smsAdapter, waAdapter, createApiStore, createInMemoryStore) yang dipilih di main.js. Ganti SMS ke WhatsApp: 0 baris core berubah. Core dites dengan fake dan penyimpanan in-memory, tanpa server dan tanpa request ke luar. Semua arah ketergantungan menuju core. Untuk prototype, script sekali jalan, atau CRUD tanpa logika, port berlebihan. Di latihan: cek duplikat dulu, simpan, kirim email, dan service yang sama bekerja dengan adapter baru.