Spec Driven Development Part 2: Anatomi Spec yang Baik
Di Part 1, kita sudah membahas kenapa spec menjadi sumber kebenaran dalam Spec-Driven Development, dan bahwa spec yang efektif memuat empat elemen: intent, constraint, acceptance criteria, dan non-goals. Tapi mengetahui empat elemen ini saja belum cukup — banyak tim sudah menulis “spec” yang punya keempat bagian tersebut secara formal, tapi tetap menghasilkan kode yang melenceng dari yang dimaksud. Masalahnya ada di kualitas isi tiap elemen, bukan sekadar keberadaannya. Artikel ini membahas cara menulis tiap elemen secara konkret, notasi praktis untuk membuat acceptance criteria yang benar-benar testable, dan bagaimana mengenali spec yang terlihat rapi tapi sebenarnya lemah.
Kenapa Spec Buruk Lebih Berbahaya dari Tidak Ada Spec
Ini terdengar kontradiktif, tapi masuk akal kalau dipikirkan dari sisi psikologi tim. Ketika tidak ada spec sama sekali, semua orang — termasuk agent — tahu bahwa mereka sedang berimprovisasi. Ada kewaspadaan ekstra saat review, karena semua pihak sadar ambiguitas itu nyata.
Spec yang buruk menghilangkan kewaspadaan ini. Dokumen yang terlihat terstruktur, dengan heading “Requirements” dan “Acceptance Criteria”, memberi rasa aman palsu. Reviewer mengasumsikan karena ada spec, maka kode pasti sudah sesuai spec — padahal spec itu sendiri penuh ambiguitas yang membuat agent (atau developer) bebas menginterpretasikan sesuka hati.
Ciri-ciri spec lemah yang paling umum:
- Bahasa subjektif tanpa ukuran — kata seperti “cepat”, “aman”, “user-friendly” tanpa definisi konkret apa artinya cepat, aman, atau user-friendly dalam konteks ini
- Acceptance criteria yang tidak bisa diuji — kriteria yang ditulis sebagai pernyataan umum, bukan kondisi yang bisa dicek pass/fail
- Constraint yang diasumsikan, bukan dinyatakan — penulis spec menganggap “tentu saja sudah jelas”, padahal agent tidak punya akses ke asumsi implisit itu
- Scope yang hanya menjelaskan apa yang dibangun, bukan apa yang tidak dibangun — membuka ruang bagi agent untuk menambahkan fitur yang tidak diminta
Spec yang penuh heading dan terlihat formal bukan jaminan kualitas. Ukuran sebenarnya adalah: bisakah dua orang (atau dua agent) yang berbeda membaca spec ini dan menghasilkan kode yang sama persis secara fungsional? Jika tidak, spec itu masih ambigu.
Sebelum masuk ke detail tiap elemen, berikut bagaimana keempatnya saling berhubungan dalam satu spec yang utuh:
flowchart TD
A[Intent: kenapa ini dibangun] --> B[Constraint: batasan yang tidak boleh dilanggar]
B --> C[Acceptance Criteria: kapan dianggap selesai]
C --> D[Non-Goals: apa yang eksplisit di luar scope]
D --> E[Spec Siap Direview]
A -.menjadi konteks keputusan saat ambigu.-> CIntent memberi konteks yang dipakai untuk mengambil keputusan ketika acceptance criteria tidak mencakup setiap kasus. Constraint dan non-goals sama-sama berfungsi sebagai batas, tapi dari arah berlawanan — constraint membatasi bagaimana implementasi boleh dilakukan, non-goals membatasi apa yang termasuk dalam scope.
Elemen 1: Intent
Intent menjawab pertanyaan “kenapa fitur ini perlu dibangun” — bukan sekadar “apa yang harus dibangun”. Ini elemen yang paling sering dilewatkan karena terasa seperti basa-basi, padahal intent adalah konteks yang membantu agent (dan reviewer manusia) mengambil keputusan yang tepat ketika spec tidak mencakup setiap kemungkinan kasus.
Intent yang lemah biasanya hanya mengulang judul fitur:
Intent: Membuat fitur reset password.
Ini bukan intent, ini label. Tidak ada informasi tentang masalah apa yang dipecahkan, siapa yang terdampak, atau kenapa sekarang.
Intent yang kuat menjelaskan masalah, dampak, dan urgensi:
Intent: Saat ini user yang lupa password harus menghubungi support secara
manual, yang memakan rata-rata 4 jam waktu respons dan menyumbang 23% dari
total tiket support bulanan. Fitur reset password mandiri akan menghilangkan
ketergantungan pada support untuk kasus ini, sekaligus meningkatkan keamanan
karena proses verifikasi terstandarisasi (saat ini verifikasi manual oleh
support rentan social engineering).
Perhatikan bedanya: versi kedua memberi agent pemahaman tentang apa yang penting dan kenapa. Ketika nanti ada keputusan implementasi yang tidak eksplisit disebutkan di spec — misalnya, apakah perlu rate limiting pada endpoint reset password — agent yang memahami intent (mengurangi risiko social engineering) lebih mungkin mengambil keputusan yang selaras dengan tujuan asli, dibanding agent yang hanya tahu “buat fitur reset password”.
Intent yang baik biasanya menjawab tiga hal sekaligus: masalah apa yang ada sekarang, siapa yang terdampak masalah itu, dan kenapa solusi ini sekarang menjadi prioritas.
Elemen 2: Constraint
Constraint adalah batasan yang harus dipatuhi implementasi — apapun caranya, batasan ini tidak boleh dilanggar. Constraint berbeda dari acceptance criteria: acceptance criteria mendefinisikan “kapan dianggap selesai”, sementara constraint mendefinisikan “batas yang tidak boleh dilewati sepanjang proses”, termasuk hal yang mungkin tidak langsung terlihat dari hasil akhir.
Constraint biasanya jatuh ke beberapa kategori:
| Kategori | Contoh |
|---|---|
| Teknis | Harus kompatibel dengan database existing, tidak boleh menambah dependency baru tanpa approval |
| Keamanan | Token reset password harus expired dalam 15 menit, tidak boleh log password dalam bentuk apapun |
| Performa | Endpoint harus merespons di bawah 200ms pada p95, query tidak boleh full table scan |
| Kompatibilitas | API harus backward compatible dengan versi mobile app yang masih digunakan 5% user aktif |
| Regulasi/compliance | Data personal harus bisa dihapus permanen sesuai permintaan user (right to erasure) |
Bagian yang sering terlewat: constraint harus dinyatakan eksplisit meskipun “menurutmu sudah jelas”. Agent tidak punya pengalaman bertahun-tahun bekerja di codebase yang sama seperti developer senior di tim. Asumsi yang menurut developer senior “ya pasti begitu” perlu ditulis, karena bagi agent itu bukan hal yang pasti sama sekali.
ANTI-PATTERN (constraint diasumsikan):
"Implementasikan endpoint reset password seperti biasa."
BENAR (constraint eksplisit):
Constraint:
- Token reset harus berupa UUID v4, disimpan dengan hash (bukan plaintext)
di database, dan kedaluwarsa 15 menit setelah dibuat
- Endpoint harus dibatasi rate limit 3 request per email per jam
- Tidak boleh mengirim informasi apakah email terdaftar atau tidak
(mencegah email enumeration)
- Harus menggunakan email service yang sudah ada (SendGrid), tidak boleh
menambah provider baru
Constraint keamanan adalah kategori yang paling berbahaya jika diasumsikan, bukan dinyatakan. Agent bisa menghasilkan kode yang lolos semua acceptance criteria fungsional tapi membuka celah keamanan, karena tidak ada constraint eksplisit yang melarangnya. Selalu nyatakan constraint keamanan secara eksplisit, jangan berasumsi itu “akal sehat”.
Elemen 3: Acceptance Criteria
Acceptance criteria mendefinisikan kapan sebuah implementasi dianggap selesai dan benar — dan ini harus bisa diverifikasi secara objektif, idealnya otomatis. Inilah elemen yang paling langsung terhubung ke testing, dan akan dibahas lebih dalam di Part 5 series ini.
Cara paling umum gagal menulis acceptance criteria adalah menulisnya sebagai pernyataan kualitatif:
ANTI-PATTERN:
- Sistem harus menangani error dengan baik
- Reset password harus aman
- Performa harus cepat
Tidak ada satupun dari tiga baris ini yang bisa dicek pass/fail oleh siapapun, termasuk agent. “Menangani dengan baik” menurut siapa? “Aman” dari ancaman apa? “Cepat” berapa milidetik?
Salah satu notasi yang cukup populer untuk menulis acceptance criteria yang testable adalah EARS — Easy Approach to Requirements Syntax. EARS menyediakan beberapa pola kalimat standar yang memaksa requirement ditulis dengan kondisi dan hasil yang jelas:
| Pola EARS | Format | Kapan dipakai |
|---|---|---|
| Ubiquitous | “Sistem harus [perilaku]” | Requirement yang selalu berlaku, tanpa kondisi |
| Event-driven | “Ketika [trigger], sistem harus [perilaku]” | Respons terhadap kejadian tertentu |
| State-driven | “Selama [state], sistem harus [perilaku]” | Perilaku yang bergantung pada kondisi sistem saat ini |
| Unwanted behavior | “Jika [kondisi tidak diinginkan], maka sistem harus [perilaku]” | Penanganan error atau kasus invalid |
| Optional | “Dimana [fitur opsional tersedia], sistem harus [perilaku]” | Perilaku yang hanya berlaku jika fitur tertentu aktif |
Dengan pola ini, tiga baris acceptance criteria yang buruk di atas bisa ditulis ulang menjadi:
BENAR (format EARS):
- Ketika user mengirim email yang terdaftar ke endpoint reset password,
sistem harus mengirim email berisi link reset dalam waktu maksimal 5 detik
- Ketika user mengirim email yang tidak terdaftar, sistem harus tetap
merespons dengan pesan sukses yang sama (mencegah email enumeration)
- Jika token reset sudah kedaluwarsa, maka sistem harus menolak request
dengan pesan "link sudah tidak berlaku" dan kode HTTP 410
- Jika token reset sudah pernah dipakai, maka sistem harus menolak request
kedua dengan pesan yang sama dan token harus langsung dihapus dari database
- Selama proses reset berlangsung, sistem harus mencatat audit log berisi
timestamp dan IP address (tanpa mencatat password atau token dalam bentuk
plaintext)
Setiap baris di versi ini bisa langsung diturunkan menjadi satu atau lebih test case. Tidak ada ruang interpretasi tentang kapan kriteria itu terpenuhi atau tidak.
Trik cepat mengecek apakah acceptance criteria sudah cukup baik: coba bayangkan kamu menulis automated test dari kriteria itu tanpa bertanya apapun ke siapapun. Kalau kamu masih perlu klarifikasi untuk tahu apa yang harus di-assert, kriteria itu belum cukup spesifik.
Elemen 4: Non-Goals
Non-goals menyatakan secara eksplisit apa yang tidak termasuk dalam scope pekerjaan ini. Elemen ini sering dianggap tidak perlu — “kan sudah jelas dari judul fiturnya” — padahal justru di sinilah salah satu sumber over-engineering paling umum dari AI agent.
Agent yang diberi spec tanpa non-goals cenderung menginterpretasikan secara luas. Diminta membuat “reset password”, agent bisa saja menambahkan fitur “ubah password tanpa reset” sekalian, atau membangun sistem notifikasi email yang lebih kompleks dari yang dibutuhkan, karena merasa itu “related” dan “membantu”. Tanpa batas eksplisit, ekspansi scope ini terjadi diam-diam dan baru ketahuan saat review — atau lebih buruk, tidak ketahuan sama sekali sampai jadi technical debt.
Non-Goals:
- TIDAK mencakup perubahan password dari halaman profil (sudah ada,
endpoint terpisah)
- TIDAK mencakup notifikasi push atau SMS — hanya email
- TIDAK mencakup audit dashboard untuk melihat history reset password
(akan jadi fitur terpisah)
- TIDAK mengubah skema tabel users yang sudah ada — hanya menambah
tabel baru password_reset_tokens
Non-goals juga berguna sebagai alat komunikasi dengan stakeholder non-teknis. Ketika seseorang bertanya “kenapa fitur X tidak ada”, jawabannya sudah tertulis di spec sejak awal — ini bukan oversight, tapi keputusan sadar.
Tanpa non-goals, scope cenderung melebar diam-diam selama eksekusi, bukan langsung dari awal:
flowchart LR
A[Scope Diminta] -->|tanpa non-goals| B[Agent Interpretasi Luas]
B --> C[Fitur Tambahan 'Related']
C --> D[Scope Membesar Tanpa Disadari]
A -->|dengan non-goals eksplisit| E[Agent Tetap di Batas]
E --> F[Scope Sesuai Rencana]Contoh Spec Lengkap (Studi Kasus)
Berikut contoh spec markdown generik yang menggabungkan keempat elemen, untuk fitur reset password yang sudah dipakai sebagai contoh di atas. Format ini tidak terikat bahasa pemrograman atau tool tertentu — bisa diadaptasi ke format spec kit manapun yang dipakai tim.
# Spec: Reset Password Mandiri
## Intent
Saat ini user yang lupa password harus menghubungi support secara manual,
yang memakan rata-rata 4 jam waktu respons dan menyumbang 23% dari total
tiket support bulanan. Fitur reset password mandiri akan menghilangkan
ketergantungan pada support untuk kasus ini, sekaligus meningkatkan
keamanan karena proses verifikasi terstandarisasi.
## Constraint
- Token reset harus UUID v4, disimpan dengan hash (bukan plaintext) di
database, kedaluwarsa 15 menit setelah dibuat
- Endpoint harus dibatasi rate limit 3 request per email per jam
- Tidak boleh membocorkan informasi apakah email terdaftar atau tidak
- Harus menggunakan email service yang sudah ada (SendGrid), tidak boleh
menambah provider baru
- Tidak boleh mengubah skema tabel users yang sudah ada
## Acceptance Criteria
- Ketika user mengirim email yang terdaftar ke endpoint reset password,
sistem harus mengirim email berisi link reset dalam waktu maksimal 5 detik
- Ketika user mengirim email yang tidak terdaftar, sistem harus tetap
merespons dengan pesan sukses yang sama
- Jika token reset sudah kedaluwarsa, sistem harus menolak request dengan
HTTP 410 dan pesan "link sudah tidak berlaku"
- Jika token reset sudah pernah dipakai, sistem harus menolak request kedua
dan langsung menghapus token dari database
- Ketika user submit password baru dengan token valid, sistem harus
menyimpan password baru (ter-hash) dan menghapus token tersebut
- Jika password baru tidak memenuhi kebijakan password (minimal 8 karakter,
kombinasi huruf dan angka), sistem harus menolak dengan pesan validasi
yang jelas
- Selama proses reset berlangsung, sistem harus mencatat audit log berisi
timestamp dan IP address, tanpa mencatat password atau token dalam
bentuk plaintext
## Non-Goals
- TIDAK mencakup perubahan password dari halaman profil
- TIDAK mencakup notifikasi push atau SMS — hanya email
- TIDAK mencakup audit dashboard untuk history reset password
- TIDAK mengubah skema tabel users yang sudah ada
Spec sepanjang ini terlihat memakan waktu untuk ditulis, tapi bandingkan dengan waktu yang biasanya hilang untuk siklus “generate — review — revisi — generate ulang” yang berulang ketika spec-nya samar. Investasi di awal ini hampir selalu lebih murah.
Spec Buruk vs Spec Baik — Perbandingan Langsung
Untuk melihat dampaknya secara langsung, berikut requirement yang sama, ditulis dengan dua kualitas berbeda.
Skenario: fitur pencarian produk di e-commerce
SPEC BURUK:
Buat fitur search produk yang cepat dan relevan. Hasil pencarian harus
akurat dan user experience-nya baik. Pastikan performanya optimal
meskipun data produk banyak.
Spec ini tidak memberi tahu agent: apa itu “cepat” dalam angka, bagaimana relevansi diukur, field apa saja yang dicari, bagaimana menangani typo, atau apa yang terjadi jika hasil kosong. Agent akan mengisi semua celah ini dengan asumsinya sendiri — dan asumsi itu kemungkinan besar berbeda dari yang dibayangkan penulis spec.
SPEC BAIK:
Intent: User saat ini kesulitan menemukan produk karena search hanya
mencocokkan judul produk secara exact match, menyebabkan 31% pencarian
tidak menghasilkan hasil meski produk yang dicari ada di katalog.
Constraint:
- Harus menggunakan Elasticsearch yang sudah ter-deploy, tidak boleh
menambah search engine baru
- Index harus ter-update maksimal 5 menit setelah produk diubah
- Tidak boleh expose field internal (cost price, supplier info) di
response API
Acceptance Criteria:
- Ketika user mencari dengan kata kunci yang ada di judul atau deskripsi
produk, sistem harus mengembalikan produk tersebut dalam 10 hasil teratas
- Ketika user mencari dengan typo ringan (edit distance maksimal 2),
sistem harus tetap menampilkan hasil yang relevan
- Sistem harus merespons dalam maksimal 300ms pada p95 untuk katalog
hingga 1 juta produk
- Jika tidak ada hasil yang cocok, sistem harus menampilkan 5 produk
terlaris di kategori yang sama sebagai rekomendasi
- Hasil pencarian harus diurutkan berdasarkan relevance score, dengan
produk yang stok habis ditampilkan terakhir
Non-Goals:
- TIDAK mencakup pencarian berdasarkan gambar (visual search)
- TIDAK mencakup personalisasi hasil berdasarkan history user
- TIDAK mengubah struktur index Elasticsearch yang dipakai fitur lain
Versi kedua memberi agent batas yang jelas untuk bekerja: teknologi apa yang dipakai, seberapa toleran terhadap typo, target performa dalam angka, dan apa yang eksplisit di luar scope. Hasil implementasi dari spec ini jauh lebih predictable dibanding versi pertama.
Anti-Pattern Umum dalam Menulis Spec
Selain spec yang terlalu samar, ada juga arah sebaliknya yang sama bermasalahnya.
Over-specification — spec yang ditulis sampai level pseudocode, mendikte struktur kode baris demi baris. Ini menghilangkan keuntungan utama bekerja dengan agent: kemampuannya mencari pendekatan implementasi yang baik. Spec seharusnya mendefinisikan apa yang harus benar, bukan bagaimana caranya baris per baris, kecuali memang ada constraint teknis yang mengharuskan pendekatan tertentu.
ANTI-PATTERN (over-specified):
Buat fungsi bernama validateEmail yang menerima parameter string,
lalu di dalam fungsi buat variabel regex, lalu gunakan regex.test(),
lalu jika false return objek {valid: false, error: "invalid"}...
BENAR:
Acceptance Criteria:
- Jika format email tidak valid (tidak ada @ atau domain), sistem harus
menolak input dengan pesan error yang menyebutkan field mana yang salah
Under-specification — kebalikannya, spec yang terlalu abstrak sehingga semua keputusan penting diserahkan ke agent. Ini biasanya terjadi ketika penulis spec terburu-buru dan menganggap detail “akan jelas dengan sendirinya saat implementasi”. Tanda paling jelas: acceptance criteria yang bisa diinterpretasikan lebih dari satu cara oleh dua orang berbeda.
Spec yang kontradiktif — terjadi terutama pada spec yang ditulis bertahap atau hasil edit berulang. Misalnya constraint menyebutkan “tidak boleh menambah dependency baru” tapi acceptance criteria mensyaratkan fitur yang secara praktis butuh library tertentu. Agent yang menemukan kontradiksi ini biasanya akan memilih salah satu secara acak, atau lebih buruk, mencoba “mengakali” keduanya dengan solusi yang aneh. Review spec sebelum eksekusi (fase kedua dari loop yang dibahas di Part 1) ada justru untuk menangkap kontradiksi semacam ini sebelum kode ditulis.
Tidak ada formula pasti soal “seberapa detail” sebuah spec harus ditulis. Panduan praktisnya: detail secukupnya untuk menghilangkan ambiguitas yang penting, tapi cukup longgar untuk membiarkan agent memilih pendekatan implementasi yang baik dalam batas constraint yang ada.
Ringkasan
- Spec yang terlihat formal tidak otomatis berkualitas — ukuran sebenarnya adalah apakah dua pihak berbeda bisa menghasilkan kode yang sama persis secara fungsional dari spec yang sama
- Intent harus menjelaskan masalah, siapa yang terdampak, dan kenapa sekarang — bukan sekadar mengulang judul fitur
- Constraint harus dinyatakan eksplisit, terutama untuk kategori keamanan, karena agent tidak punya akses ke asumsi implisit yang dimiliki developer senior
- Acceptance criteria harus bisa diverifikasi objektif; notasi EARS (ubiquitous, event-driven, state-driven, unwanted behavior, optional) membantu menulis kriteria yang langsung bisa diturunkan jadi test case
- Non-goals mencegah scope creep dan over-engineering dari agent yang cenderung menginterpretasikan requirement secara luas
- Hindari dua anti-pattern yang berlawanan: over-specification (mendikte implementasi sampai level pseudocode) dan under-specification (terlalu abstrak hingga semua keputusan penting diserahkan ke agent)
- Spec yang kontradiktif — misalnya constraint dan acceptance criteria yang saling bertentangan — harus ditangkap saat fase review, sebelum eksekusi dimulai