One PR, One Purpose #

Ada pertanyaan sederhana yang bisa langsung mengungkapkan apakah sebuah PR sehat atau tidak: “PR ini tentang apa?” Jika jawabannya bisa diungkapkan dalam satu kalimat yang spesifik — “PR ini menambahkan rate limiting ke endpoint login” — PR itu punya satu tujuan yang jelas. Jika jawabannya membutuhkan list panjang atau kalimat dengan banyak “dan” — “PR ini refactor auth service, sekalian fix bug timeout, update beberapa dependency, dan ganti nama beberapa variabel” — itu adalah multi-purpose PR.

Multi-purpose PR bukan hanya masalah ukuran. PR yang kecil pun bisa melanggar prinsip ini jika mencampur jenis perubahan yang berbeda. Dan PR yang besar pun bisa sah-sah saja jika semua perubahan di dalamnya melayani satu tujuan yang kohesif. Yang dipersoalkan adalah kejelasan niat — apakah reviewer bisa dengan cepat membangun satu model mental yang utuh tentang apa yang coba diselesaikan PR ini, mengapa, dan bagaimana cara memverifikasinya.

Prinsip One PR, One Purpose adalah tentang menghormati waktu dan energi kognitif reviewer, menjaga git history agar bermakna, dan membangun kebiasaan berpikir terstruktur yang membuat seluruh tim bergerak lebih efektif.

Apa yang Dimaksud dengan “Satu Tujuan” #

“Satu tujuan” bukan berarti satu file atau satu fungsi. Sebuah PR boleh menyentuh banyak file dan banyak fungsi — selama semua perubahan itu melayani satu tujuan yang sama dan tidak bisa dipahami secara terpisah.

Yang membedakan satu tujuan dari banyak tujuan adalah koherensi: apakah semua perubahan di PR ini bisa dijelaskan oleh satu intent yang sama?

Test koherensi — apakah PR ini punya satu tujuan?

  Pertanyaan: "Kenapa semua perubahan ini ada dalam satu PR?"

  Jawaban yang menunjukkan satu tujuan:
  → "Semuanya diperlukan untuk mengimplementasikan rate limiting di login endpoint"
  → "Semuanya bagian dari refactor UserService ke functional style"
  → "Semuanya perubahan yang dibutuhkan untuk fix bug double-charge di payment"

  Jawaban yang menunjukkan banyak tujuan:
  → "Yang pertama fix bug, yang ini refactor yang sudah lama mau dikerjakan,
     yang itu update dependency karena kebetulan buka file-nya"
  → "Agak campur-campur, tapi semuanya di area auth service"
  → Harus membaca judul PR + seluruh deskripsi + diff untuk bisa menjawab

Tes paling sederhana: apakah kamu bisa menghapus sebagian perubahan di PR ini dan sisa PR-nya masih masuk akal? Jika ya, bagian yang bisa dihapus itu kemungkinan besar termasuk tujuan yang berbeda dan seharusnya ada di PR terpisah.


Mengapa Multi-Purpose PR Merusak Lebih dari yang Terlihat #

Dampak multi-purpose PR tidak hanya terasa saat review berlangsung. Ia meninggalkan jejak yang bertahan jauh lebih lama.

Terhadap Kualitas Review #

Ketika satu PR mencampur beberapa jenis perubahan, reviewer harus melakukan context switching di tengah review. Mereka sedang memikirkan apakah logika rate limiting sudah benar, lalu tiba-tiba harus beralih mengevaluasi apakah refactor naming convention sudah konsisten, lalu beralih lagi ke dependency update.

Setiap context switch membuang sebagian working memory. Model mental yang sedang dibangun harus sebagian dibuang untuk mengakomodasi konteks yang baru. Hasilnya: review yang lebih dangkal di semua bagian.

