Repository pattern adalah pola desain yang membungkus logika akses data di balik interface yang bersih, memisahkan detail implementasi penyimpanan data (seperti Entity Framework Core) dari logika bisnis aplikasi, sehingga keduanya bisa berubah secara independen tanpa saling memengaruhi.
Dengan repository pattern, controller atau service tidak lagi berinteraksi langsung dengan DbContext, melainkan melalui interface repository yang mendefinisikan operasi apa saja yang tersedia, seperti GetById, GetAll, Add, atau Delete, tanpa perlu tahu bagaimana operasi tersebut sebenarnya diimplementasikan di baliknya.
Artikel ini membahas cara menerapkan repository pattern dasar dengan EF Core, manfaatnya untuk testability, serta perdebatan umum tentang apakah pola ini masih relevan mengingat EF Core DbContext sendiri sudah mengimplementasikan pola repository dan unit of work secara internal.
Mendefinisikan Interface Repository
Interface mendefinisikan kontrak operasi data tanpa detail implementasi
Implementasi Repository dengan EF Core
Implementasi konkret menyembunyikan detail EF Core dari pemanggil
Mendaftarkan Repository ke Dependency Injection
Controller bergantung pada abstraksi, bukan implementasi konkret
Perdebatan Relevansi Repository Pattern dengan EF Core
DbContext sendiri sudah berperilaku seperti repository
Generic Repository vs Repository Spesifik per Entity
Generic repository untuk operasi CRUD dasar, repository spesifik untuk query khusus
Generic repository bermanfaat untuk menghindari duplikasi kode CRUD dasar seperti GetByIdAsync, GetAllAsync, dan AddAsync yang polanya identik di hampir semua entity. Namun menjadikan generic repository sebagai satu-satunya lapisan akses data seringkali berujung pada masalah lain: query kompleks yang melibatkan filter bertingkat, join, atau agregasi dipaksa dibungkus ke dalam method generik yang tidak fleksibel, atau malah ditulis ulang di luar repository sehingga tujuan pattern ini menjadi sia-sia.
Pendekatan yang lebih realistis adalah menggabungkan generic repository untuk operasi dasar dengan repository spesifik per entity yang mewarisi interface generiknya, lalu menambahkan method seperti GetPendingOrdersByCustomerAsync yang memang hanya relevan untuk entity Order. Dengan begitu, kode tetap DRY untuk operasi umum, tetapi query bisnis yang spesifik tetap punya tempat yang jelas dan type-safe tanpa memaksakan generalisasi berlebihan.
Satu hal yang perlu diperhatikan ketika memakai Repository Pattern di atas EF Core adalah bahwa DbContext sendiri sebenarnya sudah merupakan implementasi dari Unit of Work dan Repository Pattern secara bawaan lewat DbSet
Unit of Work: Menyatukan Beberapa Repository dalam Satu Transaksi
Satu pemanggilan SaveChangesAsync membungkus perubahan dari dua repository berbeda
Ketika sebuah operasi bisnis melibatkan perubahan pada lebih dari satu entity sekaligus, misalnya mengurangi stok produk dan membuat record order baru dalam satu aksi checkout, memanggil SaveChangesAsync terpisah pada tiap repository berisiko menimbulkan data tidak konsisten jika salah satu operasi gagal di tengah jalan. Pattern Unit of Work mengatasi hal ini dengan membungkus beberapa repository dalam satu instance yang berbagi DbContext yang sama. Kegagalan pada satu bagian proses, seperti stok yang ternyata tidak mencukupi, bisa dideteksi sebelum SaveChangesAsync dipanggil sama sekali.
Karena seluruh repository pada contoh di atas menerima AppDbContext yang sama persis lewat dependency injection, perubahan yang dilakukan lewat Orders dan Products sama-sama tertampung di change tracker DbContext tersebut, dan baru benar-benar dikirim ke database saat SaveChangesAsync pada UnitOfWork dipanggil satu kali. Jika terjadi error sebelum SaveChangesAsync sempat dieksekusi, seluruh perubahan di memori otomatis tidak pernah tersimpan ke database. Perilaku ini setara dengan transaksi implisit, karena seluruh perubahan dikirim dalam satu batch perintah SQL yang sama ke database.
Penting dipahami bahwa AppDbContext pada EF Core sebenarnya sudah berperilaku sebagai Unit of Work secara bawaan, sehingga membungkusnya lagi dengan interface IUnitOfWork pada project kecil terkadang dianggap lapisan abstraksi berlebih. Pola ini lebih terasa manfaatnya pada aplikasi besar dengan banyak repository yang perlu dikoordinasikan secara eksplisit, atau ketika tim ingin menyembunyikan detail DbContext sepenuhnya dari layer service demi kemudahan testing dan penggantian ORM di masa depan. Keputusan memakai pattern ini sebaiknya mempertimbangkan ukuran tim dan kompleksitas domain, bukan sekadar mengikuti contoh arsitektur dari project lain.
Menggabungkan Specification Pattern dengan Repository untuk Query Fleksibel
Repository generik murni sering berujung pada penambahan method baru setiap kali ada kebutuhan filter yang berbeda, misalnya GetActiveProduk, GetProdukByCategory, dan berbagai variasi lainnya yang terus bertambah. Specification pattern memindahkan logika filter tersebut ke dalam class terpisah yang bisa disusun ulang dan dipakai kembali di banyak tempat, sementara repository itu sendiri tetap hanya punya satu method generik untuk menerima dan mengeksekusinya.
Pendekatan ini memudahkan penulisan unit test karena kriteria filter bisa diuji secara terpisah tanpa perlu memanggil database sama sekali, lalu diverifikasi ulang lewat integration test yang sesungguhnya. Untuk aplikasi kecil dengan jumlah variasi query yang masih sedikit, menambahkan layer specification bisa terasa berlebihan dibanding langsung menulis method repository yang spesifik untuk setiap kebutuhan query tersebut.
Kesalahan Umum yang Sering Terjadi
Membuat Repository Generic yang Terlalu Rumit
Repository generic yang mencoba menangani semua jenis entity dengan satu implementasi seringkali berakhir dengan interface yang terlalu banyak parameter opsional atau method yang jarang dipakai, menambah kompleksitas tanpa manfaat nyata.
Menerapkan Repository Pattern Tanpa Alasan yang Jelas
Menambahkan layer repository hanya karena dianggap 'best practice' tanpa mempertimbangkan apakah project benar-benar membutuhkan abstraksi tersebut bisa menambah kompleksitas yang tidak sepadan untuk project kecil dengan kebutuhan sederhana.
Membocorkan Detail EF Core Lewat Interface Repository
Mengembalikan IQueryable langsung dari method repository membocorkan detail implementasi EF Core ke pemanggil, menghilangkan sebagian besar manfaat abstraksi yang seharusnya diberikan repository pattern itu sendiri.
Kapan Pendekatan Ini Tidak Cocok Dipakai
Repository pattern memberikan testability yang lebih baik karena logika bisnis bisa diuji dengan mock repository tanpa database sungguhan, serta memudahkan penggantian teknologi penyimpanan data di masa depan.
Namun untuk project kecil hingga menengah yang tidak berencana mengganti EF Core, layer abstraksi tambahan ini bisa dianggap overhead yang tidak perlu, mengingat DbContext EF Core sendiri sudah cukup testable lewat in-memory database provider untuk kebutuhan unit testing.
Ringkasan
Repository pattern memberikan pemisahan yang bersih antara logika bisnis dan detail akses data, meningkatkan testability aplikasi, namun penerapannya perlu dipertimbangkan matang mengingat EF Core sendiri sudah menyediakan abstraksi serupa secara internal. Pilih berdasarkan kebutuhan nyata project, bukan sekadar mengikuti pola secara membabi buta.
Seri Belajar .NET Core:
← Sebelumnya: Relasi One to Many dan Many to Many di EF Core
→ Selanjutnya: Autentikasi JWT di ASP.NET Core Web API