YAGNI #
Ada pola yang sangat umum di codebase: fungsi dengan parameter reserved yang tidak pernah diisi, field database metadata_json yang kosong di setiap row, interface dengan method yang tidak pernah dipanggil, dan abstraksi berlapis untuk satu implementasi yang tidak pernah diganti. Semua itu adalah sisa dari keputusan “kita pasti akan butuh ini suatu hari nanti” — yang ternyata tidak pernah tiba. YAGNI — You Aren’t Gonna Need It — adalah prinsip dari Extreme Programming yang mengingatkan engineer bahwa kode yang belum dibutuhkan adalah utang, bukan investasi. Setiap baris kode yang ditulis harus dibaca, dipahami, ditest, dan dimaintain. Kode spekulatif memiliki semua biaya itu tanpa memberikan nilai apapun sampai (kalau) kebutuhannya benar-benar muncul. Panduan ini membahas YAGNI dari biaya nyata kode spekulatif, contoh konkret anti-pattern dan solusinya, YAGNI pada level arsitektur sistem, kapan refactor adalah pilihan yang lebih baik, dan batas-batas yang tidak boleh dilewati atas nama YAGNI.
Apa Itu YAGNI? #
YAGNI adalah singkatan dari You Aren’t Gonna Need It — jangan mengimplementasikan sesuatu sebelum benar-benar dibutuhkan. Prinsip ini berasal dari Extreme Programming (XP) dan Ron Jeffries, salah satu pendirinya.
“Always implement things when you actually need them, never when you just foresee that you need them.”
YAGNI bukan anti-perencanaan dan bukan berarti kode harus naif. Ada perbedaan penting antara dua hal:
Yang YAGNI larang: Yang YAGNI izinkan:
──────────────────────────────────── ────────────────────────────────────
Implementasi fitur yang belum Desain yang mudah diubah
ada requirement-nya
Abstraksi untuk use case Interface kecil yang jelas
yang belum ada
Method/parameter yang "mungkin Struktur folder yang terorganisir
nanti berguna"
Pilihan arsitektur untuk scale Kode yang mudah dites
yang belum terbukti dibutuhkan
Multi-provider untuk yang belum Error handling yang proper
ada provider kedua
Perbedaan kuncinya: YAGNI melarang implementasi spekulatif, bukan desain yang bersih. Kamu boleh mendefinisikan interface bahkan jika hari ini hanya ada satu implementasi — asalkan ada kebutuhan nyata untuk testability atau substitusi yang sudah terdokumentasi. Yang tidak boleh adalah membangun sistem registry+factory+config hanya karena “nanti pasti akan ada lebih banyak”.
Biaya Nyata dari Kode yang Tidak Dipakai #
YAGNI lahir dari pengalaman nyata bahwa kode spekulatif memiliki biaya yang jarang diperhitungkan.
Biaya langsung: waktu yang dihabiskan untuk menulis, mereview, dan mentest kode yang belum dibutuhkan. Setiap jam yang dihabiskan untuk fitur spekulatif adalah jam yang tidak dihabiskan untuk fitur yang benar-benar dibutuhkan pengguna.
Biaya maintenance: setiap baris kode harus dibaca oleh engineer yang join belakangan. Kode yang tidak dipakai memperburuk signal-to-noise ratio codebase — engineer harus memahami apakah kode itu masih relevan atau dead code.
Biaya perubahan yang salah arah: jika requirement akhirnya datang, kemungkinan besar bentuknya berbeda dari yang diantisipasi. Kode spekulatif yang dibangun berdasarkan asumsi yang salah justru menjadi beban yang harus di-refactor.
Timeline kode spekulatif:
Sprint 1: Engineer membangun multi-provider payment system
"Pasti nanti ada Xendit, DANA, GoPay..."
Waktu: 3 hari extra
Sprint 2-10: Tidak ada requirement untuk provider kedua
3 hari extra = 3 hari yang bisa untuk fitur lain
Sprint 11: Requirement datang: tambah Xendit
Ternyata interface yang dibuat tidak sesuai
(Xendit punya flow 3-step, bukan 1-step seperti yang diasumsikan)
Waktu untuk refactor: 2 hari
Total overhead: 5 hari, lebih dari jika langsung build saat dibutuhkan
Contoh 1 — Payment Service #
Ini adalah contoh klasik YAGNI violation yang sering terjadi di awal project.
// ANTI-PATTERN: dibangun dengan asumsi "nanti pasti ada banyak provider"
// Padahal hari ini hanya ada Midtrans
type PaymentProvider interface {
Pay(ctx context.Context, amount int64, currency string) (*PaymentResult, error)
Refund(ctx context.Context, transactionID string, amount int64) error
Validate(ctx context.Context, transactionID string) (*ValidationResult, error)
GetStatus(ctx context.Context, transactionID string) (PaymentStatus, error)
}
type PaymentProviderRegistry struct {
providers map[string]PaymentProvider
}
func (r *PaymentProviderRegistry) Register(name string, p PaymentProvider) {
r.providers[name] = p
}
func (r *PaymentProviderRegistry) Get(name string) (PaymentProvider, error) {
p, ok := r.providers[name]
if !ok { return nil, fmt.Errorf("provider %s not found", name) }
return p, nil
}
type MidtransProvider struct { apiKey string }
func (m *MidtransProvider) Pay(...) (*PaymentResult, error) { /* implementasi */ }
func (m *MidtransProvider) Refund(...) error { return nil } // fitur refund belum ada requirement
func (m *MidtransProvider) Validate(...) (*ValidationResult, error) { return nil, nil } // belum dipakai
func (m *MidtransProvider) GetStatus(...) (PaymentStatus, error) { return "", nil } // belum dipakai
// Padahal yang dibutuhkan hari ini hanya: proses pembayaran via Midtrans
// BENAR: implementasi minimal sesuai kebutuhan nyata
type PaymentService struct {
midtransAPIKey string
httpClient *http.Client
}
func NewPaymentService(apiKey string) *PaymentService {
return &PaymentService{
midtransAPIKey: apiKey,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
func (s *PaymentService) CreateCharge(ctx context.Context, amount int64) (*ChargeResult, error) {
// Implementasi Midtrans yang konkret dan langsung
return s.callMidtrans(ctx, amount)
}
Kapan refactor ke interface adalah tepat? Ketika requirement provider kedua benar-benar datang, dan kamu sudah tahu bentuk interface yang benar berdasarkan dua implementasi nyata.
// Refactor yang tepat saat requirement datang:
// Sekarang ada Midtrans DAN Xendit dengan flow yang sudah dipahami
type PaymentGateway interface {
// Interface didefinisikan berdasarkan kebutuhan NYATA dari dua provider
CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error)
}
// Interface ini lebih akurat karena dibuat setelah memahami keduanya
Contoh 2 — API Request Filter #
Menambahkan filter fields yang belum ada use case-nya adalah YAGNI violation yang sangat umum.
// ANTI-PATTERN: filter yang sangat lengkap tapi hanya name yang dipakai
type ListUsersFilter struct {
Name *string // dipakai
Email *string // belum ada requirement
Age *int // belum ada requirement
Gender *string // belum ada requirement
City *string // belum ada requirement
Country *string // belum ada requirement
IsActive *bool // belum ada requirement
CreatedFrom *time.Time // belum ada requirement
CreatedTo *time.Time // belum ada requirement
Tags []string // belum ada requirement
}
// Setiap field tambahan perlu ditest, didokumentasikan, dan di-maintain
// BENAR: hanya apa yang dibutuhkan sekarang
type ListUsersFilter struct {
Name string // satu-satunya filter yang ada di requirement saat ini
}
// Saat ada requirement baru untuk filter email:
// 1. Tambahkan field Email string
// 2. Update query
// 3. Update test
// Tidak perlu mengubah apapun yang sudah ada
Contoh 3 — Database Schema #
YAGNI juga berlaku pada database design. Kolom spekulatif bukan hanya pemborosan storage — ia menciptakan confusion tentang semantics dan sulit dihapus setelah production.
-- ANTI-PATTERN: kolom spekulatif yang "mungkin nanti berguna"
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
total BIGINT NOT NULL,
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL,
-- Kolom-kolom berikut belum ada requirement
metadata_json TEXT, -- "nanti mungkin butuh extra data"
source_system VARCHAR(100), -- "mungkin nanti multi-channel"
external_ref VARCHAR(255), -- "mungkin nanti perlu external ID"
deleted_at TIMESTAMP, -- soft delete belum diminta
updated_by UUID, -- audit trail belum diminta
version INTEGER -- optimistic locking belum diminta
);
-- BENAR: hanya kolom yang ada requirement nyata
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
total BIGINT NOT NULL,
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL
);
-- Saat soft delete diperlukan: ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMP
-- Saat audit trail diperlukan: buat tabel audit terpisah
-- Mudah ditambah, tidak bisa dihapus tanpa migration yang kompleks
Kolom database jauh lebih sulit dihapus daripada ditambahkan — sekali ada data production di kolom tersebut, menghapusnya memerlukan data migration yang berisiko. Ini salah satu alasan YAGNI pada database schema sangat penting: tambahkan kolom saat ada kebutuhan nyata, bukan antisipasi.
Contoh 4 — YAGNI dalam Dart/Flutter #
Di Flutter, YAGNI sering dilanggar pada level state management dan widget API.
// ANTI-PATTERN: widget dengan terlalu banyak konfigurasi yang belum dipakai
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback? onTap;
final VoidCallback? onLongPress; // belum ada requirement
final VoidCallback? onDoubleTap; // belum ada requirement
final Color? backgroundColor; // belum ada requirement
final BorderRadius? borderRadius; // belum ada requirement
final EdgeInsets? padding; // belum ada requirement
final bool showBadge; // belum ada requirement
final String? badgeText; // belum ada requirement
final bool isAnimated; // belum ada requirement
const ProductCard({
required this.product,
this.onTap,
this.onLongPress,
this.onDoubleTap,
this.backgroundColor,
this.borderRadius,
this.padding,
this.showBadge = false,
this.badgeText,
this.isAnimated = false,
});
// ...
}
// BENAR: hanya parameter yang benar-benar diperlukan saat ini
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback onTap; // satu-satunya interaksi yang ada di requirement
const ProductCard({
required this.product,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Card(
child: Column(children: [
Image.network(product.imageUrl),
Text(product.name),
Text('Rp ${product.price}'),
]),
),
);
}
}
// Saat ada requirement untuk long press: tambahkan onLongPress?
// Saat ada requirement untuk badge: tambahkan showBadge dan badgeText
YAGNI dalam Arsitektur Sistem #
YAGNI paling sering dilanggar di level arsitektur — keputusan yang paling mahal untuk diubah.
Skenario startup dengan 10 user sehari:
ANTI-YAGNI:
Kubernetes cluster multi-region
Kafka untuk async processing
CQRS + Event Sourcing
Service mesh (Istio)
Dedicated auth service
Feature flag system
A/B testing infrastructure
Biaya: 3 bulan setup, tim kecil exhaust, fitur bisnis tertunda
YAGNI:
Monolith sederhana atau modular monolith
PostgreSQL single instance
Redis untuk cache
Background job dengan queue sederhana (database-backed)
Biaya: 2 minggu, tim fokus pada fitur bisnis
Kapan evolusi arsitektur: saat ada masalah nyata yang membutuhkannya
Panduan evolusi arsitektur berdasarkan YAGNI:
Traffic < 1000 req/day:
Monolith, single DB → cukup
Optimasi query sebelum scaling
Traffic 1k-100k req/day:
Read replica
Caching strategis
Background jobs untuk operasi berat
Traffic > 100k req/day:
Mulai pertimbangkan microservices untuk komponen yang butuh scale berbeda
Message queue untuk decoupling
Horizontal scaling
Bukan: "kita pasti akan dapat 1 juta user, siapkan dari awal"
Refactor adalah Solusi yang Lebih Baik dari Antisipasi #
Salah satu alasan engineer melanggar YAGNI adalah ketakutan: “jika kita tidak bangun sekarang, akan sangat mahal untuk mengubahnya nanti.” Tapi asumsi ini sering salah.
// Contoh refactoring yang sehat — tidak menakutkan jika kode terstruktur baik
// Versi 1 (YAGNI): hanya Midtrans
type PaymentService struct {
apiKey string
}
func (s *PaymentService) CreateCharge(ctx context.Context, amount int64) (*Charge, error) {
return s.callMidtrans(ctx, amount)
}
// Versi 2 (saat Xendit dibutuhkan): refactor yang terkontrol
type PaymentGateway interface {
CreateCharge(ctx context.Context, amount int64) (*Charge, error)
}
type PaymentService struct {
gateway PaymentGateway // sekarang bisa di-inject
}
func NewPaymentService(gateway PaymentGateway) *PaymentService {
return &PaymentService{gateway: gateway}
}
// Semua caller yang sudah ada tidak perlu berubah —
// hanya tempat di-construct yang berubah (dari:)
svc := &PaymentService{apiKey: key}
// (menjadi:)
svc := NewPaymentService(&MidtransGateway{apiKey: key})
// atau
svc := NewPaymentService(&XenditGateway{apiKey: key})
Refactor mudah dilakukan ketika kode memenuhi prinsip dasar yang lain (SRP, DI) — itu yang membuat YAGNI aman diterapkan. Kode yang terstruktur dengan baik bisa dievolusi tanpa rewrite besar.
Batas YAGNI yang Tidak Boleh Dilangkahi #
YAGNI bukan izin untuk mengabaikan hal-hal fundamental. Ada kategori hal yang selalu perlu meski belum ada requirement eksplisit.
SELALU perlu (bukan YAGNI violation):
□ Error handling yang proper — kode tanpa error handling adalah bug menunggu terjadi
□ Input validation — security dan data integrity bukan fitur spekulatif
□ Logging yang cukup untuk debugging — tanpa ini, production issues sulit dilacak
□ Timeout untuk network calls — blocking indefinitely bukan fitur, itu bug
□ Retry untuk transient errors — infrastruktur tidak selalu reliable
□ Unit test untuk business logic — test bukan fitur spekulatif
□ Basic security: SQL injection prevention, XSS prevention, dll
□ Graceful shutdown — proses yang tidak bisa dihentikan dengan bersih adalah masalah
BOLEH ditunda (YAGNI berlaku):
□ Admin dashboard — sampai ada operator yang butuhnya
□ Export CSV/Excel — sampai ada user yang minta
□ Multi-language support — sampai ada user non-Indonesia
□ Advanced filtering — sampai ada use case yang konkret
□ Audit trail lengkap — sampai ada requirement compliance
□ Microservices — sampai ada alasan scale yang jelas
Checklist YAGNI Sebelum Menulis Kode #
Sebelum menambahkan fitur/abstraksi, tanyakan:
□ Ada requirement tertulis atau ticket yang mendeskripsikan ini?
□ Ada user atau stakeholder yang konkret meminta ini sekarang?
□ Jika tidak diimplementasikan hari ini, apakah ada konsekuensi nyata?
□ Apakah ini bisa ditambahkan nanti tanpa harus rewrite besar?
Jika semua jawaban "tidak/tidak ada" → tunda.
Pengecualian:
□ Ini menyangkut security atau data integrity?
□ Ini menyangkut error handling atau stability?
□ Biaya untuk menambahkan nanti jauh lebih tinggi dari sekarang?
Jika ada satu jawaban "ya" → boleh lanjutkan.
Ringkasan #
- YAGNI berarti jangan implementasikan sebelum benar-benar dibutuhkan — kode spekulatif adalah utang dengan bunga: waktu tulis, waktu test, waktu maintain, dan kemungkinan harus diubah saat requirement nyata datang berbeda dari asumsi.
- Bukan anti-desain: YAGNI melarang implementasi spekulatif, bukan desain yang bersih. Interface untuk testability boleh, registry+factory untuk provider yang belum ada tidak.
- Payment service: mulai dengan implementasi konkret satu provider, refactor ke interface saat ada kebutuhan multi-provider yang nyata.
- Database schema: jangan tambahkan kolom spekulatif — lebih mudah menambah kolom saat dibutuhkan daripada menghapus kolom yang sudah ada data.
- API filter: tambahkan field filter hanya saat ada use case yang jelas, bukan antisipasi semua kemungkinan.
- Arsitektur: mulai dengan monolith yang terstruktur baik, evolusi ke microservices saat ada masalah scale yang nyata dan terukur.
- Refactor adalah normal: kode yang terstruktur dengan baik bisa dievolusi kapanpun dibutuhkan — ini yang membuat YAGNI aman.
- Batas yang tidak boleh dilangkahi: error handling, input validation, security dasar, timeout, logging, dan unit test bukan fitur spekulatif — selalu implementasikan.