graph TD
    A[Reviewer membuka PR] --> B[Membangun model mental: Rate Limiting]
    B --> C{Tiba-tiba: refactor di file lain}
    C --> D[Context switch: model mental baru]
    D --> E{Tiba-tiba: dependency update}
    E --> F[Context switch lagi]
    F --> G[Working memory hampir habis]
    G --> H[Review menjadi dangkal]
    H --> I["LGTM" tanpa yakin]

Terhadap Git History #

Git history yang baik adalah dokumentasi yang hidup — kamu bisa melakukan git log dan memahami evolusi sistem. Kamu bisa melakukan git bisect untuk menemukan commit yang memperkenalkan sebuah bug. Kamu bisa melakukan git blame pada sebuah baris dan memahami mengapa perubahan itu dibuat.

Multi-purpose PR merusak semua ini. Ketika satu PR berisi “fix bug + refactor + update deps”, commit history menjadi tidak bisa diandalkan untuk investigasi.

Git log yang bermakna (one PR, one purpose):

  a3f1c2e  feat(auth): add rate limiting to login endpoint
  b8d4e91  fix(payment): handle double-charge on network retry
  c2a9f14  refactor(user): convert UserService to functional style
  d71b3c8  chore(deps): upgrade redis client to v5.0

  → Membaca log ini, kamu langsung tahu apa yang berubah kapan
  → git bisect mudah: commit mana yang memperkenalkan bug X?
  → git blame bermakna: baris ini diubah karena alasan Y

Git log yang tidak bermakna (multi-purpose PR):

  a3f1c2e  Various auth improvements and fixes
  b8d4e91  Update some files
  c2a9f14  Fix stuff and refactor
  d71b3c8  PR changes

  → Tidak ada informasi yang bisa diekstrak dari log ini
  → git bisect tidak berguna — setiap commit menyentuh terlalu banyak hal
  → Investigasi bug membutuhkan membaca seluruh diff, bukan hanya log

Terhadap Rollback dan Debugging #

Ketika sebuah deployment menyebabkan masalah, kemampuan rollback yang cepat dan tepat adalah perbedaan antara insiden kecil dan bencana. PR dengan satu tujuan bisa di-revert secara surgical — revert hanya PR yang bermasalah, tanpa menyentuh yang lain.

flowchart LR
    subgraph "One Purpose PR — Rollback Mudah"
        A1[PR: Rate Limiting] --> B1[Bermasalah?]
        B1 -->|Ya| C1[Revert hanya PR ini]
        C1 --> D1[Sistem pulih, fitur lain tidak terdampak]
    end

    subgraph "Multi-Purpose PR — Rollback Berisiko"
        A2[PR: Rate Limiting + Refactor + Deps] --> B2[Bermasalah?]
        B2 -->|Ya| C2[Harus revert semua atau tidak sama sekali]
        C2 --> D2[Refactor yang sudah benar ikut ter-revert]
        C2 --> E2[Dependency update ikut ter-revert]
        C2 --> F2[Tim kehilangan pekerjaan yang tidak bermasalah]
    end

Empat Jenis Perubahan yang Sering Dicampur #

Ada empat jenis perubahan yang paling sering secara tidak disengaja dicampur dalam satu PR, padahal masing-masing membutuhkan cara review yang berbeda.

1. Feature dan Bug Fix #

Ini adalah pencampuran yang paling umum. Developer menemukan bug saat mengerjakan fitur baru, memperbaikinya “sekalian”, dan memasukkan keduanya dalam satu PR.

✗ Dicampur dalam satu PR:
  PR: "Add user export feature"
  ├── feat: tambah endpoint /users/export
  ├── fix: perbaiki pagination yang salah di /users (bug tidak terkait)
  └── test: tambah test untuk export dan pagination

  Masalah:
  → Bug fix di pagination tidak ada hubungannya dengan export feature
  → Jika export feature perlu di-revert, bug fix ikut ter-revert
  → Reviewer tidak tahu mana yang perlu fokus: feature atau bug fix?

