Menurunkan arsitektur ke level kode yang sehat: prinsip SOLID beserta contoh nyata, pattern GoF yang paling relevan bagi architect, dan domain-driven design sebagai alat menemukan boundary — ditutup praktik memetakan bounded context studi kasus e-commerce

Setelah di episode 3 kalian memilih architectural style — untuk studi kasus kita: modular monolith dengan komunikasi event di bagian non-kritis — pada episode ini kita turun satu level ke design patterns & principles: fondasi yang membuat struktur besar itu tetap sehat sampai ke dalam kode.
Mengapa architect perlu peduli level ini? Karena arsitektur yang baik tidak bisa berdiri di atas kode yang busuk. Modul dengan boundary indah di diagram tetapi dipenuhi dependency acak di dalamnya akan tetap menghasilkan sistem yang sulit diubah. Sebaliknya, pemahaman pattern & principles adalah bahasa bersama antara architect dan tim implementasi — ketika kalian berkata "pisahkan lewat adapter", semua orang tahu apa maksudnya.
SOLID dirumuskan Robert C. Martin sebagai lima prinsip desain berorientasi objek. Di tingkat architect, kalian tidak menghafalnya — kalian menggunakannya sebagai alat diagnosis saat review desain.
Satu class/modul punya satu alasan untuk berubah. Kelas OrderService yang sekaligus menghitung pajak, memanggil payment gateway, dan mengirim email akan berubah karena tiga alasan berbeda — dan itu sumber konflik antar tim.
Terbuka untuk ekstensi, tertutup untuk modifikasi. Menambah metode pembayaran baru idealnya menambah class baru, bukan mengedit rantai if/else di tengah kode pembayaran.
Subclass harus bisa menggantikan parent tanpa merusak perilaku. Pelanggaran klasik: Penguin extends Bird lalu melempar error di method fly(). Di level service: API versi baru harus bisa dipakai oleh client lama tanpa surprise.
Jangan paksa client bergantung pada method yang tidak ia perlukan. Interface Repository dengan 20 method generik lebih buruk daripada beberapa interface kecil yang spesifik per use case.
Modul high-level tidak boleh bergantung pada detail low-level; keduanya bergantung pada abstraksi. Ini prinsip yang paling sering muncul di diskusi arsitektur:
interface PaymentGateway {
charge(orderId: string, amountCents: number): Promise<PaymentResult>
}
// business logic hanya kenal interface
export class CheckoutService {
constructor(private gateway: PaymentGateway) {}
}
// detail (Midtrans, Stripe) menyusul tanpa menyentuh domainDampaknya ke arsitektur: domain layer bebas dari vendor lock-in, testing mudah dengan mock, dan penggantian provider menjadi keputusan murah — persis jenis fleksibilitas yang dibayar mahal jika hilang.
Tip
Cara cepat mendeteksi pelanggaran SOLID saat code review design: hitung kata "dan". Jika kalian menjelaskan satu class dengan banyak "dan", ia melanggar SRP. Jika menambah fitur butuh mengedit banyak file yang tidak berhubungan, OCP dan DIP kemungkinan dilanggar.
Dari 23 pattern GoF, tidak semuanya penting bagi architect. Fokuslah pada yang efeknya terasa lintas modul:
| Pattern | Masalah yang Diselesaikan | Relevansi Arsitektur |
|---|---|---|
| Adapter | Interface pihak ketiga tidak cocok | Isolasi vendor, anti-corruption layer |
| Facade | Subsistem kompleks | Sederhanakan API publik modul |
| Strategy | Algoritma bervariasi per konteks | Pluggable pricing, routing |
| Observer / Pub-Sub | Notifikasi many-to-many | Fondasi event-driven (episode 13) |
| Decorator | Tambah perilaku tanpa ubah inti | Middleware, retry/logging wrapper |
| Factory | Pembuatan object kompleks | Konstruksi service per environment |
Perhatikan polanya: pattern yang naik ke level arsitektur adalah yang memengaruhi dependency antar komponen, bukan yang sekadar rapi di dalam satu file.
// Domain hanya kenal port-nya sendiri
import { PaymentGateway } from "../domain/ports"
export class MidtransAdapter implements PaymentGateway {
async charge(orderId: string, amountCents: number) {
// detail HTTP Midtrans dibungkus di sini,
// termasuk mapping error ke domain error
const res = await this.client.createTransaction({
order_id: orderId,
gross_amount: amountCents / 100,
})
return mapToDomainResult(res)
}
}Kalau suatu hari ganti ke Xendit, hanya file adapter yang berubah. Pola ini juga disebut hexagonal architecture atau ports and adapters — dan ia jembatan alami menuju DDD.
DDD sering disalahpahami sebagai sekadar anotasi @Entity. Bagi architect, bagian terpenting adalah strategic design — cara membagi sistem besar menjadi bagian yang bisa dikelola.
Tim, kode, dan dokumen memakai istilah yang sama. Kalau bisnis menyebut "Pesanan" tapi database menyebut transaction_record, setiap diskusi membayar pajak translasi. Bahasa yang konsisten membuat model kode mencerminkan realitas bisnis.
Model yang valid di satu area tidak otomatis valid di area lain. Kata "produk":
Memaksa satu class Product untuk semua context menghasilkan god-object yang tak berhenti bertumbuh. Bounded context memotong sistem berdasarkan makna bisnis — dan potongan inilah kandidat terbaik untuk module boundary (episode 7).
Antara dua bounded context selalu ada relasi: shared kernel, customer-supplier, conformist, atau anti-corruption layer. Memetakan relasi ini eksplisit mencegah model satu context bocor ke context lain.
Bagian tactical DDD (entity, value object, aggregate, repository) tetap penting — terutama aggregate sebagai unit konsistensi transaksional. Aggregate Order menentukan invariant mana yang dijaga dalam satu transaksi ACID; apa pun di luar agregat di-sync via event.
Mari praktikkan pada studi kasus kita. Langkah pertama: workshop event storming ringkas — daftar event bisnis utama:
ProdukDitambahkan, StokBerubah,
OrderDibuat, OrderDibayar, OrderDibatalkan,
PembayaranDiterima, PembayaranGagal,
PengirimanDijadwalkan, PengirimanSelesaiLangkah kedua: kluster event berdasarkan siapa yang "memiliki" bahasanya:
contexts:
catalog:
owns: [ProdukDitambahkan]
language: produk, kategori, harga-tampil
inventory:
owns: [StokBerubah]
language: sku, stok-fisik, reservasi
ordering:
owns: [OrderDibuat, OrderDibatalkan]
language: pesanan, keranjang, checkout
payment:
owns: [PembayaranDiterima, PembayaranGagal]
language: charge, refund, settlement
shipping:
owns: [PengirimanDijadwalkan, PengirimanSelesai]
language: resi, kurir, ETALima context, masing-masing dengan bahasa sendiri. Perhatikan bahwa ini bukan pemecahan teknis (frontend/backend/database), melainkan pemecahan berdasarkan makna bisnis — itulah kenapa boundary seperti ini awet terhadap perubahan framework.
Important
Uji kelayakan boundary: jika dua context selalu harus berubah bersamaan setiap sprint, mereka kemungkinan satu context yang terbelah. Jika satu context berisi tiga sub-domain yang jarang saling sentuh, ia kandidat dipecah. Boundary ditemukan lewat iterasi, bukan ditetapkan sekali jadi.
Inti yang harus dibawa pulang:
Di episode 5 selanjutnya kita kembali ke quality attributes — kali ini secara engineering: format quality scenario yang terukur, tactics untuk performance, scalability, reliability, security, dan maintainability, serta cara menyusun scenario untuk flash sale studi kasus kita. Sampai jumpa!