Sejauh ini seluruh pola kita hidup di memori: objek diciptakan, dibungkus, disiarkan — lalu hilang saat program berakhir. Aplikasi nyata menuntut keranjang Budi masih ada besok pagi, dan di sinilah domain bertemu database, tabel, dan SQL. Repository pattern, yang diperkenalkan Eric Evans dalam Domain-Driven Design, memasang satu lapisan di perbatasan itu: kode domain tetap berbicara bahasa koleksi — simpan, cari, tampilkan semua — sementara seluruh detail penyimpanan diserap ke balik satu kontrak. Chapter ini membahas perspektif desainnya; implementasi lengkap dengan JDBC dan SQL sudah menunggu di chapter Pattern Repository pada Section Database Programming, sehingga kita tidak akan mengulang materi tersebut di sini.

Koleksi Domain Menurut Evans #

Evans mendefinisikan repository sebagai sesuatu yang “memediasi antara domain dan data mapping, berperilaku seperti koleksi objek domain di memori”. Kata kuncinya adalah ilusi: kode domain seolah-olah mengambil Keranjang dari sebuah List, padahal di balik layar bisa ada koneksi database, cache, atau panggilan layanan web. Ilusi ini bukan kekanakan, melainkan disiplin arah dependensi: aturan bisnis tidak boleh tahu bentuk penyimpanan, karena bentuk penyimpanan akan berganti jauh lebih sering daripada aturan bisnis.

Kosakata kontrak repository pun sengaja memakai bahasa koleksi — simpan, cari, semuanya — bukan bahasa data seperti insert atau executeQuery. Saat nama method mulai membocorkan teknologi, kontrak tersebut sedang berubah menjadi DAO tanpa kita sadari.

Satu Repository per Aggregate Root #

Aturan penting kedua menyangkut satuan penyimpanan: repository melayani aggregate root, dan menyimpan aggregate secara utuh. RepoKeranjang menyimpan Keranjang lengkap dengan seluruh ItemBelanja di dalamnya — tidak ada RepoItem terpisah yang bisa dipanggil untuk mengubah satu item di luar pengawasan root. Dengan begitu invarian aggregate yang kita bangun di chapter Domain Modeling tetap berlaku: satu pintu masuk berlaku pula untuk penyimpanan. Konsekuensi lainnya, identitas entity berarti sesuatu yang nyata bagi repository: cari("KR-01") mengembalikan aggregate dengan identitas itu atau tidak sama sekali.

Kontrak dan Implementasi dalam Kode #

Program berikut memperlihatkan pasangan kontrak dan implementasi in-memory dalam bentuk ringkas — cukup untuk pengujian, dan bentuk yang persis sama dipakai layanan domain:

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class BelanjaDenganRepo {
    public static void main(String[] args) {
        RepoKeranjang repo = new RepoKeranjangMemori();
        LayananBelanja layanan = new LayananBelanja(repo);

        layanan.mulaiBelanja("KR-01", "Budi");
        layanan.mulaiBelanja("KR-02", "Siti");
        layanan.tambahItem("KR-01", "Buku Java", 2);

        // Kode domain bicara bahasa koleksi, bukan bahasa tabel dan SQL
        System.out.println(repo.cari("KR-01").map(Keranjang::ringkas).orElse("tidak ada"));
        System.out.println("jumlah keranjang terdaftar: " + repo.semuanya().size());
    }
}

// Aggregate yang disimpan utuh lewat root-nya
record Keranjang(String id, String pembeli, List<ItemBelanja> items) {
    Keranjang {
        items = List.copyOf(items);
    }

    static Keranjang kosong(String id, String pembeli) {
        return new Keranjang(id, pembeli, List.of());
    }

    Keranjang tambah(String nama, int jumlah) {
        List<ItemBelanja> baru = new ArrayList<>(items);
        baru.add(new ItemBelanja(nama, jumlah));
        return new Keranjang(id, pembeli, baru);
    }

    String ringkas() {
        return id + " milik " + pembeli + ": " + items.size() + " jenis item";
    }
}

record ItemBelanja(String nama, int jumlah) {
}

// Kontrak repository: kosakata koleksi domain
interface RepoKeranjang {
    void simpan(Keranjang keranjang);

    Optional<Keranjang> cari(String id);

    List<Keranjang> semuanya();
}

// Implementasi in-memory: cukup untuk pengujian dan prototipe
class RepoKeranjangMemori implements RepoKeranjang {
    private final Map<String, Keranjang> data = new LinkedHashMap<>();

    @Override
    public void simpan(Keranjang keranjang) {
        data.put(keranjang.id(), keranjang);
    }

    @Override
    public Optional<Keranjang> cari(String id) {
        return Optional.ofNullable(data.get(id));
    }

    @Override
    public List<Keranjang> semuanya() {
        return List.copyOf(data.values());
    }
}

// Layanan domain mengandalkan kontrak, bukan implementasi tertentu
class LayananBelanja {
    private final RepoKeranjang repo;

    LayananBelanja(RepoKeranjang repo) {
        this.repo = repo;
    }