✓ Dipisah menjadi dua PR:
  PR 1: "fix(user): correct off-by-one error in pagination"
  → Di-merge duluan, independen, bisa masuk ke hotfix release

  PR 2: "feat(user): add CSV export endpoint"
  → Bisa dikerjakan paralel atau setelah PR 1 di-merge

2. Refactor dan Behavior Change #

Ini adalah pencampuran yang paling berbahaya dari sudut pandang review. Refactor seharusnya tidak mengubah behavior — jika test yang ada tetap lulus setelah refactor, refactor itu aman. Tapi ketika refactor dan behavior change dicampur, reviewer tidak bisa menggunakan tes sebagai validator kebenaran.

✗ Dicampur: refactor + behavior change
  PR: "Improve payment processing"
  ├── Rename processPayment() → handleTransaction()
  ├── Extract validation logic ke PaymentValidator class
  ├── Ubah retry logic dari 3x ke 5x (behavior change!)
  └── Tambah logging

  Masalah:
  → Reviewer harus memisahkan mana yang "hanya rename/extract"
    dan mana yang "mengubah cara sistem bekerja"
  → Jika ada bug setelah merge, tidak jelas: dari refactor atau dari behavior change?
  → Test gagal tidak informatif: karena rename atau karena logika berubah?

✓ Dipisah:
  PR 1: "refactor(payment): extract validation to PaymentValidator"
  → Test yang ada harus tetap lulus (zero behavior change)
  → Reviewer bisa verify: "apakah semua test masih hijau?" → selesai

  PR 2: "fix(payment): increase retry attempts from 3 to 5"
  → Perubahan spesifik dengan alasan yang jelas
  → Reviewer fokus: "apakah 5 retry masuk akal secara bisnis dan teknis?"

3. Fungsional dan Housekeeping #

Housekeeping — formatting, rename massal, update dependency, reorganisasi file — adalah perubahan yang tidak mengubah behavior tapi mengubah tampilan atau struktur kode. Mencampur ini dengan perubahan fungsional menciptakan noise dalam diff yang menyulitkan reviewer menemukan perubahan yang penting.

✗ Dicampur: feature + housekeeping
  PR: "Add order tracking feature"
  Diff stats: 47 files changed, 1,240 insertions, 890 deletions

  Setelah diperiksa:
  → 800 deletion dan 800 insertion adalah reformat seluruh file yang disentuh
  → Actual feature change: ~100 baris
  → Reviewer harus memilah mana yang "real change" dari noise formatting

✓ Dipisah:
  PR 1: "chore: reformat files in order module" (automated, bisa di-approve cepat)
  PR 2: "feat(order): add real-time tracking endpoint"
  → PR 2 hanya berisi 100 baris perubahan nyata — review 10 menit, selesai

4. Fitur Utama dan Persiapan Infrastruktur #

Seringkali sebuah fitur membutuhkan persiapan infrastruktur terlebih dahulu — tabel database baru, abstraksi baru, konfigurasi baru. Mencampur persiapan ini dengan implementasi fitur membuat PR sulit dianalisis.

✗ Dicampur: infra + feature
  PR: "Add notification system"
  ├── Tambah tabel notifications di database
  ├── Tambah NotificationRepository
  ├── Tambah NotificationService dengan business logic
  ├── Tambah endpoint /notifications
  └── Integrasi dengan UserService dan OrderService

✓ Dipisah:
  PR 1: "feat(notification): add notifications schema and repository"
  → Reviewer fokus: "apakah schema sudah benar?"

  PR 2: "feat(notification): implement notification service"
  → Reviewer fokus: "apakah business logic sudah benar?"

  PR 3: "feat(notification): add notification endpoints and integrations"
  → Reviewer fokus: "apakah API contract dan integrasi sudah benar?"

Cara Mendefinisikan Purpose Sebelum Menulis Kode #

Salah satu kebiasaan yang paling efektif untuk memastikan PR punya satu tujuan adalah mendefinisikan tujuan tersebut sebelum mulai menulis kode — bukan setelah kode selesai.

