
Foto oleh Francesco Ungaro via Pexels
Repository pattern dan Unit of Work sering dibahas bersamaan karena keduanya saling melengkapi dalam mengelola akses data: Repository mengabstraksi operasi terhadap satu jenis entity, sementara Unit of Work mengelola transaksi yang melibatkan perubahan pada beberapa repository sekaligus sebagai satu kesatuan.
Tanpa Unit of Work, setiap repository yang memanggil SaveChanges sendiri-sendiri bisa menyebabkan sebagian perubahan tersimpan sementara perubahan lain gagal, melanggar prinsip atomicity yang penting ketika sebuah operasi bisnis sebenarnya melibatkan perubahan pada beberapa entity sekaligus.
Artikel ini membahas implementasi Unit of Work yang mengoordinasikan beberapa repository, memastikan seluruh perubahan dalam satu unit kerja bisnis berhasil atau gagal bersama-sama, konsisten dengan prinsip atomicity pada transaksi database.
Masalah Tanpa Unit of Work
SaveChanges terpisah berisiko inkonsistensi data
Unit of Work Mengoordinasikan Satu Transaksi
Satu SaveChanges untuk seluruh perubahan dalam unit kerja
Memakai Unit of Work dalam Use Case
Use case memanggil satu SaveChanges setelah seluruh operasi disiapkan
DbContext EF Core Sendiri Sudah Berperan sebagai Unit of Work
DbContext secara alami sudah mengimplementasikan pola Unit of Work
Peran Unit of Work Menjaga Konsistensi Transaksi Antar Repository
Unit of Work memastikan perubahan pada dua repository tersimpan sebagai satu transaksi
Repository pattern menyediakan abstraksi untuk mengakses satu jenis entity, namun banyak operasi bisnis nyata melibatkan perubahan pada lebih dari satu entity sekaligus, misalnya membuat pesanan baru sekaligus mengurangi stok produk. Jika setiap repository langsung melakukan commit sendiri-sendiri, kegagalan pada salah satu operasi bisa meninggalkan data dalam keadaan tidak konsisten, seperti stok sudah berkurang namun pesanan gagal tersimpan.
Unit of Work hadir sebagai lapisan yang mengoordinasikan beberapa repository dalam satu transaksi database, dengan menunda proses commit sampai method SimpanPerubahan dipanggil secara eksplisit. Semua perubahan yang dilakukan melalui repository yang berada di bawah Unit of Work yang sama akan berhasil atau gagal secara bersamaan, sesuai prinsip atomicity pada transaksi database.
Pada aplikasi yang memakai Entity Framework, DbContext sebenarnya sudah berperan sebagai Unit of Work secara bawaan melalui method SaveChanges, sehingga membungkusnya lagi dengan interface kustom lebih bermanfaat untuk kebutuhan testing dan menjaga lapisan Application tidak bergantung langsung pada DbContext. Namun untuk aplikasi sederhana dengan satu jenis repository saja, menambahkan Unit of Work secara eksplisit kadang hanya menambah kompleksitas tanpa manfaat signifikan.
Specification Pattern untuk Menghindari Repository yang Membengkak
Specification pattern merangkum kriteria query sebagai objek yang bisa digabung
Repository yang awalnya sederhana cenderung membengkak seiring waktu karena setiap kebutuhan filter baru dari sisi bisnis biasanya ditambahkan sebagai method baru, seperti CariStokMenipis, CariStokMenipisKategoriElektronik, dan seterusnya, yang pada akhirnya membuat interface repository sulit dipelihara dan penuh duplikasi logika query yang mirip satu sama lain. Perubahan kecil pada aturan bisnis juga memaksa banyak method repository diubah sekaligus, meningkatkan risiko regresi pada bagian kode yang sebenarnya tidak terkait langsung.
Specification pattern mengatasi masalah ini dengan membungkus kriteria pencarian sebagai objek tersendiri yang terpisah dari repository, sehingga repository cukup menyediakan satu method generik seperti Cari(Specification spec) yang menerima objek kriteria apa pun. Setiap kombinasi filter baru dibuat sebagai class Specification baru tanpa perlu mengubah interface atau implementasi repository yang sudah ada. Pendekatan ini sejalan dengan prinsip open-closed, karena menambah kriteria baru tidak mengharuskan modifikasi kode repository yang sudah ada dan teruji.
Pola ini juga memudahkan pengujian karena logika filter yang kompleks bisa diuji secara terisolasi sebagai unit kecil, serta memungkinkan beberapa specification digabungkan dengan operator AND atau OR untuk membentuk kriteria yang lebih kompleks. Trade-off-nya, specification pattern menambah lapisan abstraksi yang mungkin terasa berlebihan untuk aplikasi kecil dengan kebutuhan query yang masih sederhana dan jarang berubah. Pada aplikasi kecil dengan sedikit kombinasi filter, repository konvensional dengan beberapa method eksplisit biasanya masih lebih mudah dipahami tim baru.
Transactional Outbox agar Event Tidak Hilang saat Commit Gagal
Pesan outbox disimpan dalam transaksi database yang sama
Mengirim event langsung ke message broker tepat setelah commit database berisiko menimbulkan ketidakkonsistenan, misalnya data berhasil tersimpan namun pengiriman event ke broker gagal karena jaringan terputus, sehingga sistem lain yang bergantung pada event tersebut tidak pernah mendapat notifikasi perubahan yang sebenarnya sudah terjadi.
Pola transactional outbox mengatasi hal ini dengan menyimpan pesan event ke tabel Outbox dalam transaksi database yang sama dengan perubahan data utama, memakai Unit of Work yang sudah ada. Proses terpisah secara berkala membaca tabel Outbox dan mengirimkan pesan yang belum terkirim ke message broker, sehingga event dijamin konsisten dengan data yang benar-benar tersimpan di database.
Kesalahan Umum yang Sering Terjadi
Memanggil SaveChanges Terpisah di Setiap Repository
Memanggil SaveChangesAsync di dalam setiap method repository secara individual, alih-alih mengumpulkannya dan memanggil sekali di level Unit of Work atau use case, menghilangkan jaminan atomicity yang seharusnya didapat dari transaksi database tunggal.
Membuat Unit of Work yang Tidak Membagikan DbContext yang Sama
Jika setiap repository di dalam Unit of Work membuat instance DbContext-nya sendiri-sendiri, perubahan tidak akan benar-benar tergabung dalam satu transaksi meski sudah dibungkus dalam class UnitOfWork, karena SaveChanges pada satu DbContext tidak memengaruhi DbContext lain.
Menerapkan Unit of Work Terpisah Padahal Sudah Redundan dengan DbContext
Membangun wrapper Unit of Work yang kompleks padahal DbContext sendiri sudah cukup berperan sebagai Unit of Work bisa menambah kompleksitas tanpa manfaat nyata, terutama jika alasan utamanya hanya untuk mengikuti pola secara formal tanpa kebutuhan testability tambahan.
Kapan Pendekatan Ini Tidak Cocok Dipakai
Unit of Work eksplisit memberikan API yang jelas untuk mengelola transaksi yang melibatkan beberapa repository, memudahkan use case memahami bahwa seluruh operasi di dalamnya harus berhasil atau gagal bersama.
Namun karena DbContext EF Core sendiri sudah berperan sebagai Unit of Work, menambahkan wrapper tambahan bisa dianggap duplikasi yang tidak perlu untuk project yang tidak membutuhkan abstraksi tambahan di luar apa yang sudah disediakan EF Core secara default.
Ringkasan
Unit of Work memastikan perubahan yang melibatkan beberapa repository berhasil atau gagal bersama sebagai satu kesatuan atomik, mencegah data berada dalam kondisi tidak konsisten akibat SaveChanges yang terpisah-pisah. Pertimbangkan apakah project benar-benar membutuhkan wrapper eksplisit ini, mengingat DbContext EF Core sendiri sudah secara alami mengimplementasikan pola serupa.
Seri Belajar REST API & Clean Architecture:
← Sebelumnya: Dependency Rule dalam Clean Architecture
→ Selanjutnya: CQRS Sederhana untuk Aplikasi Backend