Pola Logika Bisnis Sederhana dengan Transaction Script & Active Record
Saat logika bisnis belum terlalu rumit, kita bisa pakai pola Transaction Script atau Active Record.
Business Logic
Logika bisnis (business logic) adalah bagian dari sistem yang menentukan bagaimana data diproses sesuai aturan dan tujuan bisnis.
Secara definisi:
Business logic adalah sekumpulan keputusan, kalkulasi, kondisi, dan alur kerja yang menjelaskan bagaimana bisnis seharusnya beroperasi dalam konteks sistem perangkat lunak.
Contoh:
Jika saldo rekening < 0 → tolak transaksi.
Jika pelanggan sudah bayar → ubah status pesanan menjadi “confirmed.”
Jika total belanja > 1 juta → berikan diskon 10%.
Semua ini bukan “aturan pemrograman umum,” tapi cara bisnis tertentu berjalan.
Karena itu, business logic berbeda dari technical logic (seperti validasi input, autentikasi pengguna, caching, dll.).
Intinya: logika bisnis = perilaku sistem yang mencerminkan keputusan bisnis.
Business Rules
Istilah Business Rules dan Business Logic ini sering tertukar, padahal secara konsep ada perbedaan:
Business Rules
Ciri utama: Prinsip atau kebijakan yang didefinisikan oleh organisasi, misalnya menjelaskan apa yang boleh dan tidak boleh terjadi di dunia nyata.
Contoh: Setiap transaksi harus memiliki nomor faktur unik.
Business Logic
Ciri utama: Implementasi teknis dari aturan-aturan tersebut di dalam sistem.
Contoh:
fn generate_invoice_id()
memastikan nomor faktur tidak duplikat.
Singkatnya:
Business rules = “aturan permainan” (bersifat deklaratif).
Business logic = “cara memainkan permainan itu di sistem” (bersifat prosedural).
Dalam DDD, keduanya sama-sama penting:
Aturan bisnis berasal dari pengetahuan ahli domain (domain expert).
Logika bisnis diimplementasikan di kode (biasanya di entitas atau service dalam domain model).
Transaction Script
Secara definisi, Transaction Script adalah pola arsitektur di mana setiap operasi bisnis diimplementasikan sebagai satu prosedur atau fungsi tunggal.
Istilah aslinya berasal dari Patterns of Enterprise Application Architecture oleh Martin Fowler (2003).
Definisi: “Transaction Script organizes business logic by procedures where each procedure handles a single request from the presentation.”
— Martin Fowler, P of EAA, 2003.
Dengan kata lain:
Setiap use case bisnis (misal “proses pembayaran”, “buat laporan”, “import data”) ditulis sebagai satu skrip atau fungsi.
Logika bisnis disusun secara imperatif (langkah demi langkah).
Tidak ada objek domain yang rumit — biasanya hanya sekumpulan operasi database.
Analogi: seperti membuat resep dapur dalam satu lembar kertas, semua langkahnya berurutan: ambil bahan, masak, sajikan. Simpel, tapi sulit dipakai ulang kalau resep makin kompleks.
Khononov (2022) menjelaskan bahwa Transaction Script mengorganisir logika bisnis ke dalam prosedur terpisah. Setiap prosedur (atau fungsi) menangani satu permintaan atau operasi bisnis.
Misalnya, fungsi process_payment()
yang hanya fokus menyelesaikan satu transaksi pembayaran. Pendekatan ini gampang dipahami dan minim abstraksi. Contohnya, bayangkan kita punya daftar pekerjaan (job) yang perlu diproses satu per satu:
fn process_jobs(db: &Database) {
db.start_transaction();
if let Some(job) = db.load_next_job() {
let data = std::fs::read_to_string(&job.source).unwrap();
let result = convert_to_xml(&data);
std::fs::write(&job.destination, result).unwrap();
db.mark_complete(job.id);
}
db.commit();
}
Kode di atas menunjukkan satu prosedur sederhana: mulai transaksi, baca job berikutnya, ubah data, simpan hasil, lalu commit. Jika ada kesalahan di tengah, sistem harus rollback agar data tetap konsisten.
Khononov menegaskan pentingnya perilaku transaksional di setiap skrip: operasi harus sukses seluruhnya atau gagal tanpa meninggalkan kondisi setengah jadi. Karena itu, pola Transaction Script cocok untuk subdomain pendukung atau tugas ETL (Extract-Transform-Load) sederhana. Sebagai contoh, sinkronisasi data dari satu sistem ke sistem lain bisa diimplementasikan dengan skrip seperti di atas.
Kapan pakai Transaction Script? Beberapa poin penting:
Cocok untuk subdomain pendukung atau generik di mana logika bisnisnya prosedural (misal batch job, laporan, sinkronisasi).
Sederhana dan cepat: minim abstraksi, performa tinggi karena langsung panggil database (atau penyimpanan).
Hati-hati: Jika logika bisnis makin kompleks, skrip terpisah cenderung duplikasi kode dan kesulitan pemeliharaan. Khononov bahkan menyebut pattern ini sering dianggap antipola (antipattern) karena bisa jadi “big ball of mud” jika dipakai di domain inti yang rumit. Jadi, jangan pakai Transaction Script untuk core subdomain yang punya banyak aturan bisnis, karena pola ini tidak tahan dengan kompleksitas tinggi.
Antipattern Arsitektur Big Ball of Mud
Ini adalah antipattern arsitektur yang menggambarkan sistem tanpa struktur yang jelas.
Definisi: “A Big Ball of Mud is a haphazardly structured system, tangled, and difficult to maintain.”
— Foote & Yoder, 1997.
Sederhananya:
Semua kode bercampur tanpa batas yang jelas.
Tidak ada pemisahan logika bisnis, data, dan presentasi.
Setiap perubahan kecil bisa merusak bagian lain.
Dalam konteks pembahasan di atas:
Jika kita memaksa memakai Transaction Script atau Active Record untuk domain yang kompleks (seperti core subdomain), cepat atau lambat sistem akan jadi Big Ball of Mud.
Kenapa? Karena fungsi-fungsi akan saling memanggil, logika duplikat di mana-mana, dan tidak ada satu model mental yang jelas tentang “bagaimana bisnis bekerja” di dalam kode.
Active Record
Active Record juga berasal dari buku Fowler yang sama, dan definisinya adalah:
Definisi: “An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.”
— Martin Fowler, P of EAA, 2003.
Artinya:
Setiap baris di tabel database direpresentasikan oleh sebuah objek.
Objek itu “aktif” karena bisa menyimpan (
save()
), memperbarui (update()
), atau menghapus (delete()
) dirinya sendiri.Kadang juga memuat logika bisnis sederhana, seperti validasi atau penghitungan kecil.
Analogi: seperti lembar Excel di mana tiap baris bisa langsung “menyimpan dirinya ke server” begitu kita ubah isinya.
Active Record biasanya lebih rapi dari Transaction Script karena sudah mulai ada objek dan data berelasi, tapi belum punya pemisahan yang jelas antara data dan aturan bisnis.
Pola ini diperkenalkan oleh Fowler dan disitir Khononov (2022) sebagai objek yang “membungkus satu baris di tabel database, mengenkapsulasi akses database, dan menambah logika bisnis di data tersebut”. Artinya, setiap entitas data (misalnya baris User
di tabel) direpresentasikan oleh sebuah objek yang punya metode sendiri, termasuk operasi save() atau update() ke database.
Active Record cocok ketika logika bisnis masih sederhana, tapi struktur datanya relatif rumit (misal relasi banyak-ke-banyak). Contohnya, kalau kita punya struct Rust User
:
struct User {
id: u32,
name: String,
email: String,
}
impl User {
fn save(&self, conn: &DbConnection) {
// Contoh sederhana: INSERT atau UPDATE ke DB
conn.execute(”INSERT INTO users ...”, &[&self.name, &self.email]);
}
fn increment_logins(&mut self, conn: &DbConnection) {
self.logins += 1;
self.save(conn);
}
}
Di sini, User
bertindak sebagai active record: ia menyimpan datanya (id
, name
, dll.) dan punya method save()
untuk persist ke database. Jika kita perlu modifikasi data kompleks, kita manipulasi objek User
dan panggil user.save(conn)
untuk update.
Menurut Khononov, tujuan pola ini adalah mengenkapsulasi kompleksitas pemetaan antara object ke skema database. Dibanding Transaction Script, skema Active Record menyediakan objek-objek khusus yang memuat data dan operasi CRUD mereka sendiri.
Kapan pakai Active Record? Beberapa poin:
Logika masih sederhana (CRUD, validasi input ringan) tapi datanya lebih struktur.
Berguna di subdomain pendukung/generik yang butuh konversi data kompleks ke model aplikasi.
Intinya: Active Record adalah Transaction Script yang “hidup” – yaitu setiap operasi bisnis masih dilakukan di luar objek, tapi objek data punya save() sendiri. Ini punya kekurangan: logika bisnis tetap tersebar di layanan/prosedur, jadi sering disebut anemic domain model. Maka jangan gunakan di core subdomain yang seharusnya punya domain model penuh (full domain model).
Ringkasnya, bagian ini memberikan pemahaman bahwa untuk kasus sederhana kita bisa pakai Transaction Script atau Active Record. Keduanya ringan dan cepat diimplementasi, tapi terbatas pada logika sederhana di subdomain pendukung. Untuk core subdomain dengan banyak aturan, kita akan membahas pada pola Domain Model berikutnya.
Anemic Domain Model
Istilah ini juga dari Martin Fowler, dan Khononov menyinggungnya saat membandingkan Active Record dengan Domain Model.
Definisi: “Anemic Domain Model is a domain model where the business logic is absent and resides in services instead of the entities themselves.”
— Martin Fowler, 2003.
Artinya:
Objek domain hanya menyimpan data (getter & setter).
Logika bisnis tersebar di service/fungsi lain.
Secara teknis modelnya ada, tapi secara semantik “kosong”.
Contoh dalam Rust:
struct Order {
id: u32,
total: f64,
status: String,
}
fn confirm(order: &mut Order) {
if order.total > 0.0 {
order.status = “Confirmed”.to_string();
}
}
Objek Order
di sini tidak tahu apa-apa tentang bisnisnya sendiri.
Segala logika berada di luar (confirm()
). Akibatnya, aturan bisnis bisa dengan mudah tersebar dan bertentangan.
DDD menolak pola ini karena tujuan DDD adalah memusatkan logika bisnis di dalam domain model, agar kode benar-benar mencerminkan pemikiran bisnis.
Full Domain Model
Dalam DDD, domain model penuh, istilah aslinya adalah “Rich Domain Model” atau “Full-Fledged Domain Model”.
Definisi: “A rich domain model is one where domain objects encapsulate both data and behavior, enforcing business rules internally.”
— Evans, Domain-Driven Design, 2004.
Bedanya dengan anemic domain model berdasarkan karakteristik:
Rich (Full) Domain Model
Data dan perilaku: Digabung di entitas.
Aturan Bisnis: Ditegakkan di dalam entitas.
Keterwakilan domain: Kuat, selaras dengan bahasa bisnis.
Cocok untuk: Core subdomain.
Anemic Domain Model
Data dan perilaku: Terpisah.
Aturan Bisnis: Tersebar di service luar.
Keterwakilan domain: Lemah, cenderung CRUD.
Cocok untuk: Supporting/generic subdomain.
Jadi ketika Khononov menulis bahwa “core subdomain seharusnya memiliki domain model penuh”, maksudnya adalah:
Subdomain yang menjadi keunggulan utama bisnis harus diimplementasikan dengan rich domain model, agar aturan bisnis dan konsep bisnis benar-benar hidup di dalam kode, bukan di spreadsheet, dokumen, atau service-service longgar.
Domain Model Berdasarkan Pola yang Cocok
Ada spektrum kompleksitas yang mempengaruhi pemilihan domain model yang cocok:
Kondisi domain sangat sederhana (ETL, batch job, CRUD data referensi)
Pola yang cocok: Transaction Script.
Karena tidak perlu model atau logika kompleks.
Kondisi domain sederhana tapi punya struktur data berelasi
Pola yang cocok: Active Record.
Karena sedikit OO, tapi mudah dan cepat.
Kompleks dengan aturan bisnis yang dinamis
Pola yang cocok: Rich Domain Model.
Karena logika terpusat di entitas & aggregate.
Butuh audit / histori
Pola yang cocok: Event-Sourced Domain Model.
Karena Domain Model + Event Sourcing.
Melihat Kembali Subdomain Pendukung (supporting subdomain)
Di atas banyak dibahas bahwa keduanya terbatas pada logika sederhana di subdomain pendukung. Kita lihat kembali klasifikasi subdomain pada DDD (Khononov, 2022):
Core Subdomain
Karakteristik: Menjadi sumber keunggulan kompetitif bisnis. Kompleks, sering berubah, butuh inovasi tinggi.
Fokus solusi: Perlu desain DDD mendalam, domain model penuh, investasi besar.
Supporting Subdomain
Karakteristik: Mendukung operasional inti, tapi tidak membedakan bisnis dari kompetitor. Biasanya sederhana.
Fokus solusi: Gunakan solusi pragmatis (Transaction Script, Active Record, atau framework siap pakai)
Generic Subdomain
Karakteristik: Umum dipakai semua bisnis (contoh: autentikasi, log aktivitas).
Fokus solusi: Lebih baik dibeli atau pakai library standar.
Sederhananya:
Supporting subdomain masih penting untuk bisnis, tapi tidak menentukan keberhasilan utama.
Karena kompleksitasnya rendah dan nilai strategisnya kecil, maka kita tidak perlu investasi arsitektur rumit seperti Domain Model penuh.
Inilah kenapa Transaction Script dan Active Record cocok: cepat dibuat, mudah dipelihara, cukup stabil.
Sebaliknya, core subdomain biasanya punya banyak aturan bisnis, ketergantungan lintas fungsi, dan terus berubah.
Data & Logika Bisnis
Pernyataan “pemisahan antara data dan logika bisnis“ ini sering muncul dalam konteks arsitektur software, misalnya Separation of Concerns.
Artinya: Data (apa yang disimpan) dan logika bisnis (apa yang dilakukan terhadap data itu) tidak seharusnya bercampur dalam satu tempat.
Contoh salah:
fn save_order_to_db(order_id: u32, total: f64) {
// ... query ke DB ...
if total > 1000000.0 {
// logika bisnis diselipkan di sini
println!(”Diskon 10%”);
}
}
Contoh benar:
struct Order {
id: u32,
total: f64,
}
impl Order {
fn apply_discount(&mut self) {
if self.total > 1000000.0 {
self.total *= 0.9;
}
}
}
fn save_order_to_db(order: &Order) {
// hanya urusan penyimpanan
}
Pada contoh benar, fungsi save_order_to_db()
hanya fokus pada data persistence, sedangkan apply_discount()
berisi logika bisnis.
Keduanya terpisah untuk memudahkan pengujian, refaktor, dan perubahan aturan bisnis tanpa merusak cara penyimpanan data.
Pemisahan antara data, logika bisnis, dan presentasi adalah prinsip dasar arsitektur aplikasi, sering disebut sebagai three-layer (or three-tier) architecture.
Presentation layer
Tugas utama: Menampilkan data kepada pengguna dan menerima input.
Contoh dalam aplikasi: Antarmuka pengguna (UI), web page, REST API endpoint, CLI.
Business Logic layer
Tugas utama: Menjalankan aturan dan keputusan bisnis.
Contoh dalam aplikasi: Fungsi domain, service, entitas, aggregate.
Data Access layer atau Persistence Layer
Tugas utama: menyimpan dan mengambil data.
Contoh dalam aplikasi: Database, ORM, repository.
Dalam konteks arsitektur, “presentation layer” mencakup segala sesuatu yang berhubungan dengan interaksi pengguna atau sistem lain. Misalnya:
UI web atau mobile,
API endpoint (yang melayani HTTP request),
atau bahkan CLI untuk admin.
Presentasi tidak boleh tahu detail logika bisnis, cukup memanggil fungsinya saja.
Contoh:
// presentation layer
fn handle_checkout_request(user_input: CheckoutData) {
let mut order = Order::new(user_input.items);
order.apply_discount(); // business logic
repository::save(order); // data layer
}
Ketiganya punya tanggung jawab berbeda:
Presentation layer: berbicara dengan pengguna atau sistem lain.
Business logic layer: memutuskan apa yang boleh atau harus terjadi.
Data layer: mengelola penyimpanan (database, cache, dsb).
Prinsip ini disebut Separation of Concerns: memisahkan tanggung jawab agar sistem lebih modular dan mudah berkembang.
Referensi
Khononov, V. (2022). Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy. O’Reilly Media.