Workflow yang membangun disiplin one PR, one purpose:

  Sebelum menulis kode:
  1. Tulis judul PR di kepala (atau draft judul di deskripsi)
     "feat(auth): add rate limiting to login endpoint"
  2. Definisikan scope: apa yang masuk, apa yang tidak
     Masuk: middleware, Redis counter, test
     Tidak masuk: refactor error handling yang sudah lama mau dikerjakan

  Saat menulis kode:
  3. Setiap kali ingin menambahkan sesuatu yang "sekalian":
     Tanya: "Apakah ini melayani tujuan PR yang sudah didefinisikan?"
     Jika tidak → buat catatan di TODO list, bukan di PR ini

  Saat membuka PR:
  4. Baca ulang judul yang sudah didefinisikan di awal
     Apakah semua yang ada di diff melayani judul itu?
     Jika ada yang tidak → keluarkan

Teknik sederhana yang membantu: sebelum commit apapun, tulis judul PR di sticky note atau komentar di atas branch. Gunakan itu sebagai filter — setiap perubahan yang tidak bisa dimasukkan ke dalam judul itu perlu dipertanyakan.


Cara Memecah PR yang Sudah Terlanjur Besar #

Terkadang kamu sudah menulis banyak kode sebelum menyadari PR-nya sudah terlalu bercampur. Memecah PR di tengah jalan terasa tidak nyaman, tapi jauh lebih baik dari mewariskan PR yang sulit direview.

flowchart TD
    A[PR sudah terlanjur besar dan multi-purpose] --> B[Identifikasi jenis perubahan]
    B --> C{Ada refactor murni?}
    C -- Ya --> D[Ekstrak ke PR terpisah\nBuat branch baru dari main\nCherry-pick commit refactor]
    C -- Tidak --> E{Ada housekeeping?}
    E -- Ya --> F[Ekstrak ke PR terpisah\nFormatting, rename, deps]
    E -- Tidak --> G{Ada bug fix tidak terkait?}
    G -- Ya --> H[Ekstrak ke PR terpisah\nBisa jadi hotfix]
    G -- Tidak --> I[PR sudah lebih bersih\nPeriksa apakah masih bisa dipecah lagi]
    D --> I
    F --> I
    H --> I

Langkah praktis memecah PR yang sudah ada:

Cara memecah PR yang sudah terlanjur besar:

  Situasi: branch feature/big-pr sudah punya 30 commit

  Step 1: Identifikasi commit yang bisa dipisah
  git log --oneline feature/big-pr
  → Tandai commit mana yang refactor, mana yang feature, mana yang bug fix

  Step 2: Buat branch baru untuk bagian yang akan dipisah
  git checkout main
  git checkout -b fix/pagination-bug

  Step 3: Cherry-pick commit yang relevan
  git cherry-pick a3f1c2e  # commit yang berisi bug fix pagination

  Step 4: Buka PR dari branch baru
  → PR: "fix(user): correct off-by-one error in pagination"
  → Kecil, fokus, bisa di-review dan di-merge cepat

  Step 5: Kembali ke branch utama, hapus commit yang sudah dipisah
  git checkout feature/big-pr
  git rebase -i main
  → Drop commit yang sudah di-cherry-pick ke branch terpisah

  Step 6: Update PR utama
  → Sekarang PR utama hanya berisi perubahan yang memang seharusnya di sana

Hubungan dengan Single Responsibility Principle #

Prinsip One PR, One Purpose adalah cerminan dari Single Responsibility Principle (SRP) di level PR — sama seperti SRP menyatakan bahwa satu class seharusnya punya satu alasan untuk berubah, One PR, One Purpose menyatakan bahwa satu PR seharusnya punya satu alasan untuk ada.

