Ports & Adapters (Hexagonal Architecture) — Design Patterns

Hexagonal Architecture (alias Ports & Adapters) membuat business logic tidak bergantung pada framework, database, atau UI. Logic inti (domain) berinteraksi…

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
    },
  }
}

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

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:

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:

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

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.

Yang akan kamu pelajari