YAGNI — You Aren’t Gonna Need It #
Ada sebuah pola pikir yang sangat umum di kalangan engineer yang sudah membaca banyak buku tentang arsitektur: “Nanti pasti akan berkembang, lebih baik kita siapkan dari awal.” Hasilnya adalah sistem yang baru punya lima endpoint tapi sudah menggunakan microservices, Kafka, dan CQRS — karena “nanti pasti butuh”. Atau struct dengan dua puluh field filter padahal endpoint hanya butuh satu. Atau payment service dengan interface berlapis untuk provider yang belum ada kontraknya. Kode ini tidak gratis: ia butuh waktu untuk ditulis, butuh waktu untuk dipahami, butuh waktu untuk di-maintain, dan sering kali tidak pernah benar-benar dipakai. YAGNI — You Aren’t Gonna Need It — adalah prinsip yang menyerang pola pikir ini secara langsung: jangan mengimplementasikan sesuatu sebelum benar-benar dibutuhkan. Bukan anti-perencanaan, bukan anti-arsitektur — tapi anti-asumsi berlebihan tentang masa depan yang belum terjadi.
Apa Itu YAGNI? #
YAGNI berasal dari praktik Extreme Programming (XP) yang dipopulerkan Ron Jeffries. Definisinya sederhana namun sering disalahpahami:
Jangan mengimplementasikan sesuatu sebelum benar-benar dibutuhkan.
Kata kunci yang sering terlewat adalah “benar-benar” — bukan “mungkin nanti”, bukan “kata PM bulan depan”, bukan “biasanya sistem seperti ini butuh”. YAGNI berbicara tentang kebutuhan yang sudah ada requirement tertulisnya, sudah ada use case konkretnya, dan sudah ada validasi dari stakeholder bahwa ini memang prioritas.
YAGNI bukan larangan untuk berpikir ke depan. Ada perbedaan penting yang perlu dipahami:
YAGNI MELARANG: YAGNI TIDAK MELARANG:
───────────────────────────────────── ────────────────────────────────────
Implementasi fitur yang belum Interface yang kecil dan jelas
ada requirement-nya untuk dependency yang ada
Abstraksi multi-provider ketika Struktur kode yang mudah diubah
baru ada satu provider konkret tanpa perlu rewrite besar
Field atau parameter yang "mungkin Error handling yang lengkap
nanti berguna" untuk path yang sudah ada
Layer arsitektur untuk scale yang Logging dan observability
belum ada indikator akan dicapai yang memang selalu dibutuhkan
Konfigurasi extensibility untuk Security controls yang
fitur yang tidak ada di roadmap merupakan requirement wajib
YAGNI dan extensible design bisa berjalan beriringan. Yang dilarang adalah implementasi fitur spekulatif — bukan desain yang mudah di-extend ketika waktunya tiba.
flowchart TD
Q1{"Ada requirement\ntertulis?"}
Q2{"Ada use case\nkonkret sekarang?"}
Q3{"Biaya TIDAK\nmembuatnya sekarang\nlebih besar dari\nmembuatnya?"}
BUILD["Buat sekarang ✓"]
DEFER["Tunda — YAGNI ✓"]
DESIGN["Desain agar\nmudah di-extend,\ntapi jangan\nimplementasi dulu"]
Q1 -->|Ya| Q2
Q1 -->|Tidak| DEFER
Q2 -->|Ya| BUILD
Q2 -->|Tidak| Q3
Q3 -->|Ya| BUILD
Q3 -->|Tidak| DESIGN
style BUILD fill:#5CB85C,color:#fff
style DEFER fill:#4C9BE8,color:#fff
style DESIGN fill:#F0AD4E,color:#fffEmpat Masalah Nyata Kode yang Belum Dibutuhkan #
Kode spekulatif terasa seperti investasi tapi berperilaku seperti utang. Ini empat biaya nyata yang ditimbulkannya:
1. Biaya waktu penulisan yang tidak menghasilkan value sekarang. Setiap jam yang dihabiskan untuk fitur yang belum dibutuhkan adalah satu jam yang tidak dihabiskan untuk fitur yang sudah ada di backlog dan sudah ada penggunanya. Dalam sprint yang kompetitif, ini adalah trade-off yang nyata.
2. Biaya pemahaman yang terus berulang. Kode yang ada di codebase harus dipahami oleh setiap engineer yang membacanya — sekarang dan di masa depan. Abstraksi berlapis untuk sesuatu yang tidak digunakan memperlambat onboarding dan meningkatkan cognitive load saat debug.
3. Biaya maintenance yang tidak terduga. Setiap baris kode adalah surface area untuk bug. Kode yang ditulis untuk fitur spekulatif tetap perlu diupdate ketika ada refactor, ketika ada perubahan dependency, ketika ada kebutuhan migrasi. Kode yang tidak memberikan value tapi tetap membutuhkan perawatan adalah definisi utang teknis.
4. Biaya asumsi yang salah. Yang paling berbahaya: fitur yang dibangun berdasarkan asumsi tentang masa depan sering kali tidak sesuai dengan masa depan yang sebenarnya terjadi. Ketika requirement akhirnya datang, bentuknya berbeda dari yang diasumsikan, dan abstraksi yang sudah dibangun justru menjadi penghalang, bukan kemudahan.
YAGNI di Level Struct dan Field #
Bentuk pelanggaran YAGNI yang paling sering muncul dan paling mudah masuk ke codebase tanpa terdeteksi di code review adalah penambahan field atau parameter yang “mungkin nanti berguna”.
// ANTI-PATTERN: struct filter dengan semua kemungkinan field
// padahal endpoint hanya butuh filter berdasarkan name
type UserFilter struct {
Name *string // ← dipakai sekarang
Email *string // ← "nanti pasti butuh"
Age *int // ← "mungkin untuk fitur segmentasi"
Gender *string // ← "kalau ada personalisasi"
City *string // ← "untuk regional targeting"
Country *string // ← "kalau expand ke luar negeri"
IsActive *bool // ← "untuk admin filter"
IsVerified *bool // ← "kalau ada verifikasi"
CreatedFrom *time.Time // ← "untuk laporan"
CreatedTo *time.Time // ← "untuk laporan"
Tags []string // ← "kalau ada tagging system"
}
// Masalah:
// - Setiap field *string perlu nil check di query builder
// - Dokumentasi API menjadi membingungkan — field mana yang benar-benar bekerja?
// - Test harus cover berbagai kombinasi yang belum tentu pernah dipakai
// - Jika schema database berubah, semua field ini jadi noise
// BENAR: hanya field yang ada requirement-nya sekarang
type UserFilter struct {
Name string // satu-satunya filter yang dibutuhkan saat ini
}
// Saat ada requirement baru untuk filter email:
// 1. Tambah field Email ke struct
// 2. Update query builder
// 3. Update dokumentasi
// Perubahan ini mudah dan terisolasi — tidak ada yang rusak
type UserFilter struct {
Name string
Email string
}
Aturan yang sama berlaku untuk parameter fungsi:
// ANTI-PATTERN: parameter opsional untuk semua kemungkinan behavior
func CreateUser(
name string,
email string,
role string, // ← "nanti pasti butuh role"
sendWelcomeEmail bool, // ← "mungkin ada yang tidak mau email"
skipValidation bool, // ← "untuk testing / admin"
notifySlack bool, // ← "kalau ada integrasi Slack"
auditLog bool, // ← "untuk compliance"
) error {
// Setiap boolean parameter menggandakan jumlah path yang perlu ditest
// 4 boolean = 16 kombinasi yang secara teoritis perlu diuji
}
// BENAR: hanya parameter yang ada kebutuhannya sekarang
func CreateUser(name, email string) error {
// Sederhana, jelas, mudah ditest
// Ketika butuh role: tambah parameter saat itu
// Ketika butuh welcome email: tambah saat fitur email selesai didesain
}
YAGNI di Level Service dan Abstraksi #
Kasus paling klasik YAGNI adalah abstraksi multi-provider sebelum ada provider kedua.
// ANTI-PATTERN: arsitektur untuk masa depan yang belum ada
// Dibuat karena "nanti pasti akan ada banyak payment provider"
type PaymentProvider interface {
Pay(ctx context.Context, amount int64, currency string) error
Refund(ctx context.Context, txID string, amount int64) error
Validate(ctx context.Context, txID string) (*PaymentStatus, error)
GetWebhookSecret() string
HandleWebhook(ctx context.Context, payload []byte) error
}
type PaymentProviderFactory interface {
Create(providerType string, cfg ProviderConfig) (PaymentProvider, error)
}
type providerRegistry struct {
mu sync.RWMutex
providers map[string]PaymentProvider
}
func (r *providerRegistry) Register(name string, p PaymentProvider) {
r.mu.Lock()
defer r.mu.Unlock()
r.providers[name] = p
}
type PaymentService struct {
registry *providerRegistry
factory PaymentProviderFactory
}
// Semua ini hanya untuk satu provider — Midtrans
// Fitur refund bahkan belum ada requirement-nya
// Interface Validate dan HandleWebhook belum pernah dipanggil dari manapun
// BENAR: sesuai kebutuhan nyata — satu provider, satu operasi yang ada
type PaymentService struct {
midtrans *midtrans.Client
}
func NewPaymentService(client *midtrans.Client) *PaymentService {
return &PaymentService{midtrans: client}
}
func (s *PaymentService) CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error) {
resp, err := s.midtrans.CreateTransaction(ctx, &midtrans.TransactionRequest{
OrderID: req.OrderID,
GrossAmount: req.Amount,
})
if err != nil {
return nil, fmt.Errorf("createCharge %s: midtrans: %w", req.OrderID, err)
}
return &ChargeResult{TransactionID: resp.TransactionID, PaymentURL: resp.RedirectURL}, nil
}
Saat kebutuhan kedua provider benar-benar muncul, refactor-nya jelas dan terfokus:
// YAGNI yang sehat: refactor KETIKA ada requirement nyata
// Trigger: Product memutuskan mendukung Xendit untuk merchant tertentu
// Langkah 1: ekstrak interface berdasarkan operasi yang benar-benar ada
type PaymentGateway interface {
CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error)
}
// Langkah 2: wrap implementasi yang sudah ada
type MidtransGateway struct{ client *midtrans.Client }
func (g *MidtransGateway) CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error) {
// implementasi yang sudah ada, pindahkan ke sini
}
// Langkah 3: tambah implementasi baru
type XenditGateway struct{ client *xendit.Client }
func (g *XenditGateway) CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error) {
// implementasi baru
}
// Langkah 4: update PaymentService untuk terima interface
type PaymentService struct {
gateway PaymentGateway // sekarang baru ada nilai nyata dari abstraksi ini
}
Refactor ini jauh lebih mudah karena interface-nya dibentuk oleh kebutuhan nyata — bukan oleh asumsi tentang apa yang “mungkin dibutuhkan”. Interface yang muncul dari refactor seperti ini cenderung lebih tepat dan lebih kecil dari yang didesain di awal.
sequenceDiagram
participant REQ as Requirement Datang
participant NOW as Kode Sekarang
participant REF as Refactor
participant NEW as Implementasi Baru
Note over REQ,NEW: Alur YAGNI yang Sehat
REQ->>NOW: "Buat payment — Midtrans saja dulu"
NOW->>NOW: PaymentService dengan *midtrans.Client langsung
Note over NOW: Sederhana, berfungsi, mudah ditest
REQ->>REF: "Butuh support Xendit untuk merchant enterprise"
REF->>REF: Ekstrak PaymentGateway interface
REF->>REF: Bungkus MidtransGateway
REF->>NEW: Buat XenditGateway
Note over REF,NEW: Refactor terfokus, interface tepat sasaranYAGNI di Level API Design #
YAGNI di desain API berarti endpoint yang dibuat hanya untuk use case yang sudah ada, dengan response shape yang sesuai kebutuhan saat ini — bukan dengan semua field yang “mungkin berguna”.
// ANTI-PATTERN: response dengan semua field yang "mungkin dibutuhkan client"
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"` // "nanti ada role-based UI"
Permissions []string `json:"permissions"` // "kalau ada fine-grained access"
LastLoginAt *time.Time `json:"last_login_at"` // "untuk security audit"
LoginCount int `json:"login_count"` // "untuk analytics"
ProfileScore float64 `json:"profile_score"` // "untuk recommendation engine"
Tags []string `json:"tags"` // "kalau ada tagging"
Metadata map[string]interface{} `json:"metadata"` // "untuk custom field"
// 10 field lagi yang belum pernah dibaca oleh client manapun
}
// Masalah:
// - Setiap field adalah contract yang harus dijaga kompatibilitasnya
// - Client yang di-generate otomatis menjadi bloated
// - Perubahan schema database berdampak luas karena banyak field yang di-expose
// - Tidak jelas field mana yang benar-benar dipakai client
// BENAR: hanya field yang dibutuhkan client yang ada sekarang
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Saat ada kebutuhan nyata: tambah field, versi API, atau buat endpoint baru
// Menambah field ke response lebih mudah dari menghapusnya (breaking change)
YAGNI di API design juga berarti tidak membuat endpoint yang belum ada penggunanya:
// ANTI-PATTERN: endpoint untuk fitur yang belum ada
router.GET("/api/v1/users/:id/recommendations", handleRecommendations) // tidak ada ML model
router.GET("/api/v1/users/:id/social-score", handleSocialScore) // belum ada konsep ini
router.POST("/api/v1/users/:id/import", handleBulkImport) // tidak ada UI untuk ini
router.GET("/api/v1/analytics/cohort", handleCohortAnalysis) // belum ada requirement
// BENAR: hanya endpoint yang ada penggunanya sekarang
router.POST("/api/v1/users", handleCreateUser)
router.GET("/api/v1/users/:id", handleGetUser)
router.PUT("/api/v1/users/:id", handleUpdateUser)
YAGNI di Level Database Schema #
Pelanggaran YAGNI yang paling mahal secara jangka panjang sering terjadi di schema database — karena migrasi database jauh lebih mahal dari refactor kode.
-- ANTI-PATTERN: tabel dengan kolom spekulatif
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
-- kolom yang ada requirement-nya sekarang selesai di sini --
role VARCHAR(50), -- "nanti ada RBAC"
tier VARCHAR(20), -- "nanti ada tier system"
score DECIMAL(5,2), -- "untuk gamification"
referral_code VARCHAR(20), -- "nanti ada referral program"
affiliate_id UUID, -- "nanti bisa jadi affiliate"
last_login_at TIMESTAMP, -- "untuk security dashboard"
login_count INTEGER DEFAULT 0, -- "untuk analytics"
extra_data JSONB, -- "catch-all untuk masa depan"
-- 8 kolom yang tidak pernah diisi, NULL semua, ikut di setiap query
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Masalah:
-- Setiap INSERT harus menyertakan atau mengabaikan kolom-kolom ini
-- Index planning menjadi kompleks karena banyak kolom NULL
-- JOIN dan query terasa lebih berat karena row size besar
-- Schema evolution menjadi confusing: kolom mana yang sudah "aktif"?
-- BENAR: hanya kolom yang ada kebutuhannya sekarang
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Saat RBAC dibutuhkan: migrasi menambah kolom role
-- ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'user';
-- Migrasi ini mudah, terisolasi, dan bisa di-rollback
YAGNI di Level Arsitektur Sistem #
Pelanggaran YAGNI di level arsitektur adalah yang paling visible dan paling sering jadi bahan debat di awal project. Pertanyaan “monolith atau microservices?” hampir selalu dijawab salah ketika YAGNI diabaikan.
ANTI-PATTERN: Arsitektur untuk scale yang belum ada
Aplikasi baru, 5 endpoint, 3 engineer, belum ada user — tapi:
✗ Microservices karena "nanti pasti harus scale per service"
✗ Kafka karena "nanti pasti butuh async processing"
✗ CQRS + Event Sourcing karena "nanti butuh audit trail"
✗ Redis Cluster karena "nanti traffic pasti besar"
✗ Kubernetes dengan HPA karena "nanti perlu auto-scale"
Biaya nyata:
- Waktu setup infrastruktur: 2–4 minggu
- Kompleksitas debugging distributed system
- Biaya cloud yang tidak sebanding dengan traffic
- Network overhead antar service untuk setiap request
- Engineer onboarding 3x lebih lama
BENAR: Mulai dengan arsitektur yang sesuai stage saat ini
✓ Monolith modular — mudah di-split jika benar-benar perlu
✓ REST API sederhana — tambah async ketika ada bottleneck nyata
✓ Database tunggal — pisahkan ketika ada kebutuhan isolation nyata
✓ Single instance — scale horizontal ketika ada traffic nyata
✓ Deployable ke satu VM — containerize ketika ada kebutuhan deployment nyata
flowchart LR
subgraph YAGNI["Evolusi yang Mengikuti YAGNI"]
direction TB
S1["Stage 1\nMonolith + DB tunggal\n(startup, validasi produk)"]
S2["Stage 2\nMonolith + Read replica\n(traffic mulai naik)"]
S3["Stage 3\nModular monolith\n+ beberapa service terpisah\n(tim mulai besar)"]
S4["Stage 4\nMicroservices untuk\nboundary yang jelas\n(scale berbeda per domain)"]
S1 -->|"traffic naik,\nprofiling tunjukkan\nbottleneck nyata"| S2
S2 -->|"tim > 15 orang,\ndeployment conflict\nmulai terjadi"| S3
S3 -->|"scale requirement\nper domain berbeda\nsecara signifikan"| S4
end
style S1 fill:#5CB85C,color:#fff
style S2 fill:#4C9BE8,color:#fff
style S3 fill:#F0AD4E,color:#fff
style S4 fill:#9B59B6,color:#fffSetiap transisi dipicu oleh kebutuhan nyata yang terukur — bukan oleh antisipasi. Biaya migrasi dari monolith ke microservices memang ada, tapi jauh lebih kecil dari biaya mempertahankan arsitektur yang terlalu kompleks untuk kebutuhan saat ini.
Kapan YAGNI Tidak Boleh Diterapkan Ekstrem #
YAGNI bukan alasan untuk mengabaikan hal-hal yang secara inheren selalu dibutuhkan. Ada kategori kode yang tidak pernah spekulatif, tidak peduli seberapa awal stage-nya.
SELALU DIBUTUHKAN — BUKAN YAGNI VIOLATION:
Security:
✓ Input validation — selalu dibutuhkan, bukan "nanti"
✓ SQL injection prevention — dari baris pertama kode
✓ Authentication untuk endpoint yang perlu auth
✓ Rate limiting untuk endpoint publik
Reliability:
✓ Error handling yang proper — termasuk edge case
✓ Timeout untuk semua external call
✓ Context cancellation untuk long-running operation
✓ Graceful shutdown
Observability:
✓ Logging untuk error dan operasi penting
✓ Health check endpoint
✓ Basic metrics untuk monitoring
Data integrity:
✓ Database constraint (NOT NULL, UNIQUE, FK) sesuai schema
✓ Transaction untuk operasi multi-step
✓ Idempotency untuk operasi yang bisa di-retry
// INI BUKAN YAGNI — selalu perlu, bahkan di sprint pertama:
func CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
// Input validation — BUKAN spekulatif
if req.UserID == "" {
return nil, errors.New("user_id is required")
}
if req.Amount <= 0 {
return nil, errors.New("amount must be positive")
}
// Timeout untuk external call — BUKAN spekulatif
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Transaction untuk operasi multi-step — BUKAN spekulatif
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("createOrder: begin tx: %w", err)
}
defer tx.Rollback()
order, err := orderRepo.SaveTx(ctx, tx, req)
if err != nil {
return nil, fmt.Errorf("createOrder: save order: %w", err)
}
if err := inventoryRepo.DeductTx(ctx, tx, req.Items); err != nil {
return nil, fmt.Errorf("createOrder: deduct inventory: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("createOrder: commit: %w", err)
}
// Logging untuk operasi penting — BUKAN spekulatif
slog.Info("order created", "order_id", order.ID, "user_id", req.UserID)
return order, nil
}
Cara membedakan “selalu dibutuhkan” dari “spekulatif”: apakah kode ini diperlukan agar sistem berfungsi dengan benar dan aman dalam use case yang ada sekarang? Jika ya — bukan YAGNI, wajib ada.
Refactor yang Sehat sebagai Pengganti Antisipasi #
YAGNI tidak berarti kode tidak pernah berubah. Justru sebaliknya — YAGNI mendorong refactor yang responsif terhadap kebutuhan nyata, bukan refactor preventif berdasarkan asumsi. Pola refactor yang sehat:
1. TULIS yang paling sederhana yang benar:
→ Implementasi langsung, konkret, tidak ada abstraksi prematur
2. VALIDASI bahwa ia bekerja:
→ Unit test, integration test, atau validasi ke pengguna
3. IDENTIFIKASI kebutuhan nyata yang muncul:
→ Requirement baru yang konkret, bukan asumsi
→ Duplikasi nyata yang perlu dihilangkan (DRY)
→ Bottleneck yang terukur, bukan yang dikira-kira
4. REFACTOR dengan tujuan yang jelas:
→ Ekstrak interface ketika ada implementasi kedua yang nyata
→ Tambah abstraksi ketika ada pengulangan knowledge
→ Pisahkan service ketika ada bottleneck scale yang terukur
5. ULANGI
Yang TIDAK dilakukan:
→ Refactor preventif "supaya nanti mudah diubah" tanpa kebutuhan konkret
→ Tambah abstraksi "supaya fleksibel" tanpa use case yang ada
→ Generalisasi untuk tipe yang belum ada
Hubungan YAGNI dengan Prinsip Lain #
flowchart TD
YAGNI["YAGNI\n(You Aren't Gonna Need It)"]
KISS["KISS\nJangan tambahkan\nkompleksitas yang\ntidak perlu"]
DRY["DRY\nAbstrak hanya\nknowledge yang\nbenar-benar berulang"]
SOLID["SOLID\nGunakan pattern\nhanya saat ada\nkebutuhan nyata"]
AGILE["Agile / XP\nDeliver value\nsekarang, iterasi\nberdasarkan feedback"]
YAGNI -->|"memperkuat"| KISS
YAGNI -->|"mencegah abstraksi\nprematur di"| DRY
YAGNI -->|"mencegah over-application\npada"| SOLID
YAGNI -->|"berasal dari dan\nmendukung"| AGILE
style YAGNI fill:#4C9BE8,color:#fff
style KISS fill:#5CB85C,color:#fff
style DRY fill:#5CB85C,color:#fff
style SOLID fill:#5CB85C,color:#fff
style AGILE fill:#F0AD4E,color:#fffHubungan YAGNI dengan DRY perlu perhatian khusus. DRY mendorong abstraksi untuk menghilangkan duplikasi — tapi abstraksi DRY yang dibuat terlalu awal bisa menjadi pelanggaran YAGNI. Solusinya adalah Rule of Three: biarkan kode terduplikasi sampai ada tiga occurrences, baru pertimbangkan abstraksi. Dua occurrences mungkin hanya kebetulan mirip.
YAGNI juga tidak bertentangan dengan SOLID. Terapkan SOLID ketika ada kebutuhan nyata yang diselesaikannya — interface untuk dependency yang memang akan ada implementasi lain, OCP untuk behavior yang memang akan di-extend. Jangan terapkan SOLID sebagai ritual.
Anti-Pattern dalam Satu Pandangan #
// ✗ Field yang "mungkin nanti berguna"
type CreateUserRequest struct {
Name string
Email string
Role string // belum ada RBAC
ReferralCode string // belum ada referral system
AffiliateSource string // belum ada affiliate tracking
}
// ✗ Interface multi-provider untuk satu provider
type NotificationProvider interface {
SendEmail(to, subject, body string) error
SendSMS(to, message string) error // belum ada SMS provider
SendPush(token, title, body string) error // belum ada push notif
SendWhatsApp(to, message string) error // belum ada WhatsApp
}
// Hanya ada email sekarang — tiga method lainnya belum pernah dipanggil
// ✗ Parameter boolean untuk semua kemungkinan behavior
func SendEmail(
to, subject, body string,
attachPDF bool, // belum ada fitur PDF
trackOpen bool, // belum ada email tracking
useTemplate bool, // belum ada template engine
) error {}
// ✗ Arsitektur untuk scale yang belum ada
// Microservices + Kafka + Redis Cluster untuk aplikasi dengan 10 user aktif
// ✗ Konfigurasi extensibility tanpa use case
type PluginConfig struct {
Enabled bool
MaxPlugins int
PluginPaths []string
HookPoints []string
}
// Tidak ada satu plugin pun yang ada atau direncanakan secara konkret
// ✗ Database kolom spekulatif
// score DECIMAL, referral_code VARCHAR, tier VARCHAR
// — semua NULL di semua row, tidak pernah dibaca
Checklist YAGNI Sebelum Commit #
SEBELUM MENAMBAHKAN FITUR ATAU ABSTRAKSI:
□ Ada requirement tertulis yang jelas untuk ini?
□ Ada use case konkret yang terjadi hari ini?
□ Ada stakeholder yang meminta ini secara eksplisit?
□ Jika tidak dibangun sekarang, apakah ada pengguna yang terdampak?
UNTUK INTERFACE DAN ABSTRAKSI:
□ Ada lebih dari satu implementasi yang nyata (atau di roadmap konkret)?
□ Ada use case testing yang membutuhkan mock dari interface ini?
□ Interface ini menutup boundary yang benar-benar perlu diisolasi?
UNTUK DATABASE SCHEMA:
□ Setiap kolom baru punya query yang membacanya?
□ Tidak ada kolom yang hanya "untuk jaga-jaga"?
□ Kolom nullable tidak digunakan sebagai workaround untuk schema yang belum jelas?
UNTUK ARSITEKTUR:
□ Kompleksitas arsitektur sebanding dengan masalah yang ada sekarang?
□ Ada metrik atau indikator nyata yang membenarkan kompleksitas ini?
□ Bisa dijelaskan ke engineer baru dalam <10 menit?
PENGECUALIAN (tidak berlaku YAGNI):
□ Security controls — selalu wajib ada
□ Error handling dan timeout — bukan spekulatif
□ Logging untuk operasi penting — bukan spekulatif
□ Data integrity constraint — bukan spekulatif
Ringkasan #
- YAGNI bukan anti-perencanaan — ia anti-asumsi berlebihan tentang masa depan yang belum terjadi. Perbedaan kuncinya: extensible design (desain yang mudah diubah) diizinkan; implementasi fitur spekulatif tidak.
- Empat biaya kode yang belum dibutuhkan: waktu penulisan yang tidak menghasilkan value, biaya pemahaman yang berulang untuk setiap pembaca, biaya maintenance yang terus berjalan, dan risiko asumsi yang ternyata salah.
- Di level struct dan field: tambahkan field atau parameter hanya saat ada requirement nyata. Setiap field
*stringatau*bool“untuk jaga-jaga” adalah kontrak yang harus dijaga dan surface area untuk bug.- Di level service dan abstraksi: mulai konkret dengan satu implementasi. Ekstrak interface hanya ketika ada kebutuhan nyata untuk implementasi kedua atau untuk mocking di test. Interface yang lahir dari refactor cenderung lebih tepat dari yang didesain di awal.
- Di level API: response shape dan endpoint hanya untuk use case yang sudah ada. Menambah field ke response lebih mudah dari menghapusnya (breaking change).
- Di level database schema: kolom spekulatif adalah utang paling mahal karena migrasi database mahal. Hanya buat kolom yang punya query yang membacanya.
- Di level arsitektur: mulai dengan arsitektur yang sesuai stage saat ini — monolith modular sebelum microservices, database tunggal sebelum sharding. Naikkan kompleksitas dipicu oleh metrik nyata, bukan antisipasi.
- YAGNI tidak berlaku untuk: security controls, error handling dan timeout, logging untuk operasi penting, dan data integrity constraint — ini bukan spekulatif, ini selalu dibutuhkan.
- Refactor responsif lebih sehat dari antisipasi: tulis yang paling sederhana, validasi ia bekerja, refactor ketika ada kebutuhan nyata yang muncul. “Bangun untuk kebutuhan sekarang, desain agar mudah berubah besok.”