Paralel antara SRP dan One PR, One Purpose:

  SRP (di level class):
  ✗ UserService yang menangani: auth, profile update, notification, billing
  ✓ UserService hanya menangani user authentication
    ProfileService menangani profile update
    NotificationService menangani notification

  One PR, One Purpose (di level PR):
  ✗ PR yang berisi: fix bug + refactor + tambah feature + update deps
  ✓ PR fix-bug: hanya perbaikan bug yang spesifik
    PR refactor: hanya perubahan struktural tanpa behavior change
    PR feature: hanya implementasi fitur baru
    PR deps: hanya update dependency

Keduanya didorong oleh motivasi yang sama: ketika sebuah unit bertanggung jawab atas terlalu banyak hal, ia menjadi sulit dipahami, sulit diuji, dan sulit diubah. Sebuah PR yang punya satu tujuan lebih mudah direview, lebih mudah di-revert, dan lebih mudah dipahami dari git history.


Tanda-tanda PR Melanggar Prinsip Ini #

Ada beberapa sinyal yang bisa langsung mengindikasikan bahwa sebuah PR melanggar One PR, One Purpose:

Sinyal pelanggaran One PR, One Purpose:

  Di judul dan deskripsi:
  ✗ Judul menggunakan kata "and", "also", "sekalian", "plus"
    "Fix bug and refactor UserService"
  ✗ Deskripsi berisi beberapa bagian yang tidak saling terkait
  ✗ Tidak bisa meringkas PR dalam satu kalimat tanpa "dan"

  Di diff:
  ✗ Ada file yang disentuh yang tidak ada hubungannya dengan tujuan utama
  ✗ Ada formatting change massal yang menciptakan noise
  ✗ Ada rename yang tidak terkait dengan perubahan logika

  Saat review berlangsung:
  ✗ Reviewer bertanya "ini bagian dari apa?" untuk beberapa bagian berbeda
  ✗ Ada komentar di bagian yang "kebetulan" disentuh
  ✗ Reviewer merasa perlu lebih dari satu sesi untuk menyelesaikan review

  Di git history setelah merge:
  ✗ Tidak bisa mendeskripsikan commit dalam satu kalimat yang bermakna
  ✗ git bisect menunjuk ke commit ini untuk beberapa bug yang tidak terkait

Anti-Pattern yang Harus Dihindari #

✗ Anti-pattern 1: "Sekalian refactor karena sudah buka file-nya"
  Refactor yang tidak terkait dengan tujuan PR dimasukkan
  karena kebetulan file yang sama dibuka.
  ✓ Buat catatan bahwa refactor ini perlu dikerjakan.
    Buka PR terpisah khusus untuk refactor tersebut.

────────────────────────────────────────────────────────────────────────────

✗ Anti-pattern 2: "Fix bug kecil sekalian"
  Bug ditemukan saat mengerjakan feature, langsung diperbaiki
  dalam branch yang sama tanpa dipisahkan.
  ✓ Commit bug fix ke branch terpisah (bisa dari main).
    Bug fix yang penting justru perlu di-merge lebih cepat
    daripada menunggu feature selesai.

────────────────────────────────────────────────────────────────────────────

✗ Anti-pattern 3: Formatting massal yang menciptakan noise
  Auto-formatter dijalankan pada seluruh file yang disentuh,
  menciptakan ratusan baris formatting change yang menutupi perubahan nyata.
  ✓ Jalankan formatter hanya pada file yang benar-benar dimodifikasi.
    Atau buat PR formatting terpisah sebelum mulai mengerjakan feature.

────────────────────────────────────────────────────────────────────────────

✗ Anti-pattern 4: Update dependency "sekalian"
  Dependency diupdate karena kebetulan diperhatikan saat mengerjakan PR lain.
  ✓ Dependency update adalah PR tersendiri dengan changelog yang jelas.
    Ini memudahkan rollback jika update dependency menyebabkan masalah.

────────────────────────────────────────────────────────────────────────────

