One PR, One Purpose #
Ada PR yang membuat reviewer membuka tab baru, mengambil minum, lalu kembali dan merenung: sebaiknya saya mulai dari mana? Judul PR mengatakan “update auth service”, tapi diff-nya berisi refactor model, perbaikan bug di payment flow, update tiga dependency, dan penambahan endpoint baru. Mana yang harus direview dulu? Apakah bug fix di payment berhubungan dengan refactor auth, atau hanya kebetulan ada di PR yang sama? Pertanyaan-pertanyaan ini tidak boleh ada — dan prinsip One PR, One Purpose hadir untuk mencegahnya.
Prinsip ini sederhana secara formulasi tapi membutuhkan disiplin dalam praktik: satu Pull Request seharusnya hanya memiliki satu tujuan logis yang jelas, yang bisa dinyatakan dalam satu kalimat, dan yang bisa direview sebagai satu unit yang kohesif.
Apa yang Dimaksud dengan “Satu Tujuan”? #
“Satu tujuan” bukan berarti satu file atau satu fungsi. Satu tujuan berarti satu intent yang bisa diverifikasi secara independen dan bisa dijelaskan tanpa menyebut perubahan lain dalam PR yang sama.
// ✓ PR dengan satu tujuan yang jelas
"Fix token refresh logic to prevent premature logout"
→ Semua perubahan dalam PR ini melayani satu tujuan: fix token refresh
→ Jika di-revert, hanya token refresh yang kembali ke kondisi sebelumnya
→ Reviewer tahu persis apa yang harus diuji dan apa yang harus diverifikasi
// ✗ PR dengan banyak tujuan yang tercampur
"Update auth service"
→ Apa yang di-update? Semuanya? Sebagian?
→ Reviewer tidak tahu harus fokus ke mana
→ Jika ada bug yang diintroduksi, sulit menentukan perubahan mana yang menyebabkannya
Test paling sederhana untuk mengevaluasi fokus PR: bisakah tujuan PR ini dijelaskan dalam satu kalimat yang spesifik? Jika butuh kata “dan”, “serta”, atau “juga” untuk menjelaskan tujuannya — PR tersebut kemungkinan besar sudah memiliki lebih dari satu tujuan.
// ✗ Kalimat yang mengindikasikan PR terlalu banyak tujuan
"PR ini fix bug login DAN refactor auth service DAN update dependency JWT"
"PR ini tambah fitur export SERTA perbaiki layout dashboard"
// ✓ Kalimat yang mengindikasikan PR fokus
"PR ini fix race condition di concurrent order update"
"PR ini tambah rate limiting ke endpoint public search"
"PR ini refactor payment service ke strategy pattern"
Mengapa Satu Tujuan Itu Penting? #
PR adalah Media Komunikasi, Bukan Hanya Mekanisme Merge #
PR yang baik bukan hanya tentang kode yang benar — ia tentang perubahan yang dikomunikasikan dengan jelas. Reviewer, engineer lain yang membaca git history, dan bahkan diri sendiri di masa depan, semuanya bergantung pada kejelasan sebuah PR untuk memahami mengapa sebuah perubahan dibuat.
flowchart LR
subgraph PR dengan banyak tujuan
A1[Reviewer membuka PR] --> B1[Baca diff 15 file]
B1 --> C1{Ini refactor atau feature?\nApa yang harus difokuskan?}
C1 --> D1[Ketidakpastian → review dangkal]
D1 --> E1[LGTM tanpa keyakinan]
end
subgraph PR dengan satu tujuan
A2[Reviewer membuka PR] --> B2[Baca deskripsi: fix token refresh]
B2 --> C2[Tahu persis apa yang harus diverifikasi]
C2 --> D2[Review fokus dan bermakna]
D2 --> E2[Approve dengan keyakinan]
endKognitif Reviewer Bekerja Berdasarkan Konteks, Bukan Diff #
Saat mereview kode, reviewer tidak membaca baris per baris seperti compiler. Mereka membangun model mental tentang apa yang berubah dan mengapa. Konteks yang jelas adalah bahan bakar untuk model mental itu.
PR dengan banyak tujuan memaksa reviewer untuk membangun beberapa model mental sekaligus — dan kapasitas working memory manusia terbatas. Ketika konteks terlalu banyak dan terfragmentasi, yang terjadi adalah:
Efek beban kognitif pada reviewer:
PR dengan 1 tujuan → model mental tunggal dan kohesif
→ reviewer bisa mendeteksi edge case dan asumsi yang salah
→ feedback berkualitas tinggi
PR dengan 3 tujuan → 3 model mental yang harus dibangun dan dijaga
→ energi habis hanya untuk memisahkan konteks
→ feedback menjadi dangkal: "kodenya terlihat oke"
PR dengan 5+ tujuan → cognitive overload
→ reviewer menyerah membangun model mental yang utuh
→ LGTM tanpa review yang bermakna
Rollback yang Presisi Hanya Mungkin dengan PR yang Fokus #
Ketika ada masalah di production, kemampuan rollback yang cepat dan presisi adalah aset paling berharga. PR yang fokus memungkinkan rollback bedah — hanya perubahan yang bermasalah yang di-revert, tanpa membawa ikut perubahan lain yang sebenarnya tidak bermasalah.
Skenario: Bug ditemukan di production setelah deployment
PR yang mencampur banyak tujuan:
PR berisi: fix A, refactor B, tambah feature C
Bug berasal dari: refactor B
Pilihan rollback:
→ Revert seluruh PR → fix A dan feature C yang sudah benar juga hilang
→ Tidak rollback → bug tetap ada di production
→ Tidak ada pilihan yang bagus
PR yang fokus:
PR 1: fix A (sudah di-merge, tidak ada masalah)
PR 2: refactor B (ini yang bermasalah)
PR 3: feature C (sudah di-merge, tidak ada masalah)
Pilihan rollback:
→ Revert PR 2 → hanya refactor B yang kembali, fix A dan feature C aman
→ Rollback presisi dalam hitungan menit
Git History yang Bermakna Adalah Dokumentasi Gratis #
Git log dari tim yang menerapkan One PR, One Purpose membaca seperti jurnal perubahan yang jelas. Setiap commit dan PR menceritakan satu episode yang kohesif dalam evolusi sistem.
// ✓ Git history yang bermakna
abc1234 feat(payment): add retry mechanism for network timeouts
def5678 fix(order): resolve race condition in concurrent status update
ghi9012 refactor(auth): extract token validation to dedicated service
jkl3456 chore: upgrade grpc-go to v1.62
// ✗ Git history yang tidak informatif
abc1234 update code
def5678 fix bugs and add stuff
ghi9012 misc changes
jkl3456 PR from feature branch
Perbedaan ini terlihat sederhana, tapi dampaknya nyata: engineer yang bergabung 6 bulan kemudian bisa memahami evolusi sistem dari git log yang pertama, tapi harus membaca seluruh diff dari git log yang kedua untuk mendapat informasi yang sama.
Jenis-Jenis Kontaminasi Tujuan #
Memahami bagaimana tujuan PR bisa terkontaminasi membantu engineer mendeteksi dan menghindarinya lebih awal.
Kontaminasi tipe 1: Oportunistik #
Terjadi ketika engineer membuka file yang sudah di-touch untuk keperluan PR utama, dan “sekalian” memperbaiki hal lain yang terlihat:
// Skenario kontaminasi oportunistik
Engineer sedang fix bug di auth/handler.go.
Saat membuka file, melihat:
- Variabel dengan nama yang membingungkan
- Import yang tidak terpakai
- Komentar yang outdated
Tergoda untuk "sekalian bersihkan" — dan menambahkan perubahan yang tidak
berkaitan dengan bug fix tersebut ke PR yang sama.
// Mengapa ini masalah
Reviewer melihat diff yang mencampur bug fix dan cleanup
→ Sulit memverifikasi apakah cleanup mempengaruhi behavior
→ Jika ada masalah setelah merge, sulit menentukan akar penyebabnya
// Solusi
Buat tiket tersendiri untuk cleanup
Atau buat PR terpisah setelah PR bug fix di-merge
Kontaminasi tipe 2: Rantai Ketergantungan #
Terjadi ketika feature baru membutuhkan refactor terlebih dahulu, dan keduanya digabung dalam satu PR:
// Skenario kontaminasi rantai ketergantungan
Task: Tambah dukungan multi-currency ke payment service
Butuh: Refactor PaymentService dari singleton ke instance-based dulu
✗ Yang sering dilakukan:
PR: "Add multi-currency support"
→ Refactor PaymentService (500 baris)
→ Tambah CurrencyConverter (200 baris)
→ Update payment API (150 baris)
→ Update test (100 baris)
Total: ~950 baris dengan dua konteks yang sangat berbeda
✓ Yang seharusnya dilakukan:
PR 1: "refactor(payment): convert PaymentService to instance-based"
→ Murni structural refactor, behavior tidak berubah
→ Test yang ada tetap lulus sebagai bukti refactor aman
PR 2: "feat(payment): add multi-currency support"
→ Di atas pondasi yang sudah bersih
→ Reviewer fokus hanya pada logika currency, bukan detail implementasi singleton
Kontaminasi tipe 3: Housekeeping Terselip #
Terjadi ketika perubahan non-fungsional seperti update dependency, format ulang kode, atau rename variabel massal digabung dengan perubahan fungsional:
// ✗ Housekeeping yang terselip
PR: "Fix payment timeout bug"
File yang berubah:
- internal/payment/handler.go (fix yang sebenarnya — 15 baris)
- go.mod (update 3 dependency — tidak terkait)
- internal/payment/models.go (rename variabel — tidak terkait)
- README.md (update dokumentasi — tidak terkait)
Reviewer harus memisahkan mana yang relevan dengan bug fix
dan mana yang hanya housekeeping
// ✓ Yang seharusnya
PR 1: "fix(payment): increase timeout for large transaction processing" (15 baris)
PR 2: "chore: upgrade payment-related dependencies" (jika memang diperlukan)
Kontaminasi tipe 4: Scope Creep Selama Development #
Terjadi ketika scope PR berkembang seiring development karena menemukan hal-hal yang “perlu diperbaiki”:
// Siklus scope creep yang familiar
Hari 1: PR untuk fix A
Hari 2: "Sambil ini, saya fix B juga yang terkait"
Hari 3: "B ternyata butuh refactor C dulu"
Hari 4: "Sambil refactor C, saya tambah test D"
Hari 5: PR siap review: A + B + refactor C + test D = 600 baris
// Sinyal bahwa PR sedang mengalami scope creep
→ Judul PR sudah tidak lagi mendeskripsikan seluruh isi PR
→ Menambahkan file yang "tidak terkait langsung" tapi "perlu diubah juga"
→ PR sudah berjalan lebih dari 3 hari dan terus bertambah
// Cara menghentikan scope creep
Commit perubahan yang sudah ada
Buat tiket untuk perubahan baru yang ditemukan
Selesaikan PR yang sedang berjalan
Baru mulai PR baru untuk perubahan tambahan
Cara Menerapkan One PR, One Purpose #
Definisikan Tujuan Sebelum Membuka Editor #
Langkah paling efektif untuk mencegah PR yang terkontaminasi adalah mendefinisikan tujuan sebelum menulis satu baris kode.
Pertanyaan yang harus dijawab sebelum mulai coding:
1. Apa satu hal yang akan saya ubah?
→ "Fix token refresh agar tidak expire prematur"
2. Bagaimana saya tahu PR ini selesai?
→ "User tidak lagi di-logout secara tiba-tiba setelah 30 menit aktif"
3. File apa yang paling mungkin perlu diubah?
→ "auth/token_service.go dan auth/refresh_handler.go"
4. Apakah ada refactor yang dibutuhkan sebelum ini bisa diimplementasikan?
→ "Ya — TokenService perlu di-extract dulu. Ini PR terpisah."
Dengan menjawab empat pertanyaan ini, scope PR sudah terdefinisi
sebelum coding dimulai — dan scope creep jauh lebih mudah dihindari.
Gunakan Judul PR sebagai Komitmen #
Tulis judul PR sebelum mulai coding dan jadikan ia sebagai komitmen tentang apa yang akan ada dalam PR tersebut. Setiap perubahan yang tidak bisa dideskripsikan oleh judul tersebut seharusnya tidak ada di PR ini.
// Judul sebagai komitmen
Judul ditulis di awal: "fix(auth): prevent premature token expiry on active sessions"
Selama coding:
→ Temukan bug lain di auth service: buat tiket, jangan fix di PR ini
→ Ingin refactor helper function: buat PR terpisah setelah ini selesai
→ Ingin update dependency: ini bukan tujuan PR ini, buat PR chore terpisah
Hasil: PR yang di-submit persis sesuai dengan judul yang ditulis di awal
Pisahkan Berdasarkan Jenis Perubahan #
Panduan sederhana untuk memisahkan perubahan:
flowchart TD
A{Jenis perubahan} --> B[Structural only\nRefactor, rename, reorganisasi]
A --> C[Behavioral\nFeature, bug fix, performance]
A --> D[Non-functional\nDependency, format, docs, test]
B --> E[PR: refactor/\nTest yang ada harus tetap lulus]
C --> F[PR: feat/ atau fix/\nTest baru ditambahkan]
D --> G[PR: chore/ atau docs/\nBiasanya bisa paralel]
E -.->|pondasi untuk| F| Tipe | Prefix | Bisa Dicampur Dengan |
|---|---|---|
| Refactor struktural | refactor: | Tidak bisa dicampur — selalu PR sendiri |
| Feature baru | feat: | Tidak dengan refactor atau bug fix tidak terkait |
| Bug fix | fix: | Tidak dengan refactor atau feature baru |
| Update dependency | chore: | Bisa dengan update dependency lain, tidak dengan kode |
| Format / cleanup | style: | Bisa dengan cleanup lain, tidak dengan logic change |
| Dokumentasi | docs: | Bisa dengan docs lain, tidak dengan logic change |
Gunakan “Parking Lot” untuk Ide yang Muncul Selama Coding #
Saat coding, selalu akan ada hal-hal yang terlihat dan terasa perlu diperbaiki. Alih-alih langsung menambahkannya ke PR yang sedang berjalan, buat “parking lot” — tempat sementara untuk ide-ide tersebut:
// Parking lot sederhana yang efektif
Option 1: Komentar TODO dengan tiket
// TODO(PROJ-456): nama variabel ini membingungkan, rename dalam PR tersendiri
Option 2: Langsung buat tiket
Buka Jira/Linear → buat tiket → catat nomor tiket → lanjut coding
Option 3: Draft PR
Commit perubahan ke branch baru → buka sebagai Draft PR → kembali ke PR utama
Yang tidak boleh dilakukan:
→ Langsung mengerjakan di branch yang sama
→ "Nanti hapus sebelum PR" — tidak pernah terjadi
Selamatkan PR yang Sudah Terlanjur Besar #
Tidak selalu memungkinkan untuk menghindari PR yang sudah terlanjur besar. Ketika ini terjadi, ada beberapa cara untuk menyelamatkannya:
flowchart TD
A[PR sudah terlanjur besar] --> B{Apakah ada bagian\nyang bisa dipisah\ntanpa merusak yang lain?}
B -- Ya --> C[Pisah menjadi beberapa PR kecil]
C --> D[Merge yang paling independen dulu]
D --> E[PR tersisa menjadi lebih kecil dan fokus]
B -- Tidak --> F{Apakah big PR ini\nlegitimate?}
F -- Ya --> G[Tambahkan panduan review\ndi deskripsi PR]
F -- Tidak --> H[Pertimbangkan ulang scope\napakah bisa dipecah dengan feature flag?]Langkah menyelamatkan PR yang sudah terlanjur besar:
1. Identifikasi perubahan yang paling independent
→ Biasanya: dependency update, formatting, rename variabel
2. Pindahkan ke branch baru
git checkout -b chore/cleanup-from-feature-x
git cherry-pick <commit-yang-relevan>
3. Submit sebagai PR terpisah — ini biasanya bisa langsung di-merge
4. PR asli menjadi lebih kecil dan lebih fokus
5. Jika masih terlalu besar: pertimbangkan feature flag untuk
memungkinkan bagian yang independen di-merge ke main lebih awal
Anti-Pattern yang Harus Dihindari #
// ✗ Judul yang ambigu memperbolehkan scope yang tidak terbatas
"Update auth service" → apa saja bisa masuk ke PR ini
// ✓ Judul yang spesifik mendefinisikan batas yang jelas
"Fix token refresh to prevent premature logout"
// ✗ "Biar sekalian" saat mengerjakan PR
"Sudah buka file ini, biar saya fix yang lain juga"
// ✓ Buat tiket untuk hal yang ditemukan, jangan kerjakan di PR yang sama
// ✗ Refactor dan feature dalam satu PR
Reviewer tidak bisa memisahkan mana yang mengubah structure
dan mana yang mengubah behavior
// ✓ Dua PR: refactor dulu (test lama harus lulus), baru feature di atasnya
// ✗ Dependency update dicampur dengan logic change
Update grpc-go + tambah endpoint baru dalam satu PR
→ Sulit menentukan apakah bug berasal dari dependency baru atau logic baru
// ✓ Pisahkan: chore PR untuk dependency, feat PR untuk endpoint baru
// ✗ Menunggu PR "sempurna" sebelum pisah
"Saya akan pisah setelah semuanya selesai"
→ Tidak pernah terjadi karena terlalu banyak yang perlu dipindahkan
// ✓ Pisah sejak awal, sebelum perubahan menumpuk
Checklist One PR, One Purpose #
SEBELUM MULAI CODING:
□ Tujuan PR sudah didefinisikan dalam satu kalimat yang spesifik
□ Judul PR sudah ditulis dan akan menjadi komitmen scope
□ Sudah diidentifikasi: apakah butuh refactor dulu sebelum bisa implementasi?
□ Jika iya: refactor menjadi PR terpisah yang harus selesai duluan
SELAMA CODING:
□ Setiap perubahan baru yang muncul dievaluasi: apakah sesuai dengan tujuan PR?
□ Perubahan yang tidak sesuai masuk ke parking lot (tiket / draft PR)
□ Tidak ada perubahan "biar sekalian" yang tidak terkait tujuan utama
□ Judul PR masih akurat setelah semua perubahan ditambahkan
SEBELUM MEMBUKA PR UNTUK REVIEW:
□ Baca seluruh diff dan pastikan setiap file yang berubah relevan dengan tujuan
□ Tidak ada file yang berubah karena "kebetulan dibuka" — bukan karena tujuan PR
□ Judul PR masih bisa mendeskripsikan seluruh isi PR dalam satu kalimat
□ Deskripsi PR fokus pada satu tujuan — tidak menyebut "juga", "serta", "dan juga"
TANDA PR YANG SUDAH FOKUS:
□ Reviewer bisa memahami tujuan PR sebelum membaca satu baris diff
□ Jika PR ini di-revert, hanya satu perubahan logis yang hilang
□ Test yang ditambahkan semuanya berkaitan dengan tujuan utama PR
Ringkasan #
- One PR, One Purpose bukan soal ukuran — tapi soal fokus — PR dengan 500 baris bisa tetap memiliki satu tujuan yang jelas jika semua perubahannya melayani tujuan yang sama.
- Test paling sederhana: bisakah tujuan PR dijelaskan dalam satu kalimat tanpa kata “dan”? — jika butuh lebih dari satu kalimat, PR kemungkinan sudah memiliki lebih dari satu tujuan.
- Definisikan tujuan sebelum membuka editor — ini adalah langkah tunggal yang paling efektif untuk mencegah scope creep dan kontaminasi tujuan.
- Judul PR adalah komitmen — setiap perubahan yang tidak bisa dideskripsikan oleh judul tersebut seharusnya tidak ada di PR ini.
- Pisahkan refactor dari feature — ini adalah pemisahan paling fundamental yang paling sering dilanggar. Refactor mengubah structure; feature mengubah behavior — keduanya membutuhkan jenis review yang berbeda.
- Housekeeping (dependency, format, rename) selalu PR terpisah — mencampurnya dengan logic change membuat akar penyebab bug sulit ditelusuri.
- Gunakan parking lot untuk ide yang muncul selama coding — tiket, draft PR, atau komentar TODO dengan nomor tiket — bukan langsung mengerjakan di branch yang sama.
- PR yang terlanjur besar bisa diselamatkan — identifikasi bagian yang paling independent, pindahkan ke PR terpisah, dan PR utama menjadi lebih fokus.
- One PR, One Purpose adalah tentang menghormati reviewer — PR yang fokus mengatakan: “Saya sudah memikirkan batas perubahan ini dengan serius sehingga kamu bisa me-review dengan efektif.”