    void mulaiBelanja(String id, String pembeli) {
        repo.simpan(Keranjang.kosong(id, pembeli));
    }

    void tambahItem(String id, String nama, int jumlah) {
        repo.cari(id).ifPresent(keranjang ->
                repo.simpan(keranjang.tambah(nama, jumlah)));
    }
}

Output yang Dihasilkan:

text
KR-01 milik Budi: 1 jenis item
jumlah keranjang terdaftar: 2

Bedah singkat program di atas:

  1. Baris 47-53 (interface RepoKeranjang): kontrak berbahasa koleksi — simpan, cari, semuanya. Tidak satu pun nama method menyebut tabel, koneksi, atau query.
  2. Baris 23-41 (record Keranjang): aggregate yang mengelola dirinya sendiri secara immutable: tambah mengembalikan Keranjang baru, sehingga repository tinggal menyimpan hasilnya tanpa logika tambahan.
  3. Baris 56-70 (RepoKeranjangMemori): implementasi pertama yang menyimpan di LinkedHashMap. Pada chapter Pattern Repository di Section Database Programming, implementasi saudaranya membungkus JDBC — kontrak dan layanan domain tidak berubah sedikit pun.
  4. Baris 76-92 (class LayananBelanja): layanan domain hanya mengenal kontrak. Inilah titik yang membuat pengujian domain bisa berjalan tanpa database sama sekali.

Testabilitas sebagai Hadiah Utama #

Manfaat praktis terbesar repository justru muncul sebelum database dipasang: seluruh aturan domain dapat diuji dengan implementasi in-memory yang berjalan dalam mikrodetik. Martin Fowler menyebut objek semacam ini fake — implementasi ringkas yang punya perilaku sungguhan — dan membedakannya dari mock yang sekadar memverifikasi pemanggilan; perbedaan ini penting saat memilih alat bantu uji. Pengujian domain tidak lagi menyiapkan skema database, tidak memerlukan transaksi pembersihan antar kasus uji, dan pesan kegagalannya berbicara bahasa domain, bukan bahasa kendala foreign key.

Repository menyimpan satu aggregate dalam satu operasi. Namun satu alur transaksi nyata sering menyentuh beberapa aggregate sekaligus: simpan keranjang, catat mutasi stok, tulis baris audit — semua harus menempel bersama atau batal bersama. Pola yang mengumpulkan perubahan tersebut menjadi satu kesatuan transaksi adalah unit of work pattern, chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

BelanjaDenganRepo.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class BelanjaDenganRepo {
    public static void main(String[] args) {
        RepoKeranjang repo = new RepoKeranjangMemori();
        LayananBelanja layanan = new LayananBelanja(repo);

        layanan.mulaiBelanja("KR-01", "Budi");
        layanan.mulaiBelanja("KR-02", "Siti");
        layanan.tambahItem("KR-01", "Buku Java", 2);

        // Kode domain bicara bahasa koleksi, bukan bahasa tabel dan SQL
        System.out.println(repo.cari("KR-01").map(Keranjang::ringkas).orElse("tidak ada"));
        System.out.println("jumlah keranjang terdaftar: " + repo.semuanya().size());
    }
}

// Aggregate yang disimpan utuh lewat root-nya
record Keranjang(String id, String pembeli, List<ItemBelanja> items) {
    Keranjang {
        items = List.copyOf(items);
    }

    static Keranjang kosong(String id, String pembeli) {
        return new Keranjang(id, pembeli, List.of());
    }

    Keranjang tambah(String nama, int jumlah) {
        List<ItemBelanja> baru = new ArrayList<>(items);
        baru.add(new ItemBelanja(nama, jumlah));
        return new Keranjang(id, pembeli, baru);
    }

    String ringkas() {
        return id + " milik " + pembeli + ": " + items.size() + " jenis item";
    }
}

record ItemBelanja(String nama, int jumlah) {
}

// Kontrak repository: kosakata koleksi domain
interface RepoKeranjang {
    void simpan(Keranjang keranjang);

    Optional<Keranjang> cari(String id);

    List<Keranjang> semuanya();
}

// Implementasi in-memory: cukup untuk pengujian dan prototipe
class RepoKeranjangMemori implements RepoKeranjang {
    private final Map<String, Keranjang> data = new LinkedHashMap<>();

    @Override
    public void simpan(Keranjang keranjang) {
        data.put(keranjang.id(), keranjang);
    }

    @Override
    public Optional<Keranjang> cari(String id) {
        return Optional.ofNullable(data.get(id));
    }

    @Override
    public List<Keranjang> semuanya() {
        return List.copyOf(data.values());
    }
}

// Layanan domain mengandalkan kontrak, bukan implementasi tertentu
class LayananBelanja {
    private final RepoKeranjang repo;

    LayananBelanja(RepoKeranjang repo) {
        this.repo = repo;
    }

    void mulaiBelanja(String id, String pembeli) {
        repo.simpan(Keranjang.kosong(id, pembeli));
    }

    void tambahItem(String id, String nama, int jumlah) {
        repo.cari(id).ifPresent(keranjang ->
                repo.simpan(keranjang.tambah(nama, jumlah)));
    }
}