✗ Anti-pattern 5: "Nanti dipecah kalau ada waktu"
  PR besar dibiarkan tidak dipecah karena memecahnya terasa merepotkan.
  Hasilnya PR menganggur karena tidak ada yang mau mulai mereviewnya.
  ✓ Waktu terbaik untuk memecah PR adalah sekarang, bukan nanti.
    PR yang terlanjur besar lebih baik dipecah sekarang
    daripada di-approve tanpa dibaca.

Checklist Review One PR, One Purpose #

SEBELUM MEMBUKA PR:
  □ Tujuan PR bisa diringkas dalam satu kalimat tanpa menggunakan "dan"
  □ Semua perubahan di diff melayani tujuan tersebut
  □ Tidak ada refactor yang dicampur dengan behavior change
  □ Tidak ada housekeeping (formatting, rename massal) yang dicampur dengan
    perubahan fungsional
  □ Tidak ada bug fix yang tidak terkait dengan tujuan utama PR

SAAT MENULIS KODE:
  □ Setiap kali ingin menambahkan sesuatu "sekalian", tanya:
    "Apakah ini melayani tujuan PR yang sudah didefinisikan?"
  □ Perubahan yang tidak terkait dicatat di backlog, bukan dimasukkan ke PR

SEBAGAI REVIEWER:
  □ Jika ada bagian PR yang tidak jelas hubungannya dengan judul,
    tanyakan kepada author: "Mengapa ini ada di PR ini?"
  □ Jika PR jelas melanggar one purpose, minta author untuk memecahnya
    sebelum melanjutkan review

EVALUASI AKHIR:
  □ Apakah PR ini bisa direvert tanpa merevert hal yang tidak terkait?
  □ Apakah git history setelah merge ini akan bermakna?
  □ Apakah reviewer berikutnya yang membaca git log akan memahami
    apa yang berubah dari commit ini?

Ringkasan #

  • Satu tujuan = satu kalimat yang spesifik — jika judul PR butuh “dan” untuk mendeskripsikan isinya, itu tanda ada lebih dari satu tujuan. Pecah menjadi PR terpisah.
  • Koherensi, bukan ukuran — one PR one purpose bukan soal berapa banyak baris atau file. PR besar bisa tetap punya satu tujuan, PR kecil bisa melanggar prinsip ini.
  • Multi-purpose PR merusak git history — commit yang tidak bisa dideskripsikan dengan satu kalimat bermakna membuat git bisect dan git blame tidak efektif.
  • Refactor dan behavior change harus selalu dipisah — refactor yang benar tidak mengubah behavior. Mencampur keduanya membuat reviewer tidak bisa menggunakan test sebagai validator dan debugging menjadi sulit.
  • Housekeeping adalah PR tersendiri — formatting massal, rename, dan dependency update menciptakan noise yang menyembunyikan perubahan nyata. Pisahkan selalu.
  • Bug fix tidak terkait perlu branch sendiri — bug yang ditemukan saat mengerjakan feature seringkali lebih penting dari feature itu sendiri. Memisahkannya memungkinkan bug fix di-merge lebih cepat sebagai hotfix.
  • Definisikan tujuan sebelum menulis kode — judul PR yang ditulis sebelum mulai coding adalah filter yang efektif: setiap perubahan yang tidak bisa dimasukkan ke dalam judul itu tidak seharusnya ada di PR ini.
  • Cherry-pick adalah alat untuk memecah PR yang terlanjur besar — identifikasi commit yang bisa dipisah, buat branch baru, cherry-pick, buka PR terpisah.
  • Prinsip ini adalah SRP di level PR — sama seperti satu class seharusnya punya satu alasan untuk berubah, satu PR seharusnya punya satu alasan untuk ada.
  • Memecah PR bukan tanda kegagalan — justru sebaliknya. Tim yang disiplin memecah PR menunjukkan kedewasaan engineering yang menghargai kualitas review dan keberlangsungan codebase.

← Sebelumnya: Small vs Big Pull Request   Berikutnya: Code Review →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact